Introduces a shared CSS custom property token layer and applies it across all five views (library, upload, detail, login, app shell). Each view now has intentional loading, empty, and error states. - styles.css: 13 design tokens on :root; shimmer skeleton animation - Library: 150ms-debounced skeleton loading, empty state with /upload link, error card with retry, card hover lift, broken-image fallback - Upload: token-styled drop-zone, Uploading… spinner, 4s success banner, distinct validation vs. network error messages - Detail: image skeleton, network error card (separate from 404 not-found card), Owner actions panel, danger tag error styling, broken-image fallback - Login: vertically centred surface card, danger field/server errors, Signing in… disabled button - App shell: 48px fixed header, app name left, sign-out right, no reflow on auth state change - All 24 ESLint errors resolved (including pre-existing auth spec issues); ng build and ng lint pass clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
5.5 KiB
Markdown
138 lines
5.5 KiB
Markdown
# 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.
|