# Research: UI Polish & Design System ## Decision 1: Design token delivery mechanism **Decision**: CSS custom properties declared on `:root` in `styles.css`. **Rationale**: Angular's default ViewEncapsulation.Emulated scopes component selectors with attribute hashes but does not block CSS custom property inheritance. `:root` variables cascade into every inline component style block without any special configuration. This is now the standard Angular 2025+ token approach and requires zero new dependencies. **Alternatives considered**: - SCSS variables — require a preprocessor; project currently uses plain CSS. - A shared `.css` import per component — works but adds per-component boilerplate and duplicates the token surface unnecessarily. --- ## Decision 2: Skeleton loading pattern **Decision**: Pure-CSS shimmer using `::after` pseudo-element animated gradient; no third-party library. **Rationale**: The spec assumption explicitly prohibits new icon/component libraries. The standard pure-CSS skeleton pattern (placeholder divs with a light-to-dark horizontal gradient sweeping via `@keyframes`) produces the same visual result as library skeletons with zero added dependencies. The `::after` approach requires no extra DOM nodes per skeleton block. **Pattern**: ```css @keyframes shimmer { from { background-position: -200% 0; } to { background-position: 200% 0; } } .skeleton { background: linear-gradient(90deg, var(--surface) 25%, var(--surface-raised) 50%, var(--surface) 75%); background-size: 200% 100%; animation: shimmer 1.4s infinite; border-radius: var(--radius); } ``` **Alternatives considered**: - `@angular/material` skeleton — adds the entire Material library as a dep. - CSS opacity pulse — simpler but less visually informative than a shimmer. --- ## Decision 3: Loading-flash debounce **Decision**: `timer(150).pipe(takeUntil(response$))` to gate the visibility of loading indicators. **Rationale**: Showing a spinner immediately causes a visible flash when the server responds in under ~150 ms (common on localhost). The idiomatic RxJS approach is to start a 150 ms timer alongside the real request; if the request completes first (`takeUntil`), the timer never fires and the spinner never appears. This avoids `race()` complexity and cleanly unsubscribes. **Implementation sketch**: ```typescript showSpinner = false; load(): void { const req$ = this.service.fetch().pipe(share()); timer(150).pipe(takeUntil(req$)).subscribe(() => { this.showSpinner = true; }); req$.subscribe(data => { this.showSpinner = false; /* handle data */ }); } ``` **Alternatives considered**: - `race([req$, timer(150)])` — fires timer regardless of req$ speed on certain race conditions; harder to reason about. - CSS `animation-delay` — cannot easily tie delay to actual response time. --- ## Decision 4: Broken-image fallback **Decision**: Inline `(error)` event binding on `` elements, guarded against recursive fallback. **Rationale**: For a small number of distinct image elements (card thumbnail, detail full-image), an event binding is the minimal idiomatic pattern and avoids the complexity of a directive. Recursive fallback is prevented by checking that the current `src` is not already the placeholder before reassigning. **Pattern**: ```html ``` ```typescript onImgError(e: Event): void { const img = e.target as HTMLImageElement; if (!img.src.endsWith('placeholder')) { img.src = 'data:image/svg+xml,...'; // inline SVG placeholder } } ``` **Alternatives considered**: - Custom `ImageFallbackDirective` — reusable but over-engineered for two call sites; can be extracted later if the pattern spreads. --- ## Decision 5: Global design token set **Decision**: Seven semantic tokens defined on `:root` in `styles.css`, derived from colours already present in the components. | Token | Value | Meaning | |--------------------|-----------|----------------------------------------------| | `--bg` | `#0f0f0f` | Page background (already in body) | | `--surface` | `#1a1a1a` | Card / input / panel background | | `--surface-raised` | `#252525` | Hover state, skeleton highlight | | `--border` | `#333` | Subtle dividers, input borders | | `--border-focus` | `#555` | Input focus ring | | `--text` | `#e0e0e0` | Primary text (already on body) | | `--text-muted` | `#777` | Secondary text, placeholders | | `--accent` | `#4a9eff` | CTAs, active chips (already in upload-btn) | | `--accent-text` | `#000` | Text on accent backgrounds | | `--danger` | `#c0392b` | Destructive actions (already in delete-btn) | | `--danger-text` | `#fff` | Text on danger backgrounds | | `--radius` | `6px` | Standard border radius | | `--radius-chip` | `12px` | Pill-shaped chips | | `--transition` | `200ms ease` | Standard animation duration | **Rationale**: All values are already present in the components but hard-coded per file. Centralising them eliminates drift without introducing a new colour palette — the visual result is identical to the current state, but consistent. **Alternatives considered**: - Introducing a new darker/lighter palette — unnecessary scope creep; the existing colours are well-chosen for a dark personal tool.