Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 33 additions & 93 deletions configurator/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,149 +1,89 @@
<script>
/**
* App shell: header, three-pane body (sidebar Β· token list Β· live preview)
* and the bottom output drawer.
* App shell: header (brand Β· Basic/Advanced mode Β· preview toggle), the
* domain tab strip, the active domain panel (or the WCAG tool) beside the
* live preview, and the override-CSS output drawer.
*
* Owns the filtered/grouped derivation from the shared `ui` + `overrides`
* state and feeds it to the sidebar (category counts) and the main list.
* State lives in the shared `ui` store; this component just wires the active
* domain to its panel.
*/
import { allTokens, groupTokens, matchesQuery } from './lib/model.js';
import { ui, overrides } from './lib/store.svelte.js';
import { DOMAIN_BY_ID } from './lib/domains.js';
import { ui } from './lib/store.svelte.js';
import Header from './components/Header.svelte';
import Sidebar from './components/Sidebar.svelte';
import TokenGroup from './components/TokenGroup.svelte';
import TabNav from './components/TabNav.svelte';
import DomainPanel from './components/DomainPanel.svelte';
import OutputPanel from './components/OutputPanel.svelte';
import Preview from './components/Preview.svelte';
import WcagPanel from './components/WcagPanel.svelte';
import ScaleGenerator from './components/ScaleGenerator.svelte';

let showPreview = $state(true);

const query = $derived(ui.query.trim().toLowerCase());
const searching = $derived(query.length > 0);
const domain = $derived(DOMAIN_BY_ID.get(ui.domain) ?? DOMAIN_BY_ID.get('typography'));
const isTool = $derived(domain?.tool === 'wcag');

// Tokens visible under the current tier toggles, modified filter and search.
const visible = $derived(
allTokens.filter((t) => {
if (t.tier === 'INTERNAL' && !ui.showInternal) return false;
if (t.tier === 'PUBLIC-ADVANCED' && !ui.showAdvanced) return false;
if (ui.onlyModified && !(t.name in overrides)) return false;
return matchesQuery(t, query);
})
);

const grouped = $derived(groupTokens(visible));
const categories = $derived(
grouped.map((c) => ({ category: c.category, count: c.count }))
);

// Keep a valid active category selected as filters change.
// The search query is scoped to a domain's Advanced list; clear it when the
// active tab changes so a leftover filter never makes the next tab's list
// look empty (the search box only renders in Advanced mode).
$effect(() => {
if (grouped.length === 0) return;
if (!grouped.some((c) => c.category === ui.activeCategory)) {
ui.activeCategory = grouped[0].category;
}
ui.domain;
ui.query = '';
});

// When searching we show matches across every category; otherwise just the
// active one. The active-category object drives the non-search view.
const activeCat = $derived(
grouped.find((c) => c.category === ui.activeCategory) || null
);
</script>

<div class="shell">
<Header bind:showPreview />
<TabNav />

<div class="body" class:body--no-preview={!showPreview}>
{#if ui.view === 'tokens'}
<Sidebar {categories} />

<main class="list">
{#if visible.length === 0}
<div class="list__empty">
<p>No tokens match the current search and filters.</p>
</div>
{:else if searching}
{#each grouped as cat (cat.category)}
{#each cat.groups as g (g.name)}
<TokenGroup
name={g.name}
tokens={g.tokens}
description={g.description}
showCategory={true}
category={cat.category}
/>
{/each}
{/each}
{:else if activeCat}
<h2 class="list__heading">{activeCat.category}</h2>
{#each activeCat.groups as g (g.name)}
<TokenGroup name={g.name} tokens={g.tokens} description={g.description} />
{/each}
{/if}
{#if isTool}
<main class="main"><WcagPanel /></main>
{:else}
<main class="main">
{#key ui.domain}
<DomainPanel {domain} />
{/key}
</main>
{:else if ui.view === 'a11y'}
<main class="tool"><WcagPanel /></main>
{:else if ui.view === 'scales'}
<main class="tool"><ScaleGenerator /></main>
{/if}

{#if showPreview}
<Preview />
{/if}
</div>

{#if ui.view === 'tokens'}
<OutputPanel />
{/if}
<OutputPanel />
</div>

<style>
.shell {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-rows: auto auto minmax(0, 1fr) auto;
height: 100vh;
}
.body {
display: grid;
grid-template-columns: 264px minmax(0, 1fr) minmax(360px, 38%);
grid-template-columns: minmax(0, 1fr) minmax(360px, 38%);
min-height: 0;
}
.body--no-preview {
grid-template-columns: 264px minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr);
}
.list {
overflow-y: auto;
padding: 18px;
min-width: 0;
}
.tool {
grid-column: 1 / span 2;
.main {
min-width: 0;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.tool > :global(.wcag),
.tool > :global(.scales) {
.main > :global(.wcag),
.main > :global(.panel) {
flex: 1;
min-height: 0;
}
.list__heading {
margin: 0 0 14px;
font-size: 15px;
color: var(--cfg-text);
}
.list__empty {
color: var(--cfg-text-faint);
text-align: center;
padding-top: 60px;
}

@media (max-width: 1100px) {
.body,
.body--no-preview {
grid-template-columns: 220px minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr);
}
.body :global(.preview) {
display: none;
Expand Down
189 changes: 189 additions & 0 deletions configurator/src/components/DomainPanel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<script>
/**
* One domain tab (Typography, Spacing, Colors, …).
*
* BASIC mode β†’ a curated set of "essential" token rows plus the domain's
* basic generator (e.g. the fluid spacing scale).
* ADVANCED β†’ the essentials + every generator + the FULL domain token
* catalogue (grouped, searchable, tier-filtered), so nothing is
* out of reach.
*
* Reuses the existing TokenRow/TokenEditor (color picker, font picker, length
* inputs) and the shared ScaleGenerator, so each domain is just curation β€”
* no bespoke per-field code.
*/
import { allTokens, groupTokens, matchesQuery, tokenByName } from '../lib/model.js';
import { domainOf } from '../lib/domains.js';
import { ui, overrides } from '../lib/store.svelte.js';
import TokenGroup from './TokenGroup.svelte';
import TokenRow from './TokenRow.svelte';
import ScaleGenerator from './ScaleGenerator.svelte';

/** @type {{ domain: { id:string, label:string, blurb:string, essentials?:string[], basicGenerators?:string[], advancedGenerators?:string[] } }} */
let { domain } = $props();

const advanced = $derived(ui.mode === 'advanced');

// Curated essential rows β€” only those present in the active catalogue.
const essentials = $derived(
(domain.essentials ?? [])
.map((name) => tokenByName.get(name))
.filter(Boolean)
);

// Every token in this domain (for the advanced full catalogue).
const domainTokens = $derived(allTokens.filter((t) => domainOf(t) === domain.id));

const query = $derived(ui.query.trim().toLowerCase());

// Advanced list: tier + modified + search filters, then grouped.
const advancedVisible = $derived(
domainTokens.filter((t) => {
if (t.tier === 'INTERNAL' && !ui.showInternal) return false;
if (ui.onlyModified && !(t.name in overrides)) return false;
return matchesQuery(t, query);
})
);
const grouped = $derived(groupTokens(advancedVisible));

// 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 advancedGenerators = $derived(domain.advancedGenerators ?? []);
</script>

<div class="panel">
<header class="panel__head">
<div>
<h2 class="panel__title">{domain.label}</h2>
<p class="panel__blurb">{domain.blurb}</p>
</div>
{#if advanced}
<input
class="panel__search"
type="search"
placeholder="Filter {domainTokens.length} {domain.label.toLowerCase()} tokens…"
bind:value={ui.query}
spellcheck="false"
aria-label="Filter {domain.label} tokens"
/>
{/if}
</header>

{#if advanced}
<div class="panel__filters">
<label class="panel__check">
<input type="checkbox" bind:checked={ui.onlyModified} />
<span>Modified only</span>
</label>
<label class="panel__check">
<input type="checkbox" bind:checked={ui.showInternal} />
<span>Show internal</span>
</label>
</div>
{/if}

<!-- Essentials (always shown) -->
{#if essentials.length}
<section class="essentials">
<div class="essentials__rows">
{#each essentials as token (token.name)}
<TokenRow {token} />
{/each}
</div>
</section>
{/if}

<!-- Basic generator(s) β€” e.g. the fluid spacing scale -->
{#each basicGenerators as g (g)}
<ScaleGenerator kinds={[g]} />
{/each}

{#if advanced}
<!-- Advanced generators β€” e.g. the type + display scale -->
{#if advancedGenerators.length}
<ScaleGenerator kinds={advancedGenerators} />
{/if}

<!-- Full domain catalogue -->
{#if grouped.length === 0}
<p class="panel__empty">No tokens match the current search and filters.</p>
{:else}
{#each grouped as cat (cat.category)}
{#each cat.groups as group (group.name)}
<TokenGroup
name={group.name}
tokens={group.tokens}
description={group.description}
showCategory={categoryCount > 1}
category={cat.category}
/>
{/each}
{/each}
{/if}
{:else if essentials.length === 0 && basicGenerators.length === 0}
<p class="panel__hint">
Switch to <strong>Advanced</strong> to edit the full set of {domain.label.toLowerCase()} tokens.
</p>
{/if}
</div>

<style>
.panel {
overflow-y: auto;
padding: 18px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.panel__head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
}
.panel__title { margin: 0; font-size: 18px; }
.panel__blurb { margin: 3px 0 0; color: var(--cfg-text-muted); font-size: 13px; }
.panel__search {
flex: 0 1 320px;
background: var(--cfg-bg);
border: 1px solid var(--cfg-border-strong);
border-radius: var(--cfg-radius-s);
color: var(--cfg-text);
padding: 8px 11px;
font-size: 13px;
}
.panel__search:focus { outline: 2px solid var(--cfg-accent); outline-offset: -1px; }

.panel__filters {
display: flex;
gap: 18px;
margin-top: -4px;
}
.panel__check {
display: flex;
align-items: center;
gap: 7px;
font-size: 13px;
color: var(--cfg-text-muted);
cursor: pointer;
}
.panel__check input { accent-color: var(--cfg-accent-strong); }

.essentials {
border: 1px solid var(--cfg-border);
border-radius: var(--cfg-radius);
background: var(--cfg-surface);
overflow: hidden;
}
.panel__empty,
.panel__hint {
color: var(--cfg-text-faint);
font-size: 13px;
padding: 20px 4px;
}
</style>
Loading