4. The Angular feature

Read DESIGN.md at the repository root before building UI: it is the design system's specification, and new UI uses its tokens and the @blouppy/ui components.

The API client

One service per resource in front/application/src/app/api/, returning typed models, modelled on teams-api.service.ts. A feature never calls HttpClient itself:

@Injectable({ providedIn: 'root' })
export class ProjectsApiService {
  private readonly _httpClient = inject(HttpClient);

  get(workspaceId: number): Observable<Project[]> {
    return this._httpClient.get<Project[]>(`/api/v1/workspaces/${workspaceId}/projects`);
  }

  post(workspaceId: number, projectCreation: ProjectCreation): Observable<Project> {
    return this._httpClient.post<Project>(`/api/v1/workspaces/${workspaceId}/projects`, projectCreation);
  }
}

The paths are relative: the gateway serves the app and the API from one origin, and an interceptor adds the X-CSRF: 1 header it requires.

The feature

Under front/application/src/app/features/project/, following features/workspace/:

  • project.service.ts — state as signals (signal(), computed()): the app is zoneless, so a template only sees what is a signal.
  • project-list/project-list.component.ts — a standalone component, OnPush, with an inline template built from @blouppy/ui primitives and Tailwind classes.

The route

The organization leads the URL, by its slug, and a project lives under its workspace: /acme-corp/workspaces/12/projects. Add a child to the workspace's routes, in features/workspace/workspace.routes.ts, next to general and team:

{
  path: 'projects',
  loadComponent: () => import('app/features/project/project-list/project-list.component').then(m => m.ProjectListComponent),
},

Build every link with the helpers in core/routing/app.paths.ts — add 'projects' to the WorkspaceSection type and call workspacePath(slug, workspaceId, 'projects') — never with a literal URL.

The text

No user-facing string is hard-coded. Add the keys to both src/assets/i18n/en.json and fr.json, and use the pipe:

<h1>{{ 'project.title' | transloco }}</h1>

The status labels are a list built in TypeScript, so they are a computed() over translateSignal(...): that way they follow a language switch.

Add the new route to PAGES in tools/gallery-scenes.mjs, with the responses it needs in tools/gallery-fixtures.mjs — enough projects, with long names, to make the layout fail visibly — then look at it in both themes and three widths:

cd front/application
npm run build && npm run gallery -- --page=projects