diff --git a/configurator/src/components/BrandColorRow.svelte b/configurator/src/components/BrandColorRow.svelte index e6d0df59..0fbad2af 100644 --- a/configurator/src/components/BrandColorRow.svelte +++ b/configurator/src/components/BrandColorRow.svelte @@ -10,9 +10,10 @@ * Layout: * [label] [light swatch | text input] → [dark swatch | "auto" or value] [⟲] */ - import { overrides, setOverride, clearOverride } from '../lib/store.svelte.js'; + import { overrides, setOverride, clearOverride, ui } from '../lib/store.svelte.js'; import { defaultsByName } from '../lib/model.js'; import { computeAutoDark } from '../lib/brandColors.js'; + import { measureBackground } from '../lib/probeHost.js'; import OklchPicker from './OklchPicker.svelte'; /** @type {{ colorKey: string, label: string }} */ @@ -72,6 +73,23 @@ function resetLight() { clearOverride(lightName); } function resetDark() { clearOverride(darkName); } + + // ── Inline shade strip ─────────────────────────────────────────────────── + const SHADE_SUFFIXES = ['-superlight', '-xlight', '-lighter', '', '-darker', '-xdark', '-superdark']; + let shadeColors = $state([]); + + $effect(() => { + void overrides[lightName]; + void overrides[darkName]; + void ui.previewTheme; + queueMicrotask(() => { + shadeColors = SHADE_SUFFIXES.map((s) => { + const rgb = measureBackground(`var(--sf-color-${colorKey}${s})`); + return rgb && rgb !== 'rgba(0, 0, 0, 0)' ? rgb : null; + }); + }); + }); +
@@ -140,6 +158,18 @@ auto {/if}
+ + +
+ {#each shadeColors as bg, i (i)} +
+ {/each} +
@@ -158,9 +188,10 @@ .bcr { display: grid; grid-template-columns: 80px 1fr 18px 1fr; + grid-template-rows: auto auto; gap: 8px; align-items: center; - padding: 8px 16px; + padding: 8px 16px 10px; border-bottom: 1px solid var(--cfg-border); transition: background 0.15s; } @@ -237,6 +268,25 @@ cursor: default; } + .bcr__strip { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; + border-radius: var(--cfg-radius-s, 4px); + overflow: clip; + } + + .bcr__strip-swatch { + height: 8px; + background-image: conic-gradient(#444 25%, #2a2a2a 0 50%, #444 0 75%, #2a2a2a 0); + background-size: 6px 6px; + transition: background-color 0.2s ease; + } + .bcr__strip-swatch:not(.bcr__strip-swatch--empty) { + background-image: none; + } + @media (max-width: 720px) { .bcr { grid-template-columns: 60px 1fr 14px 1fr; diff --git a/configurator/src/components/ContainerBars.svelte b/configurator/src/components/ContainerBars.svelte new file mode 100644 index 00000000..980d183a --- /dev/null +++ b/configurator/src/components/ContainerBars.svelte @@ -0,0 +1,117 @@ + + +
+ {#each rows as row (row.name)} + {@const pct = row.px != null ? Math.min(100, (row.px / maxPx) * 100) : 0} + {@const mod = modified(row.name)} +
+
+ {row.label} + {row.raw || '—'} +
+
+
+ {#if row.px != null} + ~{Math.round(row.px)}px + {/if} +
+ {#if row.help} +

{row.help}

+ {/if} +
+ {/each} +
+ + diff --git a/configurator/src/components/DomainPanel.svelte b/configurator/src/components/DomainPanel.svelte index 6fa5fe24..a45dd94b 100644 --- a/configurator/src/components/DomainPanel.svelte +++ b/configurator/src/components/DomainPanel.svelte @@ -37,10 +37,11 @@ import StylePresetRow from './StylePresetRow.svelte'; import ColorAssignments from './ColorAssignments.svelte'; import ShadeRamp from './ShadeRamp.svelte'; - import DomainPreview from './DomainPreview.svelte'; import SmartSettings from './SmartSettings.svelte'; + import HeadingEditor from './HeadingEditor.svelte'; + import RadiusEditor from './RadiusEditor.svelte'; + import ContainerBars from './ContainerBars.svelte'; import Icon from './Icon.svelte'; - import { DOMAIN_PREVIEWS } from '../lib/domainPreviews.js'; /** @type {{ domain: { id:string, label:string, icon:string, blurb:string, intro?:string, scaleIntro?:string, essentials?:string[], basicGenerators?:string[], brandColors?:boolean, docsPath?:string } }} */ let { domain } = $props(); @@ -105,11 +106,6 @@ let showColorRoles = $state(true); let showShadeRamp = $state(false); - // Preview disclosure (open by default; for generator domains it appears below generators). - let showPreview = $state(true); - - const previewSpec = $derived(DOMAIN_PREVIEWS[domain.id]); - const BRAND_PRIMARY = BRAND_COLOR_KEYS.filter((c) => ['base', 'neutral', 'primary'].includes(c.key)); const BRAND_SECONDARY = BRAND_COLOR_KEYS.filter((c) => ['secondary', 'tertiary', 'action'].includes(c.key)); const BRAND_STATUS = BRAND_COLOR_KEYS.filter((c) => c.group === 'status'); @@ -231,12 +227,10 @@ {@render catalogue()} {:else} - {#if domain.brandColors} @@ -244,34 +238,20 @@ {@render expandSummary('Semantic roles', 'How your brand colors surface')} - {#if knobs.length} {/if} - {:else if previewSpec} - -
- {@render expandSummary('Preview', previewSpec.blurb)} - -
- - - {#if hasGenerators} - {#each generators as g (g)} - - {/each} - - {#if knobs.length} - - {/if} - {:else} - - {#if knobs.length} - - {/if} + {:else if hasGenerators} + {#each generators as g (g)} + + {/each} + {#if knobs.length} + {/if} + + {:else if knobs.length} + {/if} @@ -349,6 +329,18 @@ + {:else if domain.id === 'typography'} + + + + {:else if domain.id === 'borders'} + + + + {:else if domain.id === 'layout'} + + + {:else if basicGroups.length} {#each basicGroups as group (group.title)} diff --git a/configurator/src/components/HeadingEditor.svelte b/configurator/src/components/HeadingEditor.svelte new file mode 100644 index 00000000..94004d70 --- /dev/null +++ b/configurator/src/components/HeadingEditor.svelte @@ -0,0 +1,214 @@ + + +
+
+ {#each TABS as tab (tab.id)} + {@const modified = tabHasOverride(tab.id)} + + {/each} +
+ +
+ {#if activeTab === 'all'} +
+ {#each LEVELS as lvl (lvl)} + + {SAMPLE_TEXTS[lvl]} + + {/each} +

+ Body: The quick brown fox jumps over the lazy dog. A short paragraph to show body rhythm and spacing at a glance. +

+
+
+ {#each GLOBAL_HEADING_TOKENS.filter(exists) as name (name)} + + {/each} +
+ {:else if activeTab === 'body'} +
+

+ The quick brown fox jumps over the lazy dog. This paragraph demonstrates your body typography — font, size, line-height, and text-wrap — all live. A longer line helps you judge measure and readability at a glance. +

+
+
+ {#each BODY_TOKENS.filter(exists) as name (name)} + + {/each} +
+ {:else if activeTab === 'mono'} +
+ const scale = computeFluidClamp(minSize, maxSize, minVp, maxVp); +// → clamp(1rem, 0.5rem + 2.5vw, 1.5rem) +
+
+ {#each MONO_TOKENS.filter(exists) as name (name)} + + {/each} +
+ {:else} + {@const lvl = activeTab} +
+ {SAMPLE_TEXTS[lvl]} +

+ Followed by a body line — judge the size contrast at a glance. +

+
+
+ {#each PER_LEVEL_TOKENS(lvl).filter(exists) as name (name)} + + {/each} +
+ {/if} +
+
+ + diff --git a/configurator/src/components/Preview.svelte b/configurator/src/components/Preview.svelte index cc443734..601740cf 100644 --- a/configurator/src/components/Preview.svelte +++ b/configurator/src/components/Preview.svelte @@ -46,15 +46,44 @@ const SECTIONS = [ { id: 'overview', label: 'Overview' }, { id: 'colors', label: 'Colors' }, + { id: 'gradients', label: 'Gradients' }, { id: 'palette', label: 'Palette' }, - { id: 'typography', label: 'Typography' }, + { id: 'typography', label: 'Type' }, + { id: 'spacing', label: 'Spacing' }, { id: 'layout', label: 'Layout' }, + { id: 'borders', label: 'Borders' }, + { id: 'shadows', label: 'Shadows' }, + { id: 'motion', label: 'Motion' }, + { id: 'effects', label: 'Effects' }, { id: 'macros', label: 'Macros' }, { id: 'tokens', label: 'Tokens' }, ]; + const DOMAIN_TO_SECTION = { + home: 'overview', + colors: 'colors', + gradients: 'gradients', + typography: 'typography', + spacing: 'spacing', + layout: 'layout', + borders: 'borders', + shadows: 'shadows', + motion: 'motion', + effects: 'effects', + misc: 'tokens', + wcag: 'colors', + themes: 'palette', + setup: 'overview', + cheatsheet: 'tokens', + }; + let activeSection = $state('overview'); + $effect(() => { + const sec = DOMAIN_TO_SECTION[ui.domain]; + if (sec) activeSection = sec; + }); + const baseStyle = $derived(buildPreviewDeclarations(overrides, ui.previewTheme)); const styleStr = $derived( ui.previewMotion === 'reduced' @@ -351,6 +380,17 @@ {#if activeSection === 'colors'} +
+

Applied to components

+
+ + + + + +
+
+

Brand colors + on-color text (auto WCAG black/white)

@@ -1031,6 +1071,331 @@ {/if} + + {#if activeSection === 'gradients'} + +
+

Brand gradients — --sf-gradient-*

+
+ {#each gradients as g (g)} +
{g}
+ {/each} +
+
+ +
+

Directional fade gradients

+
+ {#each ['fade--t','fade--b','fade--l','fade--r'] as g (g)} +
{g}
+ {/each} +
+
+ +
+

Gradient on real content — card + hero strip

+
+
+

Hero headline

+

Gradient background from --sf-gradient-primary

+
+
+ {#each ['secondary','tertiary','brand'] as g (g)} +
{g}
+ {/each} +
+
+
+ + {/if} + + + {#if activeSection === 'spacing'} + +
+

Full space scale — --sf-space-* (resize viewport to see fluid values)

+
+ {#each ['2xs','xs','s','m','l','xl','2xl','3xl'] as s (s)} +
+ --sf-space-{s} + + {s} +
+ {/each} +
+
+ +
+

Component spacing — gaps and padding

+
+
+
+
Item
+
Item
+
Item
+
+ gap: --sf-space-s +
+
+
+
Item
+
Item
+
Item
+
+ gap: --sf-space-m +
+
+
+
Item
+
Item
+
Item
+
+ gap: --sf-space-l +
+
+
+ +
+

Section padding — --sf-section-pad · --sf-content-gap

+
+
+
Section content
+
More content
+
+

Padding: --sf-section-pad · Gap: --sf-content-gap

+
+
+ + {/if} + + + {#if activeSection === 'borders'} + +
+

Border radius scale — --sf-radius-*

+
+ {#each radii as r (r)} +
+ + {r} +
+ {/each} +
+
+ +
+

Borders on real components

+
+ {#each ['s','m','l','xl','full'] as r (r)} +
+ radius-{r} +
+ {/each} +
+
+ +
+

Border color variants — --sf-color-border-*

+
+ {#each ['border','border--subtle','border--strong','border--focus','border--disabled','border--translucent'] as b (b)} +
+
+ --sf-color-{b} +
+ {/each} +
+
+ +
+

Focus ring — --sf-focus-ring-*

+
+ + + +
+
+ --sf-focus-ring-width + --sf-focus-ring-color + --sf-focus-ring-offset +
+
+ +
+

Border widths — --sf-border-width-*

+
+ {#each [1,2,3,4] as w (w)} +
--sf-border-width-{w} ({w}px default)
+ {/each} +
+
+ + {/if} + + + {#if activeSection === 'shadows'} + +
+

Elevation ramp — --sf-shadow-*

+
+ {#each ['xs','s','m','l','xl','2xl'] as s (s)} +
+ {s} +
+ {/each} +
+
+ +
+

Shadows on real UI components

+
+
+
+

Card — shadow-s

+

Subtle lift above background

+
+
+
+
+

Card — shadow-m

+

Default card elevation

+
+
+
+
+

Card — shadow-xl

+

Modal / overlay elevation

+
+
+
+
+ +
+

Specialty shadows

+
+ Text shadow +
+
Drop shadow
+
+ Glow +
+
+ +
+

Inner shadow — --sf-shadow-inner

+
+
Inset / inner shadow
+
+
+ + {/if} + + + {#if activeSection === 'motion'} + +
+

Live animations — hit 🐢 above to preview reduced motion

+
+
pulse
+
spin
+
bounce
+
slide·fade
+
shimmer
+
ping
+
+
+ +
+

Duration scale — sweeping bar shows the relative timing

+
+ {#each ['instant','fast','normal','slow'] as d (d)} +
+ --sf-duration-{d} +
+
+ {/each} +
+
+ +
+

Easing functions — --sf-ease-* (bar sweeps with each easing)

+
+ {#each ['in','out','in-out','spring','bounce'] as e (e)} +
+ --sf-ease-{e} +
+
+ {/each} +
+
+ +
+

Transition on hover — using duration + easing tokens

+
+
Hover me — color transition
+
Hover me — scale transform
+
+
+ + {/if} + + + {#if activeSection === 'effects'} + +
+

Blur scale — --sf-blur-*

+
+ {#each [['none','0px'],['subtle','2px'],['base','8px'],['strong','16px'],['heavy','40px']] as [label, fb] (label)} +
+
+ {label} +
+ {/each} +
+

Applied as backdrop-filter — requires a translucent background to be visible.

+
+ +
+

Opacity scale — --sf-opacity-*

+
+ {#each [['disabled','0.4'],['muted','0.6'],['subtle','0.75'],['mid','0.85'],['full','1.0']] as [label, fb] (label)} +
+
{label}
+ {fb} +
+ {/each} +
+
+ +
+

Scrim / overlay — --sf-scrim · --sf-overlay

+
+
+
+ --sf-scrim overlay +
+
+
+
+ --sf-overlay +
+
+
+
+ +
+

Frosted glass — backdrop-filter + surface

+
+
+
+

Glass surface

+

backdrop-filter: blur + translucent surface color

+
+
+
+
+ + {/if} +
@@ -1248,4 +1613,52 @@ .pv__ease-row { display: flex; align-items: center; gap: 10px; } .pv__ease-label { font-family: var(--sf-font-mono,monospace); font-size: 11px; color: var(--sf-color-text--muted,#888); min-width: 14ch; flex-shrink: 0; } .pv__ease-bar { height: 8px; flex: 1; background: var(--sf-color-primary,#4f8cff); border-radius: 4px; transform-origin: left; animation: pv-sweep 2s var(--pv-ease,ease) infinite; } + + /* ── Gradients section ── */ + .pv__grad-card { display: flex; flex-direction: column; gap: 8px; border-radius: var(--sf-radius-m,8px); overflow: hidden; border: 1px solid var(--sf-color-border,rgba(127,127,127,.3)); } + .pv__grad-card__hero { padding: 20px 16px 16px; display: flex; flex-direction: column; gap: 4px; min-height: 5rem; } + .pv__grad-card__title { margin: 0; font-size: var(--sf-text-xl,1.5rem); font-weight: 700; color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,.4); } + .pv__grad-card__sub { margin: 0; font-size: var(--sf-text-s,.875rem); color: rgba(255,255,255,.8); } + .pv__grad-card__row { display: flex; gap: 6px; padding: 8px; background: var(--sf-color-inset,rgba(127,127,127,.06)); } + .pv__grad-card__chip { flex: 1; min-height: 2.5rem; border-radius: var(--sf-radius-m,8px); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.3); } + + /* ── Spacing section ── */ + .pv__space--full .pv__space-row { padding-block: 2px; } + .pv__sp-demos { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px,1fr)); gap: 10px; } + .pv__sp-card { background: var(--sf-color-inset,rgba(127,127,127,.06)); border: 1px solid var(--sf-color-border,rgba(127,127,127,.2)); border-radius: var(--sf-radius-m,8px); padding: 10px; display: flex; flex-direction: column; gap: 8px; } + .pv__sp-card__inner { display: flex; flex-direction: column; } + .pv__sec-demo { background: var(--sf-color-inset,rgba(127,127,127,.06)); border: 1px solid var(--sf-color-border,rgba(127,127,127,.2)); border-radius: var(--sf-radius-m,8px); padding: var(--sf-section-pad,clamp(2rem,5vw,5rem)) var(--sf-gutter,1.5rem); } + .pv__sec-inner { display: flex; flex-direction: column; gap: var(--sf-content-gap,clamp(1rem,3vw,2rem)); } + + /* ── Borders section ── */ + .pv__border-cards { display: flex; flex-wrap: wrap; gap: 10px; } + .pv__border-card { padding: 12px 16px; background: var(--sf-color-surface,#fff); border: 1px solid var(--sf-color-border,rgba(127,127,127,.3)); font-size: 11px; font-family: monospace; color: var(--sf-color-text--muted,#888); min-width: 6rem; display: flex; align-items: center; justify-content: center; } + + /* ── Shadows section ── */ + .pv__elev-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end; padding: 12px 4px 20px; } + .pv__elev-card { background: var(--sf-color-surface,#fff); border-radius: var(--sf-radius-m,8px); width: 72px; height: 72px; display: grid; place-items: center; border: 1px solid var(--sf-color-border,rgba(127,127,127,.1)); } + .pv__elev-label { font-family: monospace; font-size: 10px; color: var(--sf-color-text--muted,#888); } + .pv__shadow-ui { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px,1fr)); gap: 16px; padding: 8px 4px 4px; } + + /* ── Motion section ── */ + .pv__trans-demo { display: flex; gap: 12px; flex-wrap: wrap; } + .pv__trans-card { padding: 12px 20px; background: var(--sf-color-surface,#fff); border: 1px solid var(--sf-color-border,rgba(127,127,127,.3)); border-radius: var(--sf-radius-m,8px); font-size: var(--sf-text-s,.875rem); font-weight: 500; cursor: default; transition: background var(--sf-duration-normal,250ms) var(--sf-ease-out,ease-out), color var(--sf-duration-normal,250ms) var(--sf-ease-out,ease-out), box-shadow var(--sf-duration-normal,250ms) var(--sf-ease-out,ease-out); } + .pv__trans-card:hover { background: var(--sf-color-primary,#4f8cff); color: var(--sf-color-text--on-primary,#fff); box-shadow: var(--sf-shadow-m,0 2px 8px rgba(0,0,0,.15)); } + .pv__trans-card--scale { transition: transform var(--sf-duration-normal,250ms) var(--sf-ease-spring,cubic-bezier(.34,1.56,.64,1)); } + .pv__trans-card--scale:hover { transform: scale(1.06); } + + /* ── Effects section ── */ + .pv__blur-row { display: flex; gap: 10px; flex-wrap: wrap; } + .pv__blur-item { display: flex; flex-direction: column; align-items: center; gap: 5px; } + .pv__blur-box { width: 70px; height: 70px; border-radius: var(--sf-radius-m,8px); background: var(--sf-color-surface,rgba(255,255,255,.3)); border: 1px solid var(--sf-color-border,rgba(255,255,255,.3)); position: relative; overflow: hidden; } + .pv__blur-box::before { content: ''; position: absolute; inset: 0; background: linear-gradient(135deg,var(--sf-color-primary,#4f8cff),var(--sf-color-tertiary,#8858ff)); opacity: 0.7; } + .pv__opac-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } + .pv__opac-item { display: flex; flex-direction: column; align-items: center; gap: 5px; } + .pv__scrim-demo { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } + .pv__scrim-photo { height: 80px; border-radius: var(--sf-radius-m,8px); overflow: hidden; background: linear-gradient(135deg,var(--sf-color-primary,#4f8cff),var(--sf-color-action,#0891b2)); position: relative; } + .pv__scrim-photo--alt { background: linear-gradient(135deg,var(--sf-color-tertiary,#8858ff),var(--sf-color-secondary,#6b7280)); } + .pv__scrim-layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; } + .pv__glass-demo { border-radius: var(--sf-radius-m,8px); overflow: hidden; } + .pv__glass-bg { padding: 24px; background: linear-gradient(135deg,var(--sf-color-primary,#4f8cff) 0%,var(--sf-color-tertiary,#8858ff) 50%,var(--sf-color-action,#0891b2) 100%); display: flex; align-items: center; justify-content: center; min-height: 8rem; } + .pv__glass-panel { background: rgba(255,255,255,.15); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,.3); border-radius: var(--sf-radius-m,8px); padding: 16px 20px; color: #fff; max-width: 280px; width: 100%; } diff --git a/configurator/src/components/RadiusEditor.svelte b/configurator/src/components/RadiusEditor.svelte new file mode 100644 index 00000000..888a5044 --- /dev/null +++ b/configurator/src/components/RadiusEditor.svelte @@ -0,0 +1,208 @@ + + +
+
+ {#each TABS as tab (tab.id)} + {@const modified = tabHasOverride(tab.id)} + + {/each} +
+ +
+ {#if activeTab === 'all'} +
+ {#each LEVELS.filter((l) => exists(l.token)) as lvl (lvl.id)} + {@const [w, h] = DEMO_SIZES[lvl.id]} +
+
+ {lvl.label} +
+ {/each} +
+
+ {#each GLOBAL_TOKENS.filter(exists) as name (name)} + + {/each} +
+ {:else} + {@const lvl = LEVELS.find((l) => l.id === activeTab)} + {#if lvl && exists(lvl.token)} + {@const [w, h] = DEMO_SIZES[activeTab]} +
+
+
+ {lvl.demo} +
+
+ {#each LEVELS.filter((l) => exists(l.token)) as l (l.id)} + {@const [sw, sh] = DEMO_SIZES[l.id]} + + {/each} +
+
+
+ +
+ {/if} + {/if} +
+
+ + diff --git a/configurator/src/components/ScaleGenerator.svelte b/configurator/src/components/ScaleGenerator.svelte index b482eec1..6b8b72a9 100644 --- a/configurator/src/components/ScaleGenerator.svelte +++ b/configurator/src/components/ScaleGenerator.svelte @@ -32,8 +32,15 @@ resetEnginePatch, resetViewportPatch, } from '../lib/fluidEngine.js'; + import { getFold, setFold } from '../lib/foldState.js'; - let { kinds = ['type', 'display', 'space'] } = $props(); + /** @type {{ kinds?: Array<'type'|'display'|'space'>, collapsible?: boolean }} */ + let { kinds = ['type', 'display', 'space'], collapsible = false } = $props(); + + // svelte-ignore state_referenced_locally + const foldKey = `generator:${kinds[0]}`; + // svelte-ignore state_referenced_locally + let open = $state(collapsible ? getFold(foldKey, true) : true); /** * The framework's display ramp: --sf-text-display-{s,m,l}, a separate modular @@ -178,6 +185,14 @@
+ {#if collapsible} + + {/if} Scale generator {#if kinds.length > 1}
@@ -195,6 +210,7 @@
+ {#if !collapsible || open}
@@ -291,6 +307,7 @@ {/each}
+ {/if}