Files
Mangalord/frontend/src/lib/styles/tokens.css
MechaCat02 0d9505ce9f fix: address skeleton/nav-progress review findings
Correctness:
- MangaGridSkeleton: covers rendered 0px tall because height="0" is a
  definite height that defeats aspect-ratio. Skeleton gains an `aspectRatio`
  prop (height stays auto) so the 2/3 cover box is reserved and the grid no
  longer shifts when real covers load.
- Move `.manga-card` layout into global tokens.css (it was scoped to
  MangaCard, so the skeleton's cards inherited none of it). Both consumers
  now share one definition.
- NavProgress: rebuilt as an idle/loading/done state machine. It now
  completes to full and fades on settle (was snapping away instantly),
  restarts the trickle when one navigation supersedes another (was parking
  frozen near 90%), animates transform/opacity only with will-change gated to
  the active phase, and hides itself in reader fullscreen.
- Skeleton: text-variant height resolved in one place (no inline style
  shadowing a stylesheet rule); shimmer gradient/animation gated behind
  prefers-reduced-motion: no-preference so reduced motion shows a flat block.

A11y:
- Home loading region announces via visually-hidden text instead of an
  aria-label a live region won't reliably read.

Cleanup:
- Drop the fragile :first-child selector and index-keying ceremony in
  MangaGridSkeleton; simplify Skeleton's style construction.

Tests: adds regression guards for the 0px cover collapse, the aspect-ratio
box, text-height defaulting, and the nav-progress complete/restart phases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:02:27 +02:00

372 lines
9.4 KiB
CSS

/* Design tokens — see docs/design-system.md.
Components consume tokens only; no raw hex or raw px in scoped styles. */
:root {
--bg: #ffffff;
--surface: #f6f7f9;
--surface-elevated: #ffffff;
--border: #e3e5ea;
--border-strong: #c8ccd4;
--text: #16181d;
--text-muted: #5b6168;
--primary: #2563eb;
--primary-hover: #1d4ed8;
--primary-contrast: #ffffff;
--primary-soft-bg: rgb(37 99 235 / 0.08);
--danger: #b00020;
--danger-soft-bg: #fff5f5;
--success: #0a7d2c;
--success-soft-bg: #eaf7ee;
--warning-soft-bg: #fff5d6;
--warning-border: #d6a800;
--focus-ring: #2563eb;
--focus-ring-soft: rgb(37 99 235 / 0.25);
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
--shadow-md: 0 2px 8px rgb(0 0 0 / 0.08);
--font-stack: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--font-xs: 0.75rem;
--font-sm: 0.875rem;
/* Alias of --font-base at the body size; named on the xs/sm/md/lg/xl
scale that some components reach for. */
--font-md: 1rem;
--font-base: 1rem;
--font-lg: 1.125rem;
--font-xl: 1.5rem;
--font-2xl: 2rem;
--leading-tight: 1.3;
--leading-snug: 1.45;
--leading-normal: 1.55;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
--radius-pill: 999px;
--transition: 120ms ease-out;
--icon-sm: 14px;
--icon-md: 18px;
--icon-lg: 22px;
/* App-frame heights (fixed-position bars at the top and bottom of
the viewport). These are first-paint fallbacks — the real
values are written by ResizeObservers on the actual elements
in +layout.svelte and the reader, so they reflect rendered
size and survive font / zoom / wrap changes. */
--app-header-h: 60px;
--mobile-app-bar-h: 48px;
--app-bottom-nav-h: 56px;
--reader-nav-h: 56px;
--reader-bar-h: 56px;
/* Mobile breakpoint tokens. CSS @media queries can't read custom
properties, so these are documented anchors — keep the pixel
values in @media (max-width: 640px) etc. in lockstep. */
--bp-sm: 640px;
--bp-md: 768px;
--bp-lg: 1024px;
/* Comfortable touch-target floor for primary controls on phones.
Controls compact to their desktop height by default and grow to
this minimum under the 640px breakpoint. See touch-targets.test.ts
for the shared-primitive contract. */
--tap-min: 44px;
/* Safe-area helpers for notched / home-indicator devices. Resolve
to 0 on devices without insets, so they're safe everywhere as
long as <meta name="viewport" content="…, viewport-fit=cover">
is set (see app.html). */
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-left: env(safe-area-inset-left, 0px);
--safe-right: env(safe-area-inset-right, 0px);
--z-dropdown: 10;
--z-sticky: 50;
/* Above the fixed header/app-bar (--z-sticky) so the nav progress bar
is never occluded, but below modals and toasts. */
--z-nav-progress: 60;
--z-modal: 100;
--z-toast: 1000;
}
:root[data-theme='dark'] {
--bg: #0f1115;
--surface: #161a21;
--surface-elevated: #1c2129;
--border: #2a2f37;
--border-strong: #3a414d;
--text: #e8eaed;
--text-muted: #9aa0a6;
--primary: #60a5fa;
--primary-hover: #3b82f6;
--primary-contrast: #0f1115;
--primary-soft-bg: rgb(96 165 250 / 0.12);
--danger: #f87171;
--danger-soft-bg: #3a1620;
--success: #4ade80;
--success-soft-bg: #12301c;
--warning-soft-bg: #3a2e10;
--warning-border: #a37800;
--focus-ring: #60a5fa;
--focus-ring-soft: rgb(96 165 250 / 0.3);
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
--shadow-md: 0 2px 8px rgb(0 0 0 / 0.5);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-stack);
font-size: var(--font-base);
line-height: var(--leading-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3 {
color: var(--text);
line-height: var(--leading-tight);
font-weight: var(--weight-semibold);
margin: 0 0 var(--space-3);
}
h1 {
font-size: var(--font-2xl);
}
h2 {
font-size: var(--font-xl);
}
h3 {
font-size: var(--font-lg);
}
p {
margin: 0 0 var(--space-3);
}
a {
color: var(--primary);
text-decoration: none;
transition: color var(--transition);
}
a:hover {
text-decoration: underline;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[role='button']:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
button,
input,
select,
textarea {
font: inherit;
color: inherit;
}
input[type='text'],
input[type='search'],
input[type='password'],
input[type='number'],
input[type='email'],
select,
textarea {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border-strong);
border-radius: var(--radius-md);
padding: 0 var(--space-3);
height: 36px;
width: 100%;
transition:
border-color var(--transition),
background var(--transition),
box-shadow var(--transition);
}
textarea {
height: auto;
min-height: 6rem;
padding: var(--space-2) var(--space-3);
line-height: var(--leading-snug);
resize: vertical;
}
input[type='text']:focus,
input[type='search']:focus,
input[type='password']:focus,
input[type='number']:focus,
input[type='email']:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary);
background: var(--surface-elevated);
box-shadow: 0 0 0 3px var(--focus-ring-soft);
}
input[aria-invalid='true'],
select[aria-invalid='true'],
textarea[aria-invalid='true'] {
border-color: var(--danger);
}
input:disabled,
select:disabled,
textarea:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button {
cursor: pointer;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-md);
padding: 0 var(--space-3);
height: 36px;
font-weight: var(--weight-medium);
color: var(--text);
transition:
background var(--transition),
border-color var(--transition),
color var(--transition);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
::placeholder {
color: var(--text-muted);
opacity: 1;
}
.form-field {
display: flex;
flex-direction: column;
gap: var(--space-1);
font-size: var(--font-sm);
color: var(--text);
}
img {
max-width: 100%;
height: auto;
}
/* Horizontal-overflow safety net on phones. Manga apps never have a
legitimate need for horizontal page scroll, and an off-screen
element here often means another bug — but it's better to clip
silently than to expose touch-pan that shifts the whole layout
under the user's thumb. position:fixed descendants are unaffected,
so the bottom nav / sticky CTAs still span edge-to-edge. */
@media (max-width: 640px) {
html,
body {
overflow-x: hidden;
}
/* Touch-target floor on phones. Form controls and submit buttons keep
their compact 36px desktop height above the breakpoint but grow to a
comfortable tap size here. `min-height` (not `height`) so the
already-tall textarea is unaffected. Scoped to text fields, selects,
and `type=submit` deliberately — bare `button` would also catch dense
icon buttons (chip remove, segmented options, reader chrome) that meet
the floor through their own hit-area rules instead. */
input[type='text'],
input[type='search'],
input[type='password'],
input[type='number'],
input[type='email'],
select,
button[type='submit'] {
min-height: var(--tap-min);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0ms !important;
animation-duration: 0ms !important;
}
}
/* Shared grid for manga cover cards — used by the catalog, author and
collection pages, and the similar-mangas section. Single-sourced here
so the responsive layout can't drift between pages. Four covers per
row on phones is a deliberate user preference; `minmax(0, 1fr)` is
load-bearing so the grid never overflows the viewport. */
.manga-grid {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: var(--space-4);
}
/* A single cover cell — the catalog/author/collection cards and the
loading skeleton share this so their layout can't drift. */
.manga-card {
display: flex;
flex-direction: column;
gap: var(--space-2);
list-style: none;
/* Grid items default to min-width: auto which equals the intrinsic
content size — long author / title strings then push the cell past
`1fr`. Forcing 0 lets the column control the width and the children
ellipsize inside it. */
min-width: 0;
}
@media (max-width: 640px) {
.manga-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--space-2);
}
}