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/uiprimitives 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.
The gallery
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