refactor(libs): graduate Icon / LayoutStateService / brand tokens to libs/shared/* (#99)
CI / check (push) Successful in 3m34s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 2m16s
CI / a11y (push) Successful in 2m3s
CI / perf (push) Successful in 3m34s

## Summary

Per ADR-0020 §"Shared-libs graduation": the three primitives that `portal-shell` (today) and `portal-admin` (next) will both share move out of `apps/portal-shell/src/` and into the workspace libs. Mechanical move — no behaviour change, prepares the ground for the admin-app PR.

## Graduated

| Primitive | Old location | New location |
|---|---|---|
| `Icon` component (+ `IconName` type) | `apps/portal-shell/src/app/components/icon/` | [`libs/shared/ui/src/lib/icon/`](libs/shared/ui/src/lib/icon/) |
| `LayoutStateService` (+ `ThemeMode` type) | `apps/portal-shell/src/app/state/` | [`libs/shared/state/src/lib/`](libs/shared/state/src/lib/) |
| Brand-palette `@theme` block | `apps/portal-shell/src/styles.css` | [`libs/shared/tokens/src/brand-tokens.css`](libs/shared/tokens/src/brand-tokens.css) |

## Notable changes

- **Icon selector renamed `app-icon` → `lib-icon`.** Required by the lib's ESLint `@angular-eslint/component-selector` rule (prefix `lib` for shared libs). All ~20 usages in `portal-shell` templates updated in one sweep.
- **New `shared-state` library** generated via `nx g @nx/angular:library libs/shared/state`. Mirrors the existing `feature-auth` / `shared-ui` shape (vite + Angular + spec setup). `tsconfig.base.json` path alias `shared-state` added by the generator.
- **Lib tags rebalanced** to satisfy the module-boundary lint rule:
  - `shared-state`: new lib → `scope:shared, type:shared`.
  - `shared-ui`: was `scope:portal-shell` (scaffold default) → `scope:shared, type:shared`.
- **`shared-tokens` is now CSS-only.** The placeholder TS function is gone; `index.ts` is an empty `export {}` with a TODO note. Vitest config flips `passWithNoTests: true` so the test target stops failing on the empty lib.
- **`portal-shell`'s `styles.css`** drops the inline `@theme {}` block and instead `@import`s `../../../libs/shared/tokens/src/brand-tokens.css`. Both apps will read the same source.
- Placeholder scaffolded files in `shared-ui/src/lib/shared-ui/` removed.

## Verification

- `nx run-many -t lint test build` across `portal-shell, shared-ui, shared-state, shared-tokens` — green.
- Tests redistributed: **portal-shell 28**, **shared-state 9**, **shared-ui 3**, **shared-tokens 0** = 40 total (same as before the move).
- Production build: **128.7 KB gzip** initial per locale (was 128.5 KB on `main` — noise-level delta).
- Brand-color CSS variables (`--color-brand-primary-500: #12546c`, …) still inlined in the produced `styles-*.css`.
- `dist/.../browser/{en,fr}/` emitted as expected.

## What this PR explicitly does NOT do

- Move other components (Header, Sidebar, Footer, ThemeSwitcher, LocaleSwitcher). Those are app-specific shell concerns; if `portal-admin` ends up needing them, they graduate in their own PR.
- Set up `portal-admin`. That's the next PR on the admin track — and now imports from `shared-ui` / `shared-state` / `shared-tokens` will Just Work.
- Refactor `feature-auth`'s tagging. It still says `scope:portal-shell`; revisit when the auth implementation actually lands and the dual-audience design (per ADR-0008) makes the right scope obvious.

## Test plan

- [x] Per-project run-many — green.
- [x] Production build emits both locales with brand tokens applied.
- [ ] Manual: `pnpm exec nx serve portal-shell` — the dev experience is unchanged.
- [ ] Manual: serve-static + locale switcher / theme switcher / sidebar collapse / navigation — all the behaviours the moved primitives drive still work in production.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #99
This commit was merged in pull request #99.
This commit is contained in:
2026-05-12 01:17:30 +02:00
parent 99522540a5
commit 8329fa133d
37 changed files with 267 additions and 125 deletions
@@ -29,7 +29,7 @@
<div
class="flex w-full max-w-xl items-center gap-2 rounded-full border border-gray-200 bg-gray-50 px-4 py-2 focus-within:border-brand-primary-500 focus-within:bg-white focus-within:ring-2 focus-within:ring-brand-primary-200 dark:border-gray-700 dark:bg-gray-800 dark:focus-within:border-brand-primary-300 dark:focus-within:bg-gray-900 dark:focus-within:ring-brand-primary-700"
>
<app-icon name="search" [size]="18" />
<lib-icon name="search" [size]="18" />
<input
id="global-search"
type="search"
@@ -47,7 +47,7 @@
i18n-aria-label="@@header.action.notifications"
aria-label="Notifications"
>
<app-icon name="bell" />
<lib-icon name="bell" />
<span
aria-hidden="true"
class="absolute right-2 top-2 h-2 w-2 rounded-full bg-brand-accent-400 ring-2 ring-white dark:ring-gray-900"
@@ -59,7 +59,7 @@
i18n-aria-label="@@header.action.help"
aria-label="Help"
>
<app-icon name="circle-help" />
<lib-icon name="circle-help" />
</button>
<app-theme-switcher />
<button
@@ -68,7 +68,7 @@
i18n-aria-label="@@header.action.settings"
aria-label="Settings"
>
<app-icon name="settings" />
<lib-icon name="settings" />
</button>
<span
i18n-aria-label="@@header.action.userMenu"
@@ -1,7 +1,7 @@
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { Header } from './header';
import { LayoutStateService } from '../../state/layout-state.service';
import { LayoutStateService } from 'shared-state';
describe('Header', () => {
beforeEach(async () => {
@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Icon } from '../icon/icon';
import { Icon } from 'shared-ui';
import { ThemeSwitcher } from '../theme-switcher/theme-switcher';
import { LayoutStateService } from '../../state/layout-state.service';
import { LayoutStateService } from 'shared-state';
/**
* Top-of-page banner.