# 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.