Compare commits

..

10 Commits

Author SHA1 Message Date
Ilia Mashkov 4126275c4d refactor(SliderArea): extract grid overlay into bg-grid utilities
Workflow / build (pull_request) Successful in 37s
Workflow / publish (pull_request) Has been skipped
The decorative dotted-grid background on the paper surface was a
6-line $derived gridStyle string applied via inline style="" plus four
extra utility classes for color and opacity. Replace with two named
utilities and let CSS handle the responsive switch.

app.css:
- New --color-grid-line CSS var (light + dark) so the grid colour and
  intensity auto-switch without consumers needing a dark: variant or an
  opacity layer.
- @utility bg-grid (20px cells) and @utility bg-grid-sm (10px cells).
  Both reference --color-grid-line, so the same markup paints correctly
  in light and dark mode.

SliderArea.svelte:
- Drop the gridStyle $derived block and the inline style= attribute.
- Overlay becomes a single line:
  <div class="absolute inset-0 pointer-events-none bg-grid-sm md:bg-grid"
       aria-hidden="true" />
  Mobile picks the tight 10px grid; the md: breakpoint flips to 20px,
  matching the prior JS-driven behaviour with no extra runtime cost.
2026-05-25 11:09:26 +03:00
Ilia Mashkov ffc28f78f5 refactor(SliderArea): bump the padding to avoid overlap with TypographyMenu 2026-05-25 11:08:04 +03:00
Ilia Mashkov 80241aa352 refactor(SliderArea): remove $derived className 2026-05-25 11:07:20 +03:00
Ilia Mashkov 37886f3aa7 refactor(TypographyMenu): use separators in one style 2026-05-25 11:01:11 +03:00
Ilia Mashkov 410a7cd37e feat(SliderArea): keyboard accessibility for the comparison slider
The slider element had role="slider" and tabindex="0" but no keyboard
handler — the focus ring appeared but the slider could not be moved.

Add a keydown handler implementing the standard ARIA slider contract:
- ArrowLeft / ArrowDown — step left by 1 percent
- ArrowRight / ArrowUp — step right by 1 percent
- Shift + arrow — coarse step (10 percent)
- PageUp / PageDown — coarse step (10 percent)
- Home — jump to 0
- End — jump to 100

Bounds and step sizes extracted as named constants (SLIDER_MIN,
SLIDER_MAX, SLIDER_STEP_FINE, SLIDER_STEP_COARSE). Position updates go
through sliderSpring.target so keyboard moves animate the same way as
pointer drags.

Also adds the missing ARIA attributes that screen readers need:
- aria-valuemin / aria-valuemax (bounds)
- aria-orientation (horizontal)
2026-05-25 10:57:54 +03:00
Ilia Mashkov b5fec3a1ba fix(SliderArea): inset paper with padding instead of scale for even gaps
scale-[0.94] shrinks proportionally — on wide viewports this produced
visibly larger horizontal gaps than vertical ones when the sidebar
opens, and it left the text engine measuring the un-scaled width
(causing the thumb-to-character morph boundary to drift).

Switch to outer-container padding (p-6 when sidebar is open on desktop)
so the paper inherits an equal pixel inset on all four sides. The
ResizeObserver picks up the new dimensions and the layout engine
re-wraps text at the actual rendered width.
2026-05-25 10:57:23 +03:00
Ilia Mashkov 8eee815e9a refactor(styles): improve light-mode contrast across surfaces and muted text
Dark mode unchanged. Targets that were reported as "barely visible" in
light theme:

Surfaces / dividers
- --color-border-subtle (light) bumped from rgb(0 0 0 / 0.05) to
  --neutral-300 (matches the Input underline variant's border color and
  yields a visible hairline on bg-surface / bg-paper).
- New bg-subtle utility (same color as border-subtle but as
  background-color) — used by Divider component and the TypographyMenu
  inline column separator. Replaces ad-hoc 'bg-black/5 dark:bg-white/10'
  and 'bg-black/10 dark:bg-white/10' bands.
- FontSearch + ComparisonView Search wrapper borders switched from
  hand-written 'border-swiss-black/5 dark:border-white/10' to
  border-subtle so they participate in the palette.

Muted text
- Button tertiary inactive text (light) bumped neutral-400 → neutral-600
  (~2.7:1 → ~7.5:1 contrast). Covers the A/B toggle and the font-list
  rows in the sidebar.
- Label/TechText muted variant (light) bumped neutral-400 → neutral-600.
  Covers the ComboControl value text.
- Link text aligned to neutral-500 / neutral-400 (subtle but visible).

No behavior changes; pure styling.
2026-05-25 10:56:51 +03:00
Ilia Mashkov 5b7ec03973 refactor: sweep call sites onto design-system utilities + bug fixes
Replace inline class clusters with the design-system utilities and
tokens established in the prior two commits. No behavior changes
intended beyond two real bug fixes.

Bug fixes:
- SampleList.svelte: 'border-border-subtle bg-background-40' was a
  silent no-op (both classes mis-spelled). Now 'border-subtle
  bg-background/40' applies as intended.
- FontList.svelte: 'h-[44px]' → 'h-11' (44px = 2.75rem = spacing-11,
  no need for arbitrary value).

Sweeps:
- TypographyMenu: popover + floating bar now use surface-popover /
  surface-floating + shadow-popover.
- FontList + FilterGroup: tertiary list buttons use the new
  Button layout="block-list-row" variant; skeleton fills use
  the skeleton-fill utility.
- Footer / BreadcrumbHeader: surface-floating absorbs the
  bg-surface/blur/border cluster. Footer bumped to z-20 with a
  comment explaining the stacking against SidebarContainer (z-40/50).
- FontSampler: surface-card + hover shadow-stamp-card token.
- SliderArea: surface-canvas, flex-center, shadow-floating-panel
  tokens (light + dark variants).
- Sidebar / Header / ButtonGroup / Layout / SidebarContainer:
  bg-surface dark:bg-dark-bg → surface-canvas (8 sites);
  SidebarContainer mobile panel uses shadow-overlay.
- Loader / Thumb: flex items-center justify-center → flex-center;
  Thumb durations → duration-fast.
- ComboControl: trigger uses surface-card-elevated when open,
  popover uses surface-card-elevated, label cluster → text-label-mono,
  flex-center for the trigger interior.
- Slider: shadow-sm → shadow-rest, duration-150 → duration-fast.
- text-secondary → text-subtle across Input, Slider, ComboControl
  (matches the rename in the styles commit).
- Link: reverted earlier surface-floating attempt — Link's original
  bg-surface/80 backdrop-blur pattern was thinner than surface-floating
  (no border, smaller blur), and the Footer was overlaying its own
  border-subtle on top, fighting the utility. Kept the original style.
2026-05-25 10:20:40 +03:00
Ilia Mashkov 15bb961ccc refactor(Button): add block-list-row layout variant + adopt design-system tokens
- New layout prop with values 'inline' (default) and 'block-list-row'.
  The block-list-row variant bakes in full-width, left-aligned content
  with trailing icon and text-sm, replacing the ~10-class override
  duplicated across FilterGroup, FontList, and similar list-row sites.
- primary variant's three hard-offset shadows now reference the
  shadow-stamp-{rest,hover,pressed} tokens; the 0.0625rem translate
  becomes translate-{x,y}-px.
- Base classes use text-label-mono and duration-normal utilities
  instead of inline 'font-primary font-bold tracking-tight uppercase'
  and 'duration-200'.
- The icon variant's background uses surface-canvas (semantic naming;
  picks up dark-mode automatically via --color-surface).
- text-secondary → text-subtle (avoids collision with the @theme
  --color-secondary token; see earlier styles commit).

New exported type: ButtonLayout.
2026-05-25 10:19:56 +03:00
Ilia Mashkov 4e7f76ecb1 feat(styles): add shadow + motion tokens, surface utilities, mode-switching color vars
Establish a real design system foundation by moving the project from
inline arbitrary-value classes to named tokens and reusable utilities.

Tokens added to @theme (auto-generate Tailwind utilities):
- Shadows: shadow-rest, shadow-stamp-{rest,hover,pressed,card},
  shadow-popover, shadow-floating-panel{,-dark}, shadow-overlay
- Motion: duration-{fast,normal,slow,slower};
  ease-{standard,out-soft,spring-overshoot}

Semantic mode-switching colors added to :root / .dark so utilities
auto-adapt without dark: variants:
- --color-border-subtle, --color-text-subtle, --color-skeleton

Utilities migrated to Tailwind v4's @utility directive with direct CSS
properties (previously @layer utilities with @apply chains, which
silently failed when chaining to other user-defined utilities):
- border-subtle, text-subtle, focus-ring
- surface-canvas, surface-card, surface-card-elevated, surface-popover,
  surface-floating
- flex-center, skeleton-fill, text-label-mono

Notes:
- text-secondary was renamed to text-subtle because --color-secondary is
  registered in @theme (a near-white shadcn surface token), which made
  Tailwind v4 auto-generate a colliding text-secondary utility that won
  over the user-defined one — every consumer effectively rendered as
  near-white text. The text-subtle name pairs cleanly with border-subtle
  and avoids any @theme collisions.
- Dead --space-* variable scale removed (was defined but never wired
  into @theme; Tailwind's default spacing scale is used everywhere).
2026-05-25 10:19:45 +03:00
26 changed files with 327 additions and 121 deletions
+159 -20
View File
@@ -14,6 +14,13 @@
--swiss-black: #1a1a1a; --swiss-black: #1a1a1a;
--swiss-white: #ffffff; --swiss-white: #ffffff;
/* Semantic mode-switching colors. These are redefined inside `.dark`
so utilities that reference them auto-adapt without a `dark:` variant. */
--color-border-subtle: var(--neutral-300);
--color-text-subtle: var(--neutral-500);
--color-skeleton: var(--neutral-200);
--color-grid-line: rgb(0 0 0 / 0.03);
/* Neutral Grays */ /* Neutral Grays */
--neutral-50: #fafafa; --neutral-50: #fafafa;
--neutral-100: #f5f5f5; --neutral-100: #f5f5f5;
@@ -80,16 +87,6 @@
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
/* Spacing Scale (rem-based) */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 0.75rem;
--space-lg: 1rem;
--space-xl: 1.5rem;
--space-2xl: 2rem;
--space-3xl: 3rem;
--space-4xl: 4rem;
/* Typography Scale */ /* Typography Scale */
--text-xs: 0.75rem; --text-xs: 0.75rem;
--text-sm: 0.875rem; --text-sm: 0.875rem;
@@ -114,6 +111,12 @@
--color-surface: var(--dark-bg); --color-surface: var(--dark-bg);
--color-paper: var(--dark-card); --color-paper: var(--dark-card);
/* Dark-mode overrides for the semantic mode-switching colors. */
--color-border-subtle: rgb(255 255 255 / 0.1);
--color-text-subtle: var(--neutral-400);
--color-skeleton: var(--neutral-800);
--color-grid-line: rgb(255 255 255 / 0.05);
--background: oklch(0.145 0 0); --background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0); --card: oklch(0.145 0 0);
@@ -212,6 +215,51 @@
--text-2xs: 0.625rem; --text-2xs: 0.625rem;
/* Monospace label tracking — used in Loader and Footnote */ /* Monospace label tracking — used in Loader and Footnote */
--tracking-wider-mono: 0.2em; --tracking-wider-mono: 0.2em;
/* ============================================
SHADOW TOKENS
============================================ */
/* Default resting shadow — equivalent to Tailwind's shadow-sm. Used on
buttons, sliders, popover triggers in non-floating state. */
--shadow-rest: 0 1px 2px 0 rgb(0 0 0 / 0.05);
/* Swiss "hard offset" stamp — rests at 2px/2px, lifts to 3px/3px on
hover, presses back to 1px/1px on active. Primary button motif. */
--shadow-stamp-rest: 0.125rem 0.125rem 0 0 rgb(0 0 0 / 0.1);
--shadow-stamp-hover: 0.1875rem 0.1875rem 0 0 rgb(0 0 0 / 0.15);
--shadow-stamp-pressed: 0.0625rem 0.0625rem 0 0 rgb(0 0 0 / 0.1);
/* Card-tier hard-offset stamp — wider, brand-tinted. Used on
interactive cards (FontSampler hover). */
--shadow-stamp-card: 5px 5px 0 0 var(--color-brand);
/* Floating popovers (typography menu, combo control list). */
--shadow-popover: 0 20px 40px -10px rgb(0 0 0 / 0.15);
/* Drop-shadow under semi-translucent floating panels like the
comparison slider's character row. */
--shadow-floating-panel: 0 25px 50px -12px rgb(0 0 0 / 0.05);
--shadow-floating-panel-dark: 0 25px 50px -12px rgb(0 0 0 / 0.2);
/* Drawer / overlay shadow — full-strength shadow-2xl. */
--shadow-overlay: 0 25px 50px -12px rgb(0 0 0 / 0.25);
/* ============================================
MOTION TOKENS
============================================ */
--duration-fast: 150ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--duration-slower: 500ms;
/* Tailwind's default ease-in-out — symmetric, good for layout shifts. */
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
/* Decelerating curve — matches Tailwind's ease-out. Dominant in this codebase. */
--ease-out-soft: cubic-bezier(0, 0, 0.2, 1);
/* Spring overshoot — used in character pop animation. */
--ease-spring-overshoot: cubic-bezier(0.34, 1.56, 0.64, 1);
} }
@layer base { @layer base {
@@ -277,19 +325,110 @@
} }
} }
@layer utilities { /* ============================================
/* 21× border-black/5 dark:border-white/10 → single token */ DESIGN-SYSTEM UTILITIES
.border-subtle { ============================================
@apply border-black/5 dark:border-white/10; Defined via `@utility` (Tailwind v4) so they integrate with the variant
system (`hover:`, `dark:`, breakpoints) and don't rely on `@apply`
chains. Colors reference the mode-switching semantic vars defined in
`:root`/`.dark` above, so most utilities need no `dark:` variant in
their definition or at call sites. */
@utility border-subtle {
border-color: var(--color-border-subtle);
} }
/* Secondary text pair */
.text-secondary { /* Same color as border-subtle, applied via background-color — for 1px
@apply text-neutral-500 dark:text-neutral-400; dividers, inline separator strips, and other hairlines that aren't
element borders. */
@utility bg-subtle {
background-color: var(--color-border-subtle);
} }
/* Standard focus ring */
.focus-ring { /* Muted text color — paired with `border-subtle` naming. The previous
@apply focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2; name `text-secondary` collided with Tailwind v4 auto-generating a
utility from `--color-secondary` (the shadcn near-white surface token
registered in `@theme`), which made every consumer effectively
invisible (near-white text on light backgrounds). */
@utility text-subtle {
color: var(--color-text-subtle);
} }
@utility focus-ring {
&:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px var(--color-background, white), 0 0 0 4px var(--color-brand);
}
}
/* ── Surface utilities ────────────────────────────────────────── */
@utility surface-canvas {
background-color: var(--color-surface);
}
@utility surface-card {
background-color: var(--color-paper);
border: 1px solid var(--color-border-subtle);
}
@utility surface-card-elevated {
background-color: var(--color-paper);
border: 1px solid var(--color-border-subtle);
box-shadow: var(--shadow-rest);
}
@utility surface-popover {
background-color: var(--color-paper);
border: 1px solid var(--color-border-subtle);
box-shadow: var(--shadow-popover);
}
@utility surface-floating {
background-color: color-mix(in srgb, var(--color-surface) 80%, transparent);
backdrop-filter: blur(12px);
border: 1px solid var(--color-border-subtle);
}
/* ── Shape / layout ───────────────────────────────────────────── */
@utility flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@utility skeleton-fill {
background-color: color-mix(in srgb, var(--color-skeleton) 70%, transparent);
}
/* Subtle dotted-grid overlay used as a decorative background on the
comparison paper surface. Color and intensity auto-switch via
--color-grid-line. `bg-grid-sm` uses a tighter cell — typical mobile
choice; `bg-grid` is the default desktop cell. Pair with absolute /
pointer-events-none on the overlay element. */
@utility bg-grid {
background-image:
linear-gradient(var(--color-grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--color-grid-line) 1px, transparent 1px);
background-size: 20px 20px;
}
@utility bg-grid-sm {
background-image:
linear-gradient(var(--color-grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--color-grid-line) 1px, transparent 1px);
background-size: 10px 10px;
}
/* ── Typography ───────────────────────────────────────────────── */
@utility text-label-mono {
font-family: var(--font-primary);
font-weight: 700;
letter-spacing: -0.025em;
text-transform: uppercase;
} }
/* Global utility - useful across your app */ /* Global utility - useful across your app */
+1 -1
View File
@@ -74,7 +74,7 @@ onDestroy(() => themeManager.destroy());
<div <div
id="app-root" id="app-root"
class={cn( class={cn(
'min-h-dvh w-auto flex flex-col bg-surface dark:bg-dark-bg relative', 'min-h-dvh w-auto flex flex-col surface-canvas relative',
theme === 'dark' ? 'dark' : '', theme === 'dark' ? 'dark' : '',
)} )}
> >
@@ -43,8 +43,8 @@ function createButtonText(item: BreadcrumbItem) {
md:h-16 px-4 md:px-6 lg:px-8 md:h-16 px-4 md:px-6 lg:px-8
flex items-center justify-between flex items-center justify-between
z-40 z-40
bg-surface/90 dark:bg-dark-bg/90 backdrop-blur-md surface-floating bg-surface/90 dark:bg-dark-bg/90
border-b border-subtle border-x-0 border-t-0
" "
> >
<div class="max-w-8xl px-4 sm:px-6 h-full w-full flex items-center justify-between gap-2 sm:gap-4"> <div class="max-w-8xl px-4 sm:px-6 h-full w-full flex items-center justify-between gap-2 sm:gap-4">
@@ -90,11 +90,8 @@ $effect(() => {
align="end" align="end"
sideOffset={8} sideOffset={8}
class={cn( class={cn(
'z-50 w-72', 'z-50 w-72 p-4 rounded-none',
'bg-surface dark:bg-dark-card', 'surface-popover',
'border border-subtle',
'shadow-[0_20px_40px_-10px_rgba(0,0,0,0.15)]',
'rounded-none p-4',
'data-[state=open]:animate-in data-[state=closed]:animate-out', 'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
@@ -118,7 +115,7 @@ $effect(() => {
{#snippet child({ props })} {#snippet child({ props })}
<button <button
{...props} {...props}
class="inline-flex items-center justify-center size-6 rounded-none hover:bg-black/5 dark:hover:bg-white/5 transition-colors" class="flex-center size-6 rounded-none hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
aria-label="Close controls" aria-label="Close controls"
> >
<XIcon class="size-3.5 text-neutral-500" /> <XIcon class="size-3.5 text-neutral-500" />
@@ -150,14 +147,13 @@ $effect(() => {
<div <div
class={cn( class={cn(
'flex items-center gap-1 md:gap-2 p-1.5 md:p-2', 'flex items-center gap-1 md:gap-2 p-1.5 md:p-2',
'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl', 'surface-floating bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl',
'border border-subtle', 'shadow-popover rounded-none',
'shadow-[0_20px_40px_-10px_rgba(0,0,0,0.1)]', 'ring-1 ring-black/5 dark:ring-white/5',
'rounded-none ring-1 ring-black/5 dark:ring-white/5',
)} )}
> >
<!-- Header: icon + label --> <!-- Header: icon + label -->
<div class="px-2 md:px-3 flex items-center gap-1.5 md:gap-2 border-r border-subtle mr-1 text-swiss-black dark:text-neutral-200 shrink-0"> <div class="px-2 md:px-3 flex items-center gap-1.5 md:gap-2 mr-1 text-swiss-black dark:text-neutral-200 shrink-0">
<Settings2Icon <Settings2Icon
size={14} size={14}
class="text-swiss-red" class="text-swiss-red"
@@ -171,9 +167,7 @@ $effect(() => {
<!-- Controls with dividers between each --> <!-- Controls with dividers between each -->
{#each typographySettingsStore.controls as control, i (control.id)} {#each typographySettingsStore.controls as control, i (control.id)}
{#if i > 0} <div class="w-px h-4 md:h-6 bg-subtle mx-0.5 md:mx-1 shrink-0"></div>
<div class="w-px h-6 md:h-8 bg-black/5 dark:bg-white/10 mx-0.5 md:mx-1 shrink-0"></div>
{/if}
<ComboControl <ComboControl
control={control.instance} control={control.instance}
@@ -58,12 +58,10 @@ const stats = $derived([
class=" class="
group relative group relative
w-full h-full w-full h-full
bg-paper dark:bg-dark-card surface-card
border border-subtle
hover:border-brand dark:hover:border-brand hover:border-brand dark:hover:border-brand
hover:shadow-brand/10 hover:shadow-stamp-card
hover:shadow-[5px_5px_0px_0px] transition-all duration-normal
transition-all duration-200
overflow-hidden overflow-hidden
flex flex-col flex flex-col
min-h-60 min-h-60
+32 -11
View File
@@ -7,6 +7,7 @@ import { cn } from '$shared/lib';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements'; import type { HTMLButtonAttributes } from 'svelte/elements';
import type { import type {
ButtonLayout,
ButtonSize, ButtonSize,
ButtonVariant, ButtonVariant,
IconPosition, IconPosition,
@@ -23,6 +24,14 @@ interface Props extends HTMLButtonAttributes {
* @default 'md' * @default 'md'
*/ */
size?: ButtonSize; size?: ButtonSize;
/**
* Layout shape
* - `inline`: default — content-sized, centered.
* - `block-list-row`: full-width row with the content left-aligned and any
* trailing icon pushed to the right (used for filter-group rows, etc).
* @default 'inline'
*/
layout?: ButtonLayout;
/** /**
* Icon snippet * Icon snippet
*/ */
@@ -56,6 +65,7 @@ interface Props extends HTMLButtonAttributes {
let { let {
variant = 'secondary', variant = 'secondary',
size = 'md', size = 'md',
layout = 'inline',
icon, icon,
iconPosition = 'left', iconPosition = 'left',
active = false, active = false,
@@ -76,10 +86,10 @@ const variantStyles: Record<ButtonVariant, string> = {
'hover:bg-swiss-red/90', 'hover:bg-swiss-red/90',
'active:bg-swiss-red/80', 'active:bg-swiss-red/80',
'border border-swiss-red', 'border border-swiss-red',
'shadow-[0.125rem_0.125rem_0_0_rgba(0,0,0,0.1)]', 'shadow-stamp-rest',
'hover:shadow-[0.1875rem_0.1875rem_0_0_rgba(0,0,0,0.15)]', 'hover:shadow-stamp-hover',
'active:shadow-[0.0625rem_0.0625rem_0_0_rgba(0,0,0,0.08)]', 'active:shadow-stamp-pressed',
'active:translate-x-[0.0625rem] active:translate-y-[0.0625rem]', 'active:translate-x-px active:translate-y-px',
'disabled:bg-neutral-300 dark:disabled:bg-neutral-700', 'disabled:bg-neutral-300 dark:disabled:bg-neutral-700',
'disabled:text-neutral-500 dark:disabled:text-neutral-500', 'disabled:text-neutral-500 dark:disabled:text-neutral-500',
'disabled:border-neutral-300 dark:disabled:border-neutral-700', 'disabled:border-neutral-300 dark:disabled:border-neutral-700',
@@ -111,7 +121,7 @@ const variantStyles: Record<ButtonVariant, string> = {
), ),
ghost: cn( ghost: cn(
'bg-transparent', 'bg-transparent',
'text-secondary', 'text-subtle',
'border border-transparent', 'border border-transparent',
'hover:bg-transparent dark:hover:bg-transparent', 'hover:bg-transparent dark:hover:bg-transparent',
'hover:text-brand dark:hover:text-brand', 'hover:text-brand dark:hover:text-brand',
@@ -120,8 +130,8 @@ const variantStyles: Record<ButtonVariant, string> = {
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
), ),
icon: cn( icon: cn(
'bg-surface dark:bg-dark-bg', 'surface-canvas',
'text-secondary', 'text-subtle',
'border border-transparent', 'border border-transparent',
'hover:bg-paper dark:hover:bg-paper', 'hover:bg-paper dark:hover:bg-paper',
'hover:text-brand', 'hover:text-brand',
@@ -133,9 +143,11 @@ const variantStyles: Record<ButtonVariant, string> = {
tertiary: cn( tertiary: cn(
// Font override — must come after base in cn() to win via tailwind-merge // Font override — must come after base in cn() to win via tailwind-merge
'font-secondary font-medium normal-case tracking-normal', 'font-secondary font-medium normal-case tracking-normal',
// Inactive state // Inactive state — bumped in light mode for readable contrast against
// bg-surface (~7.5:1 vs. the prior ~2.7:1 with neutral-400). Dark
// unchanged because the existing tone reads well on dark-bg.
'bg-transparent', 'bg-transparent',
'text-neutral-400 dark:text-neutral-400', 'text-neutral-600 dark:text-neutral-400',
'border border-transparent', 'border border-transparent',
// Hover (inactive) — semi-transparent lift, no bg-paper token // Hover (inactive) — semi-transparent lift, no bg-paper token
'hover:bg-paper/50 dark:hover:bg-dark-card/50', 'hover:bg-paper/50 dark:hover:bg-dark-card/50',
@@ -174,12 +186,19 @@ const activeStyles: Partial<Record<ButtonVariant, string>> = {
icon: 'bg-paper dark:bg-paper text-brand border-subtle', icon: 'bg-paper dark:bg-paper text-brand border-subtle',
}; };
const layoutStyles: Record<ButtonLayout, string> = {
inline: '',
/* List-row buttons act as content labels rather than action buttons,
so they bump to `text-sm` regardless of the size prop's default. */
'block-list-row': 'w-full justify-between text-left text-sm',
};
const classes = $derived(cn( const classes = $derived(cn(
// Base // Base
'inline-flex items-center justify-center', 'inline-flex items-center justify-center',
'font-primary font-bold tracking-tight uppercase', 'text-label-mono',
'rounded-none', 'rounded-none',
'transition-all duration-200', 'transition-all duration-normal',
'select-none', 'select-none',
'outline-none', 'outline-none',
'cursor-pointer', 'cursor-pointer',
@@ -190,6 +209,8 @@ const classes = $derived(cn(
variantStyles[variant], variantStyles[variant],
// Size (square when icon-only) // Size (square when icon-only)
isIconOnly ? iconSizeStyles[size] : sizeStyles[size], isIconOnly ? iconSizeStyles[size] : sizeStyles[size],
// Layout
layoutStyles[layout],
// Animate (CSS tap scale — excluded for primary which uses translate instead) // Animate (CSS tap scale — excluded for primary which uses translate instead)
animate && !disabled && variant !== 'primary' && 'active:scale-[0.97]', animate && !disabled && variant !== 'primary' && 'active:scale-[0.97]',
// Active override // Active override
+1 -1
View File
@@ -25,7 +25,7 @@ let { children, class: className, ...rest }: Props = $props();
<div <div
class={cn( class={cn(
'flex items-center gap-1 p-1', 'flex items-center gap-1 p-1',
'bg-surface dark:bg-dark-bg', 'surface-canvas',
'border border-subtle', 'border border-subtle',
'rounded-none', 'rounded-none',
'transition-colors duration-500', 'transition-colors duration-500',
+1
View File
@@ -1,3 +1,4 @@
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'outline' | 'icon'; export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'outline' | 'icon';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export type ButtonLayout = 'inline' | 'block-list-row';
export type IconPosition = 'left' | 'right'; export type IconPosition = 'left' | 'right';
@@ -91,7 +91,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
step={control.step} step={control.step}
orientation="horizontal" orientation="horizontal"
/> />
<span class="font-mono text-xs text-secondary tabular-nums w-10 text-right shrink-0"> <span class="font-mono text-xs text-subtle tabular-nums w-10 text-right shrink-0">
{formattedValue()} {formattedValue()}
</span> </span>
</div> </div>
@@ -120,12 +120,12 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
<button <button
{...props} {...props}
class={cn( class={cn(
'flex flex-col items-center justify-center w-14 py-1', 'flex flex-col flex-center w-14 py-1',
'select-none rounded-none transition-all duration-150', 'select-none rounded-none transition-all duration-fast',
'border border-transparent', 'border border-transparent',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30',
open open
? 'bg-paper dark:bg-dark-card shadow-sm border-subtle' ? 'surface-card-elevated'
: 'hover:bg-paper/50 dark:hover:bg-dark-card/50', : 'hover:bg-paper/50 dark:hover:bg-dark-card/50',
)} )}
aria-label={controlLabel ? `${controlLabel}: ${formattedValue()}` : undefined} aria-label={controlLabel ? `${controlLabel}: ${formattedValue()}` : undefined}
@@ -134,7 +134,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
{#if displayLabel} {#if displayLabel}
<span <span
class=" class="
text-3xs font-primary font-bold tracking-tight uppercase text-3xs text-label-mono
text-neutral-900 dark:text-neutral-100 text-neutral-900 dark:text-neutral-100
mb-0.5 leading-none mb-0.5 leading-none
" "
@@ -153,7 +153,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
<!-- Vertical slider popover --> <!-- Vertical slider popover -->
<Popover.Content <Popover.Content
class="w-auto py-4 px-3 h-64 flex items-center justify-center rounded-none border border-subtle shadow-sm bg-paper dark:bg-dark-card" class="w-auto py-4 px-3 h-64 flex-center rounded-none surface-card-elevated"
align="center" align="center"
side="top" side="top"
> >
+1 -1
View File
@@ -25,7 +25,7 @@ let {
<div <div
class={cn( class={cn(
'bg-black/10 dark:bg-white/10', 'bg-subtle',
orientation === 'horizontal' ? 'w-full h-px' : 'w-px h-full', orientation === 'horizontal' ? 'w-full h-px' : 'w-px h-full',
className, className,
)} )}
+2 -2
View File
@@ -83,9 +83,9 @@ $effect(() => {
<div transition:fly={{ y: 20, duration: 200, easing: cubicOut }}> <div transition:fly={{ y: 20, duration: 200, easing: cubicOut }}>
<Button <Button
variant="tertiary" variant="tertiary"
layout="block-list-row"
active={property.selected} active={property.selected}
onclick={() => (property.selected = !property.selected)} onclick={() => (property.selected = !property.selected)}
class="w-full px-3 md:px-4 py-2.5 md:py-3 justify-between text-left text-sm flex"
iconPosition="right" iconPosition="right"
icon={property.selected ? icon : undefined} icon={property.selected ? icon : undefined}
> >
@@ -96,8 +96,8 @@ $effect(() => {
{#if hasMore} {#if hasMore}
<Button <Button
variant="icon" variant="icon"
layout="block-list-row"
onclick={() => (showMore = !showMore)} onclick={() => (showMore = !showMore)}
class="w-full px-3 md:px-4 py-2.5 md:py-3 justify-between text-left text-sm flex"
iconPosition="left" iconPosition="left"
> >
{#snippet icon()} {#snippet icon()}
+1 -1
View File
@@ -149,7 +149,7 @@ const inputClasses = $derived(cn(
<span <span
class={cn( class={cn(
'text-2xs font-mono tracking-wide px-1', 'text-2xs font-mono tracking-wide px-1',
error ? 'text-brand ' : 'text-secondary', error ? 'text-brand ' : 'text-subtle',
)} )}
> >
{helperText} {helperText}
+3 -1
View File
@@ -25,7 +25,9 @@ export const labelSizeConfig: Record<LabelSize, string> = {
export const labelVariantConfig: Record<LabelVariant, string> = { export const labelVariantConfig: Record<LabelVariant, string> = {
default: 'text-neutral-900 dark:text-neutral-100', default: 'text-neutral-900 dark:text-neutral-100',
accent: 'text-brand', accent: 'text-brand',
muted: 'text-neutral-400 dark:text-neutral-500', /* Light mode bumped from neutral-400 (~2.7:1 contrast, barely visible)
to neutral-600 (~7.5:1). Dark mode unchanged. */
muted: 'text-neutral-600 dark:text-neutral-500',
success: 'text-green-600 dark:text-green-400', success: 'text-green-600 dark:text-green-400',
warning: 'text-yellow-600 dark:text-yellow-400', warning: 'text-yellow-600 dark:text-yellow-400',
error: 'text-brand', error: 'text-brand',
+1 -1
View File
@@ -34,7 +34,7 @@ let {
<a <a
class={cn( class={cn(
'group inline-flex items-center gap-1 text-2xs font-mono uppercase tracking-wider-mono', 'group inline-flex items-center gap-1 text-2xs font-mono uppercase tracking-wider-mono',
'text-neutral-400 hover:text-brand transition-colors', 'text-neutral-500 dark:text-neutral-400 hover:text-brand transition-colors',
'bg-surface/80 dark:bg-dark-bg/80 backdrop-blur-sm px-2 py-1 pointer-events-auto', 'bg-surface/80 dark:bg-dark-bg/80 backdrop-blur-sm px-2 py-1 pointer-events-auto',
className, className,
)} )}
+1 -1
View File
@@ -26,7 +26,7 @@ let { size = 20, class: className = '', message = 'analyzing_data' }: Props = $p
</script> </script>
<div <div
class="absolute inset-x-0 inset-y-0 flex items-center justify-center gap-4 {className}" class="absolute inset-x-0 inset-y-0 flex-center gap-4 {className}"
in:fade={{ duration: 300 }} in:fade={{ duration: 300 }}
out:fade={{ duration: 300 }} out:fade={{ duration: 300 }}
> >
@@ -59,7 +59,7 @@ function close() {
<!-- Panel --> <!-- Panel -->
<div <div
class="fixed left-0 top-0 bottom-0 w-80 z-50 shadow-2xl" class="fixed left-0 top-0 bottom-0 w-80 z-50 shadow-overlay"
in:fly={{ x: -320, duration: 300, easing: cubicOut }} in:fly={{ x: -320, duration: 300, easing: cubicOut }}
out:fly={{ x: -320, duration: 250, easing: cubicOut }} out:fly={{ x: -320, duration: 250, easing: cubicOut }}
> >
@@ -83,11 +83,10 @@ function close() {
'shrink-0 z-30 h-full relative', 'shrink-0 z-30 h-full relative',
'overflow-hidden', 'overflow-hidden',
'will-change-[width]', 'will-change-[width]',
'transition-[width] duration-300 ease-out',
'border-r border-subtle', 'border-r border-subtle',
'bg-surface dark:bg-dark-bg', 'surface-canvas',
isOpen ? 'w-80 opacity-100' : 'w-0 opacity-0', isOpen ? 'w-80 opacity-100' : 'w-0 opacity-0',
'transition-[width,opacity] duration-300 ease-out', 'transition-[width,opacity] duration-slow ease-out',
className, className,
)} )}
> >
+3 -3
View File
@@ -70,17 +70,17 @@ let {
const isVertical = $derived(orientation === 'vertical'); const isVertical = $derived(orientation === 'vertical');
const labelClasses = `font-mono text-2xs tabular-nums shrink-0 const labelClasses = `font-mono text-2xs tabular-nums shrink-0
text-secondary text-subtle
group-hover:text-neutral-700 dark:group-hover:text-neutral-300 group-hover:text-neutral-700 dark:group-hover:text-neutral-300
transition-colors`; transition-colors`;
const thumbClasses = `block w-2.5 h-2.5 bg-brand const thumbClasses = `block w-2.5 h-2.5 bg-brand
rotate-45 shadow-sm rotate-45 shadow-rest
hover:scale-125 hover:scale-125
focus-visible:outline-none focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-brand/20 focus-visible:ring-2 focus-visible:ring-brand/20
data-active:scale-90 data-active:scale-90
transition-transform duration-150 transition-transform duration-fast
disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none disabled:opacity-50
cursor-grab active:cursor-grabbing`; cursor-grab active:cursor-grabbing`;
</script> </script>
@@ -110,28 +110,28 @@ function isFontReady(font: UnifiedFont): boolean {
<div class="w-full px-3 py-3 flex items-center justify-between"> <div class="w-full px-3 py-3 flex items-center justify-between">
<div class="flex-1 flex items-center gap-3"> <div class="flex-1 flex items-center gap-3">
<Skeleton <Skeleton
class="h-4 w-32 bg-neutral-200/70 dark:bg-neutral-800/70" class="h-4 w-32 skeleton-fill"
style="width: {getSkeletonWidth(index)}" style="width: {getSkeletonWidth(index)}"
/> />
</div> </div>
<Skeleton class="w-1.5 h-1.5 rounded-full bg-neutral-200/70 dark:bg-neutral-800/70" /> <Skeleton class="w-1.5 h-1.5 rounded-full skeleton-fill" />
</div> </div>
{/each} {/each}
</div> </div>
{/snippet} {/snippet}
{#snippet children({ item: font, index })} {#snippet children({ item: font, index })}
<div class="relative h-[44px] w-full"> <div class="relative h-11 w-full">
{#if !isFontReady(font)} {#if !isFontReady(font)}
<div <div
class="absolute inset-0 px-3 md:px-4 flex items-center justify-between border border-transparent" class="absolute inset-0 px-3 md:px-4 flex items-center justify-between border border-transparent"
transition:fade={{ duration: 300 }} transition:fade={{ duration: 300 }}
> >
<Skeleton <Skeleton
class="h-4 bg-neutral-200/70 dark:bg-neutral-800/70" class="h-4 skeleton-fill"
style="width: {getSkeletonWidth(index)}" style="width: {getSkeletonWidth(index)}"
/> />
<Skeleton class="w-1.5 h-1.5 rounded-full bg-neutral-200/70 dark:bg-neutral-800/70" /> <Skeleton class="w-1.5 h-1.5 rounded-full skeleton-fill" />
</div> </div>
{:else} {:else}
{@const isSelectedA = font.id === comparisonStore.fontA?.id} {@const isSelectedA = font.id === comparisonStore.fontA?.id}
@@ -141,9 +141,10 @@ function isFontReady(font: UnifiedFont): boolean {
<div transition:fade={{ duration: 300 }} class="h-full"> <div transition:fade={{ duration: 300 }} class="h-full">
<Button <Button
variant="tertiary" variant="tertiary"
layout="block-list-row"
{active} {active}
onclick={() => handleSelect(font)} onclick={() => handleSelect(font)}
class="w-full h-full px-3 md:px-4 py-2.5 md:py-3 flex !justify-between text-left text-sm" class="h-full"
iconPosition="right" iconPosition="right"
> >
<FontApplicator {font}> <FontApplicator {font}>
@@ -53,7 +53,7 @@ const fontBName = $derived(comparisonStore.fontB?.name ?? '');
'px-4 md:px-8 py-4 md:py-6', 'px-4 md:px-8 py-4 md:py-6',
'h-16 md:h-20 z-20', 'h-16 md:h-20 z-20',
'border-b border-subtle', 'border-b border-subtle',
'bg-surface dark:bg-dark-bg', 'surface-canvas',
className, className,
)} )}
> >
@@ -9,7 +9,7 @@ import { appliedFilterStore } from '$features/FilterAndSortFonts';
import { SearchBar } from '$shared/ui'; import { SearchBar } from '$shared/ui';
</script> </script>
<div class="p-6 border-b border-black/5"> <div class="p-6 border-b border-subtle">
<SearchBar <SearchBar
id="font-search" id="font-search"
class="w-full" class="w-full"
@@ -43,7 +43,7 @@ let {
class={cn( class={cn(
'flex flex-col h-full', 'flex flex-col h-full',
'w-80', 'w-80',
'bg-surface dark:bg-dark-bg', 'surface-canvas',
'border-r border-subtle', 'border-r border-subtle',
'transition-colors duration-500', 'transition-colors duration-500',
className, className,
@@ -54,7 +54,7 @@ let {
class=" class="
p-6 shrink-0 p-6 shrink-0
border-b border-subtle border-b border-subtle
bg-surface dark:bg-dark-bg surface-canvas
" "
> >
<!-- Title --> <!-- Title -->
@@ -90,7 +90,7 @@ let {
</div> </div>
<!-- ── Main: content area (no scroll - VirtualList handles scrolling) ─────────────────────────────── --> <!-- ── Main: content area (no scroll - VirtualList handles scrolling) ─────────────────────────────── -->
<div class="flex-1 min-h-0 bg-surface dark:bg-dark-bg"> <div class="flex-1 min-h-0 surface-canvas">
{#if main} {#if main}
{@render main()} {@render main()}
{/if} {/if}
@@ -101,7 +101,7 @@ let {
<div <div
class=" class="
shrink-0 p-6 shrink-0 p-6
bg-surface dark:bg-dark-bg surface-canvas
border-t border-subtle border-t border-subtle
z-10 z-10
" "
@@ -70,6 +70,19 @@ const SLIDER_PERSIST_DEBOUNCE_MS = 100;
const SLIDER_PADDING_MOBILE_PX = 48; const SLIDER_PADDING_MOBILE_PX = 48;
const SLIDER_PADDING_DESKTOP_PX = 96; const SLIDER_PADDING_DESKTOP_PX = 96;
/**
* Position bounds (percent of container width).
*/
const SLIDER_MIN = 0;
const SLIDER_MAX = 100;
/**
* Fine and coarse keyboard step sizes. Shift / Page keys use the coarse
* step; bare arrow keys use the fine step.
*/
const SLIDER_STEP_FINE = 1;
const SLIDER_STEP_COARSE = 10;
const fontA = $derived(comparisonStore.fontA); const fontA = $derived(comparisonStore.fontA);
const fontB = $derived(comparisonStore.fontB); const fontB = $derived(comparisonStore.fontB);
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady); const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
@@ -130,6 +143,46 @@ function startDragging(e: PointerEvent) {
handleMove(e); handleMove(e);
} }
/**
* Keyboard control for the comparison slider. Implements the standard
* ARIA slider keyboard contract: arrows step the position, Shift+arrow
* and PageUp/PageDown jump by the coarse step, Home/End snap to bounds.
*/
function handleKeydown(e: KeyboardEvent) {
const coarse = e.shiftKey;
const step = coarse ? SLIDER_STEP_COARSE : SLIDER_STEP_FINE;
const current = sliderSpring.target;
let next = current;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowDown':
next = current - step;
break;
case 'ArrowRight':
case 'ArrowUp':
next = current + step;
break;
case 'PageDown':
next = current - SLIDER_STEP_COARSE;
break;
case 'PageUp':
next = current + SLIDER_STEP_COARSE;
break;
case 'Home':
next = SLIDER_MIN;
break;
case 'End':
next = SLIDER_MAX;
break;
default:
return;
}
e.preventDefault();
sliderSpring.target = Math.max(SLIDER_MIN, Math.min(SLIDER_MAX, next));
}
const storeSliderPosition = debounce((value: number) => { const storeSliderPosition = debounce((value: number) => {
comparisonStore.sliderPosition = value; comparisonStore.sliderPosition = value;
}, SLIDER_PERSIST_DEBOUNCE_MS); }, SLIDER_PERSIST_DEBOUNCE_MS);
@@ -213,47 +266,37 @@ $effect(() => {
cancelled = true; cancelled = true;
}; };
}); });
// Dynamic backgroundSize based on isMobile — can't express this in Tailwind.
// Color is set to currentColor so it respects dark mode via text color.
const gridStyle = $derived(
`background-image: linear-gradient(currentColor 1px, transparent 1px), linear-gradient(90deg, currentColor 1px, transparent 1px); `
+ `background-size: ${isMobile ? '10px 10px' : '20px 20px'};`,
);
// Replaces motion.div animate={{ scale: isSidebarOpen && !isMobile ? 0.94 :1 }}
const scaleClass = $derived(
isSidebarOpen && !isMobile
? 'scale-[0.94]'
: 'scale-100',
);
</script> </script>
<!-- <!--
Outer flex container — fills parent. Outer flex container — fills parent.
The paper div inside scales down when the sidebar opens on desktop. Pads in when the sidebar opens on desktop, insetting the paper evenly.
--> -->
<div class={cn('flex-1 relative flex items-center justify-center p-0 overflow-hidden bg-surface dark:bg-dark-bg', className)}> <div
class={cn(
'flex-1 relative flex-center overflow-hidden surface-canvas',
'transition-[padding] duration-slow ease-out',
isSidebarOpen && !isMobile ? 'p-6' : 'p-0',
className,
)}
>
<!-- Paper surface --> <!-- Paper surface -->
<div <div
class={cn( class={cn(
'w-full h-full flex flex-col items-center justify-center relative', 'w-full h-full flex flex-col flex-center relative',
'bg-paper dark:bg-dark-card', 'bg-paper dark:bg-dark-card',
'shadow-2xl shadow-black/5 dark:shadow-black/20', 'shadow-floating-panel dark:shadow-floating-panel-dark',
'transition-transform duration-300 ease-out',
scaleClass,
)} )}
> >
<!-- Subtle grid overlay — pointer-events-none, very low opacity --> <!-- Subtle dotted-grid overlay — purely decorative. -->
<div <div
class="absolute inset-0 pointer-events-none opacity-[0.03] dark:opacity-[0.05] text-swiss-black dark:text-swiss-white" class="absolute inset-0 pointer-events-none bg-grid-sm md:bg-grid"
style={gridStyle}
aria-hidden="true" aria-hidden="true"
> >
</div> </div>
<!-- Slider interaction area --> <!-- Slider interaction area -->
<div class="w-full h-full flex items-center justify-center p-4 md:p-8 overflow-hidden"> <div class="w-full h-full flex items-center justify-center p-4 md:p-12 overflow-hidden">
{#if isLoading} {#if isLoading}
<div out:fade={{ duration: 300 }}> <div out:fade={{ duration: 300 }}>
<Loader size={24} /> <Loader size={24} />
@@ -264,8 +307,12 @@ const scaleClass = $derived(
role="slider" role="slider"
tabindex="0" tabindex="0"
aria-valuenow={Math.round(sliderPos)} aria-valuenow={Math.round(sliderPos)}
aria-valuemin={SLIDER_MIN}
aria-valuemax={SLIDER_MAX}
aria-orientation="horizontal"
aria-label="Font comparison slider" aria-label="Font comparison slider"
onpointerdown={startDragging} onpointerdown={startDragging}
onkeydown={handleKeydown}
class=" class="
relative w-full max-w-6xl h-full relative w-full max-w-6xl h-full
flex flex-col justify-center flex flex-col justify-center
@@ -36,9 +36,9 @@ let { sliderPos, isDragging }: Props = $props();
'-ml-2.5 md:-ml-3', '-ml-2.5 md:-ml-3',
'mt-2 md:mt-4', 'mt-2 md:mt-4',
'bg-brand text-white', 'bg-brand text-white',
'flex items-center justify-center', 'flex-center',
'rounded-none shadow-md', 'rounded-none shadow-md',
'transition-transform duration-150', 'transition-transform duration-fast',
isDragging ? 'scale-110' : 'scale-100', isDragging ? 'scale-110' : 'scale-100',
)} )}
> >
@@ -52,9 +52,9 @@ let { sliderPos, isDragging }: Props = $props();
'-ml-2.5 md:-ml-3', '-ml-2.5 md:-ml-3',
'mb-2 md:mb-4', 'mb-2 md:mb-4',
'bg-brand text-white', 'bg-brand text-white',
'flex items-center justify-center', 'flex-center',
'rounded-none shadow-md', 'rounded-none shadow-md',
'transition-transform duration-150', 'transition-transform duration-fast',
isDragging ? 'scale-110' : 'scale-100', isDragging ? 'scale-110' : 'scale-100',
)} )}
> >
@@ -51,8 +51,8 @@ function toggleFilters() {
} }
</script> </script>
<div class="flex flex-col gap-3 border-b border-t border-swiss-black/5 dark:border-white/10"> <div class="flex flex-col gap-3 border-b border-t border-subtle">
<div class="relative w-full flex flex-col md:flex-row gap-y-4 border-b border-swiss-black/5 dark:border-white/10 py-4 md:py-6"> <div class="relative w-full flex flex-col md:flex-row gap-y-4 border-b border-subtle py-4 md:py-6">
<SearchBar <SearchBar
id="font-search" id="font-search"
class="w-full" class="w-full"
+6 -2
View File
@@ -14,15 +14,19 @@ const isVertical = $derived(responsive?.isDesktop || responsive?.isDesktopLarge)
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
</script> </script>
<!--
z-20 keeps the footer below the sidebar's mobile/tablet overlay
(z-40) and panel (z-50) so an open sidebar fully covers the link.
-->
<footer <footer
class={cn( class={cn(
'fixed z-10 flex flex-row items-end gap-1 pointer-events-none', 'fixed z-20 flex flex-row items-end gap-1 pointer-events-none',
isVertical ? 'bottom-2.5 right-2.5 [writing-mode:vertical-rl] rotate-180' : 'bottom-4 left-4', isVertical ? 'bottom-2.5 right-2.5 [writing-mode:vertical-rl] rotate-180' : 'bottom-4 left-4',
)} )}
> >
<!-- Project Name (Horizontal) --> <!-- Project Name (Horizontal) -->
{#if isVertical} {#if isVertical}
<div class="flex flex-row pointer-events-auto items-center gap-2 bg-surface/80 dark:bg-dark-bg/80 backdrop-blur-sm px-2 py-1 border border-subtle"> <div class="flex flex-row pointer-events-auto items-center gap-2 surface-floating backdrop-blur-sm px-2 py-1">
<div class="w-1.5 h-1.5 bg-brand"></div> <div class="w-1.5 h-1.5 bg-brand"></div>
<span class="text-2xs font-mono uppercase tracking-wider-mono text-neutral-500 dark:text-neutral-400"> <span class="text-2xs font-mono uppercase tracking-wider-mono text-neutral-500 dark:text-neutral-400">
GlyphDiff © 2025 — {currentYear} GlyphDiff © 2025 — {currentYear}
@@ -84,7 +84,7 @@ const fontRowHeight = $derived.by(() =>
{#snippet skeleton()} {#snippet skeleton()}
<div class="flex flex-col gap-3 sm:gap-4 p-3 sm:p-4"> <div class="flex flex-col gap-3 sm:gap-4 p-3 sm:p-4">
{#each Array(5) as _, i} {#each Array(5) as _, i}
<div class="flex flex-col gap-1.5 sm:gap-2 p-3 sm:p-4 border rounded-lg sm:rounded-xl border-border-subtle bg-background-40"> <div class="flex flex-col gap-1.5 sm:gap-2 p-3 sm:p-4 border rounded-lg sm:rounded-xl border-subtle bg-background/40">
<div class="flex items-center justify-between mb-3 sm:mb-4"> <div class="flex items-center justify-between mb-3 sm:mb-4">
<Skeleton class="h-6 sm:h-8 w-1/3" /> <Skeleton class="h-6 sm:h-8 w-1/3" />
<Skeleton class="h-6 sm:h-8 w-6 sm:w-8 rounded-full" /> <Skeleton class="h-6 sm:h-8 w-6 sm:w-8 rounded-full" />