# 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 `<img>` 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
<img [src]="url" (error)="onImgError($event)" />
```
```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.
