Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4126275c4d | |||
| ffc28f78f5 | |||
| 80241aa352 | |||
| 37886f3aa7 | |||
| 410a7cd37e | |||
| b5fec3a1ba | |||
| 8eee815e9a | |||
| 5b7ec03973 | |||
| 15bb961ccc | |||
| 4e7f76ecb1 |
+159
-20
@@ -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 */
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user