diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index a86c6827..ad073452 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -4,7 +4,7 @@ * * Layout (desktop ≥ 1100px): * ┌─────────────────────────────────────────────────────────────────┐ - * │ Header (brand · search · basic/advanced · theme · preview) │ + * │ Header (brand · search · undo/redo · theme · preview) │ * ├──────────┬──┬────────────────────────────────────┬──┬───────────┤ * │ │ │ │ │ │ * │ Sidebar │↔│ Domain panel / WCAG tool │↔│ Preview │ @@ -23,7 +23,7 @@ * State lives in the shared `ui` store; this component just wires the * active domain to its panel. */ - import { DOMAINS, DOMAIN_BY_ID, BASIC_DOMAIN_IDS } from './lib/domains.js'; + import { DOMAINS, DOMAIN_BY_ID } from './lib/domains.js'; import { ui, undo, redo, overrides, loadSharedConfig } from './lib/store.svelte.js'; import { buildShareUrl } from './lib/share.js'; import { UI_STORAGE_KEY } from './lib/uiState.js'; @@ -97,20 +97,10 @@ try { localStorage.setItem(WIDTHS_KEY, JSON.stringify({ sidebar: sidebarWidth, preview: previewWidth })); } catch { /* ignore */ } } - const home = $derived(ui.mode === 'basic' && ui.domain === 'home'); + const home = $derived(ui.domain === 'home'); const domain = $derived(DOMAIN_BY_ID.get(ui.domain) ?? DOMAIN_BY_ID.get('colors')); const tool = $derived(domain?.tool ?? ''); - // Keep the active domain valid for the current mode: Home exists only in - // Basic, and Basic hides the non-checklist domains (Motion, Effects, …). - $effect(() => { - if (ui.mode === 'advanced' && ui.domain === 'home') { - ui.domain = 'colors'; - } else if (ui.mode === 'basic' && ui.domain !== 'home' && !BASIC_DOMAIN_IDS.includes(ui.domain)) { - ui.domain = 'home'; - } - }); - // Keep the contrast-probe host in sync with the user's cascade so every // ContrastBadge resolves `var(...)`, `light-dark()` and `oklch(from ...)` // expressions correctly. setProbeContext is internally idempotent. @@ -157,7 +147,7 @@ // Persist the navigation prefs so a reload restores where the user was. // Restore (with validation) happens in store.svelte.js via sanitiseUiState. $effect(() => { - const snapshot = JSON.stringify({ mode: ui.mode, domain: ui.domain, outputMode: ui.outputMode, uiTheme: ui.uiTheme, bundle: ui.bundle }); + const snapshot = JSON.stringify({ domain: ui.domain, outputMode: ui.outputMode, uiTheme: ui.uiTheme, bundle: ui.bundle }); try { localStorage.setItem(UI_STORAGE_KEY, snapshot); } catch { @@ -183,9 +173,8 @@ return () => mql.removeEventListener('change', onChange); }); - // Keyboard shortcuts: '/' focuses the search box; 'b'/'a' switch mode; - // '[' / ']' cycle domains; 'Escape' clears the search; Ctrl+Z / Ctrl+Shift+Z - // step the override history. + // Keyboard shortcuts: '/' focuses the search box; '[' / ']' cycle domains; + // 'Escape' clears the search; Ctrl+Z / Ctrl+Shift+Z step the override history. function onKey(e) { // Ctrl/Cmd + Z handlers run regardless of focus context — undo/redo // is a global affordance, even while typing in an input. @@ -222,15 +211,9 @@ } else if (e.key === '/') { e.preventDefault(); document.querySelector('#cfg-search')?.focus(); - } else if (e.key === 'b' || e.key === 'B') { - ui.mode = 'basic'; - } else if (e.key === 'a' || e.key === 'A') { - ui.mode = 'advanced'; } else if (e.key === '[' || e.key === ']') { - // Cycle the domains visible in the current mode. - const ids = ui.mode === 'basic' - ? ['home', ...BASIC_DOMAIN_IDS] - : DOMAINS.map((d) => d.id); + // Cycle Overview + the full domain taxonomy. + const ids = ['home', ...DOMAINS.map((d) => d.id)]; const i = ids.indexOf(ui.domain); if (i !== -1) { const next = (i + (e.key === ']' ? 1 : -1) + ids.length) % ids.length; diff --git a/configurator/src/components/DomainPanel.svelte b/configurator/src/components/DomainPanel.svelte index 96240e54..be570765 100644 --- a/configurator/src/components/DomainPanel.svelte +++ b/configurator/src/components/DomainPanel.svelte @@ -2,16 +2,24 @@ /** * One domain tab (Colors, Typography, Spacing, …). * - * BASIC mode → intro copy + curated essential rows + the domain's basic - * generator (e.g. fluid spacing scale). No quick knobs — - * global multipliers are a power tool, not a starting point. - * ADVANCED → essentials + every generator + the FULL domain catalogue - * (grouped, searchable, tier-filtered, modified-only filter), - * capped by a collapsed "Power knobs" group for the global - * multipliers, so nothing is ever out of reach. + * The panel has two stacked zones, in the same order for every category: * - * Reuses the existing TokenRow / TokenEditor / OklchPicker / ScaleGenerator, - * so each domain is just curation — no bespoke per-field code. + * SETTINGS (always visible, inputs-first) — the system inputs a typical + * project sets: one-click presets, brand colors, font stacks, scale + * generators and the global "Scaling" multipliers. Curated via + * lib/basics.js + domains.js so each domain is just curation, no + * bespoke per-field code. + * + * ALL VARIABLES (progressive disclosure) — a collapsed disclosure holding + * the FULL domain catalogue (grouped, with tier / modified / usage + * filters), so every token is one click away. Domains with no curated + * Settings surface (e.g. Misc) render the catalogue inline instead of + * hiding it behind a redundant click. + * + * An active search collapses the panel to the filtered catalogue alone — + * search means "find any token in this category", curated forms aside. + * + * Reuses the existing TokenRow / TokenEditor / OklchPicker / ScaleGenerator. */ import { allTokens, groupTokens, matchesQuery, tokenByName } from '../lib/model.js'; import { domainOf, KNOBS_BY_DOMAIN, DOCS_BASE_URL } from '../lib/domains.js'; @@ -26,11 +34,11 @@ import QuickKnobs from './QuickKnobs.svelte'; import StylePresetRow from './StylePresetRow.svelte'; - /** @type {{ domain: { id:string, label:string, icon:string, blurb:string, intro?:string, powerIntro?:string, essentials?:string[], basicGenerators?:string[], advancedGenerators?:string[], brandColors?:boolean } }} */ + /** @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(); - const advanced = $derived(ui.mode === 'advanced'); const query = $derived(ui.query.trim().toLowerCase()); + const searching = $derived(query.length > 0); // Curated essential rows — only those present in the active catalogue. const essentials = $derived( @@ -39,7 +47,7 @@ .filter(Boolean) ); - // Friendly Basic groups (lib/basics.js): label + help per control, with + // Friendly curated groups (lib/basics.js): label + help per control, with // controls whose token is missing from the catalogue dropped defensively // (tests/basics.test.js makes that a CI failure, never a silent gap). const basicGroups = $derived( @@ -56,10 +64,10 @@ // Every token in this domain. const domainTokens = $derived(allTokens.filter((t) => domainOf(t) === domain.id)); - // Advanced list: tier + modified + usage + search filters, then grouped. + // All-variables catalogue: tier + modified + usage + search filters, grouped. // Use property-access (overrides[t.name]) instead of `in` so Svelte 5's // state proxy triggers reactivity via the `get` trap on every key check. - const advancedVisible = $derived( + const catalogueVisible = $derived( domainTokens.filter((t) => { if (t.tier === 'INTERNAL' && !ui.showInternal) return false; if (ui.onlyModified && overrides[t.name] == null) return false; @@ -68,47 +76,35 @@ return matchesQuery(t, query); }) ); - const grouped = $derived(groupTokens(advancedVisible)); - - // The curated Basic surface for this domain: essentials plus the brand - // light/dark pair tokens. Basic search renders matches from this set only; - // everything else is one click away via the "more in Advanced" affordance. - const basicSurface = $derived.by(() => { - const s = new Set(domain.essentials ?? []); - if (domain.brandColors) { - for (const { key } of BRAND_COLOR_KEYS) { - s.add(`--sf-color-${key}-light`); - s.add(`--sf-color-${key}-dark`); - } - } - return s; - }); - - // Basic mode + active search: matches across the whole domain, split into - // curated-surface hits (rendered) and the advanced-only remainder (count). - const basicSearch = $derived.by(() => { - if (advanced || !query) return null; - const hits = domainTokens.filter((t) => { - if (t.tier === 'INTERNAL' && !ui.showInternal) return false; - return matchesQuery(t, query); - }); - const shown = hits.filter((t) => basicSurface.has(t.name)); - return { shown, hiddenCount: hits.length - shown.length }; - }); + const grouped = $derived(groupTokens(catalogueVisible)); // Only label each group's category when more than one is present in this // domain (otherwise the repeated "Core tokens" prefix is just noise). const categoryCount = $derived(new Set(grouped.map((c) => c.category)).size); - const basicGenerators = $derived(domain.basicGenerators ?? []); + const generators = $derived(domain.basicGenerators ?? []); - // Power knobs for this domain (global multipliers that cascade through - // many derived tokens — see lib/domains.js → KNOBS_BY_DOMAIN). Advanced - // only: they sit in a collapsed, visually fenced group below the catalogue. + // Scaling knobs for this domain (global multipliers that cascade through + // many derived tokens — see lib/domains.js → KNOBS_BY_DOMAIN). They sit in + // the Settings zone as a "Scaling" group. const knobs = $derived(KNOBS_BY_DOMAIN[domain.id] ?? []); - const totalDriven = $derived(knobs.reduce((n, k) => n + (k.driving ?? 1), 0)); - // Modified tokens within this domain — surfaced as a collapsible block. + // Does this domain expose a curated Settings surface above the raw + // catalogue? When it does not (e.g. Misc), the All-variables list is the + // whole panel and renders inline rather than behind a disclosure. + const hasSettings = $derived( + !!domain.brandColors || + !!STYLE_PRESETS_BY_DOMAIN[domain.id] || + basicGroups.length > 0 || + essentials.length > 0 || + generators.length > 0 || + knobs.length > 0 + ); + + // "Show all variables" disclosure state (only meaningful when hasSettings). + let showAll = $state(false); + + // Modified tokens within this domain — drives the header "Reset N" button. const modifiedHere = $derived( domainTokens.filter((t) => overrides[t.name] != null) ); @@ -130,6 +126,66 @@ {/snippet} +{#snippet filters()} +
+
+ + + +
+ + +
+{/snippet} + +{#snippet catalogue()} + {#if grouped.length === 0} + {#if searching} +

+ No {domain.label.toLowerCase()} tokens match {ui.query}. +

+ {:else} +

No tokens match the current filters.

+ {/if} + {:else} + {#each grouped as cat (cat.category)} + {#each cat.groups as group (group.name)} + 1} + category={cat.category} + /> + {/each} + {/each} + {/if} +{/snippet} +
@@ -141,39 +197,6 @@
- {#if advanced} -
- - - -
- - - {/if} {#if modifiedHere.length} - {/if} - {/if} + {#if searching} + + {@render filters()} + {@render catalogue()} {:else} + + - {#if !advanced && domain.intro} + projects change it. --> + {#if domain.intro}

{domain.intro}

{/if} - - {#if !advanced && STYLE_PRESETS_BY_DOMAIN[domain.id]} + + {#if STYLE_PRESETS_BY_DOMAIN[domain.id]} {/if} - - {#if domain.brandColors && !advanced} - + + {#if domain.brandColors}
{@render cardHead( 'Brand & status colors', BRAND_COLOR_KEYS.length, - advanced ? '' : 'Set light-mode values — dark mode is auto-derived. Click the dark swatch to pin a custom value.' + 'Set light-mode values — dark mode is auto-derived. Click the dark swatch to pin a custom value.' )}
{#each BRAND_COLOR_KEYS as { key, label } (key)} @@ -246,8 +244,7 @@ {/each}
- {:else if !advanced && basicGroups.length} - + {:else if basicGroups.length} {#each basicGroups as group (group.title)}
{@render cardHead(group.title, group.controls.length)} @@ -258,16 +255,9 @@
{/each} - {:else if !advanced && essentials.length} - + {:else if essentials.length}
- {@render cardHead( - 'Essentials', - essentials.length, - `Curated for most projects · switch to Advanced (A) for the full ${domainTokens.length}-token catalogue.` - )} + {@render cardHead('Essentials', essentials.length, 'Curated for most projects.')}
{#each essentials as token (token.name)} @@ -276,50 +266,34 @@
{/if} - - {#each basicGenerators as g (g)} + + {#each generators as g (g)} {/each} - {#if advanced} - - {#if grouped.length === 0} -

No tokens match the current search and filters.

- {:else} - {#each grouped as cat (cat.category)} - {#each cat.groups as group (group.name)} - 1} - category={cat.category} - /> - {/each} - {/each} - {/if} - - - {#if knobs.length} -
- - - Power knobs - drives {totalDriven} token{totalDriven === 1 ? '' : 's'} - - {#if domain.powerIntro} -

{domain.powerIntro}

- {/if} - -
- {/if} - {:else if !domain.brandColors && essentials.length === 0 && basicGenerators.length === 0} - -

- This domain has no curated basics — switch to Advanced (A) to edit the full catalogue. -

+ + {#if knobs.length} + + {/if} + + + {#if hasSettings} +
+ + + {showAll ? 'Hide' : 'Show'} all variables + {domainTokens.length} token{domainTokens.length === 1 ? '' : 's'} + +
+ {@render filters()} + {@render catalogue()} +
+
+ {:else} + + {@render filters()} + {@render catalogue()} {/if} {#if domain.docsPath} @@ -443,31 +417,14 @@ border-radius: var(--cfg-radius-s); } - .panel__more { - align-self: center; - padding: 8px 16px; - font-size: 12.5px; - color: var(--cfg-text-muted); - background: var(--cfg-surface-2); - border: 1px dashed var(--cfg-border-strong); - border-radius: var(--cfg-radius-s); - cursor: pointer; - transition: color 0.12s, border-color 0.12s; - } - .panel__more:hover { - color: var(--cfg-text); - border-color: var(--cfg-accent-strong); - } - - /* Power knobs — visually fenced: these multipliers cascade through dozens - of tokens, so the group reads as "handle with intent". */ - .power { - border: 1px solid rgba(240, 173, 78, 0.45); + /* All-variables disclosure — the full domain catalogue, one click away. */ + .allvars { + border: 1px solid var(--cfg-border); border-radius: var(--cfg-radius); - background: rgba(240, 173, 78, 0.06); + background: var(--cfg-surface); overflow: clip; } - .power__summary { + .allvars__summary { display: flex; align-items: center; gap: 10px; @@ -476,38 +433,44 @@ list-style: none; user-select: none; } - .power__summary::-webkit-details-marker { display: none; } - .power__summary:hover { background: rgba(240, 173, 78, 0.1); } - .power__bolt { font-size: 14px; } - .power__title { + .allvars__summary::-webkit-details-marker { display: none; } + .allvars__summary:hover { background: var(--cfg-surface-2); } + .allvars__chev { + font-size: 16px; + line-height: 1; + color: var(--cfg-text-faint); + transition: transform 0.15s; + } + .allvars[open] .allvars__chev { transform: rotate(90deg); } + .allvars__title { font-size: 12.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; + color: var(--cfg-text); } - .power__chip { + .allvars__count { font-family: var(--cfg-mono); - font-size: 10.5px; - color: rgb(240, 173, 78); - border: 1px solid rgba(240, 173, 78, 0.45); - border-radius: 999px; - padding: 1px 8px; + font-size: 11px; + color: var(--cfg-text-faint); } - .power__intro { - margin: 0; - padding: 0 16px 10px; - font-size: 12.5px; - line-height: 1.55; - color: var(--cfg-text-muted); + .allvars__body { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + border-top: 1px solid var(--cfg-border); } - .power > :global(.knobs) { - border: 0; - border-top: 1px solid rgba(240, 173, 78, 0.25); - border-radius: 0; + + /* Filter bar above the catalogue (usage segment + modified/internal toggles). */ + .allvars__filters { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; } - .panel__empty, - .panel__hint { + .panel__empty { color: var(--cfg-text-faint); font-size: 13.5px; padding: 20px 4px; diff --git a/configurator/src/components/Header.svelte b/configurator/src/components/Header.svelte index 6a13a93e..4225a833 100644 --- a/configurator/src/components/Header.svelte +++ b/configurator/src/components/Header.svelte @@ -4,11 +4,9 @@ * * Contents (left → right): * • Brand mark + product title + framework version pill - * • Global search input (filters tokens across the active domain; - * also triggers cross-domain matches in advanced mode via the panel) - * • Basic / Advanced segmented control (global mode toggle) - * • Light / Dark preview-theme toggle - * • Sidebar / Preview pane toggles + * • Global search input (filters the active domain's All-variables list) + * • Undo / redo + * • Share-link / Light-Dark theme / Sidebar / Preview pane toggles * * Below ~760px the labels collapse to icons but every control stays * reachable. Keyboard shortcut: `/` focuses the search box. @@ -95,23 +93,6 @@ >↷ -
- - -
- - - {/if} +
  • + +
  • {#each visibleDomains as d (d.id)}