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()} +
+ No {domain.label.toLowerCase()} tokens match {ui.query}.
+
No tokens match the current filters.
+ {/if} + {:else} + {#each grouped as cat (cat.category)} + {#each cat.groups as group (group.name)} +