Skip to content

Commit f43f751

Browse files
Merge pull request #398 from codeslash-dev/codex/fix-pr-397
UI: Add Heading/Radius editors, ContainerBars, expanded Preview sections, shade strip and fold-state persistence
2 parents 1a3197a + 9edd540 commit f43f751

12 files changed

Lines changed: 1162 additions & 97 deletions

configurator/src/components/BrandColorRow.svelte

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
* Layout:
1111
* [label] [light swatch | text input] → [dark swatch | "auto" or value] [⟲]
1212
*/
13-
import { overrides, setOverride, clearOverride } from '../lib/store.svelte.js';
13+
import { overrides, setOverride, clearOverride, ui } from '../lib/store.svelte.js';
1414
import { defaultsByName } from '../lib/model.js';
1515
import { computeAutoDark } from '../lib/brandColors.js';
16+
import { measureBackground } from '../lib/probeHost.js';
1617
import OklchPicker from './OklchPicker.svelte';
1718
1819
/** @type {{ colorKey: string, label: string }} */
@@ -72,6 +73,23 @@
7273
7374
function resetLight() { clearOverride(lightName); }
7475
function resetDark() { clearOverride(darkName); }
76+
77+
// ── Inline shade strip ───────────────────────────────────────────────────
78+
const SHADE_SUFFIXES = ['-superlight', '-xlight', '-lighter', '', '-darker', '-xdark', '-superdark'];
79+
let shadeColors = $state([]);
80+
81+
$effect(() => {
82+
void overrides[lightName];
83+
void overrides[darkName];
84+
void ui.previewTheme;
85+
queueMicrotask(() => {
86+
shadeColors = SHADE_SUFFIXES.map((s) => {
87+
const rgb = measureBackground(`var(--sf-color-${colorKey}${s})`);
88+
return rgb && rgb !== 'rgba(0, 0, 0, 0)' ? rgb : null;
89+
});
90+
});
91+
});
92+
7593
</script>
7694
7795
<div class="bcr" class:bcr--light-mod={lightModified} class:bcr--dark-mod={darkModified}>
@@ -140,6 +158,18 @@
140158
<span class="bcr__auto" title="Auto-derived from the light color via OKLCH. Click the swatch to pin a custom value.">auto</span>
141159
{/if}
142160
</div>
161+
162+
<!-- Inline shade strip -->
163+
<div class="bcr__strip" aria-label="{label} shade ramp">
164+
{#each shadeColors as bg, i (i)}
165+
<div
166+
class="bcr__strip-swatch"
167+
class:bcr__strip-swatch--empty={!bg}
168+
style:background-color={bg ?? 'transparent'}
169+
title="--sf-color-{colorKey}{SHADE_SUFFIXES[i] || ' (base)'}"
170+
></div>
171+
{/each}
172+
</div>
143173
</div>
144174
145175
<!-- Floating picker (shared for light and dark) -->
@@ -158,9 +188,10 @@
158188
.bcr {
159189
display: grid;
160190
grid-template-columns: 80px 1fr 18px 1fr;
191+
grid-template-rows: auto auto;
161192
gap: 8px;
162193
align-items: center;
163-
padding: 8px 16px;
194+
padding: 8px 16px 10px;
164195
border-bottom: 1px solid var(--cfg-border);
165196
transition: background 0.15s;
166197
}
@@ -237,6 +268,25 @@
237268
cursor: default;
238269
}
239270
271+
.bcr__strip {
272+
grid-column: 1 / -1;
273+
display: grid;
274+
grid-template-columns: repeat(7, 1fr);
275+
gap: 2px;
276+
border-radius: var(--cfg-radius-s, 4px);
277+
overflow: clip;
278+
}
279+
280+
.bcr__strip-swatch {
281+
height: 8px;
282+
background-image: conic-gradient(#444 25%, #2a2a2a 0 50%, #444 0 75%, #2a2a2a 0);
283+
background-size: 6px 6px;
284+
transition: background-color 0.2s ease;
285+
}
286+
.bcr__strip-swatch:not(.bcr__strip-swatch--empty) {
287+
background-image: none;
288+
}
289+
240290
@media (max-width: 720px) {
241291
.bcr {
242292
grid-template-columns: 60px 1fr 14px 1fr;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<script>
2+
/**
3+
* Visual ruler for container-width tokens.
4+
* Renders a proportional bar for each --sf-container-* token so users can
5+
* compare relative widths at a glance and see the live value alongside it.
6+
*/
7+
import { overrides } from '../lib/store.svelte.js';
8+
import { tokenByName, defaultsByName } from '../lib/model.js';
9+
10+
const CONTAINERS = [
11+
{ name: '--sf-container-narrow', label: 'Narrow', help: 'Asides and narrow columns' },
12+
{ name: '--sf-container-prose', label: 'Prose', help: 'Long-form readable line length' },
13+
{ name: '--sf-container-default',label: 'Default', help: 'Main content container' },
14+
{ name: '--sf-container-wide', label: 'Wide', help: 'Marketing & docs sections' },
15+
{ name: '--sf-container-full', label: 'Full', help: 'Edge-to-edge / fluid' },
16+
];
17+
18+
const exists = (name) => tokenByName.has(name);
19+
20+
/** Extract a pixel value from a CSS value string (handles rem, ch, px, %). */
21+
function parsePx(raw) {
22+
if (!raw) return null;
23+
const n = parseFloat(raw);
24+
if (!Number.isFinite(n)) return null;
25+
if (/rem/.test(raw)) return n * 16;
26+
if (/ch/.test(raw)) return n * 9; // ~9px per ch at 16px base
27+
if (/em/.test(raw)) return n * 16;
28+
if (/%/.test(raw)) return 1200 * (n / 100);
29+
return n; // assume px
30+
}
31+
32+
/** Live effective value string for display, pulling overrides → defaults. */
33+
function effectiveValue(name) {
34+
return overrides[name] ?? defaultsByName.get(name) ?? '';
35+
}
36+
37+
const rows = $derived(
38+
CONTAINERS.filter((c) => exists(c.name)).map((c) => {
39+
const raw = effectiveValue(c.name);
40+
return { ...c, raw, px: parsePx(raw) };
41+
})
42+
);
43+
44+
const maxPx = $derived(Math.max(...rows.map((r) => r.px ?? 0), 1));
45+
const modified = (name) => overrides[name] != null;
46+
</script>
47+
48+
<div class="cbars">
49+
{#each rows as row (row.name)}
50+
{@const pct = row.px != null ? Math.min(100, (row.px / maxPx) * 100) : 0}
51+
{@const mod = modified(row.name)}
52+
<div class="cbars__row" class:cbars__row--mod={mod}>
53+
<div class="cbars__meta">
54+
<span class="cbars__label">{row.label}</span>
55+
<code class="cbars__val" class:cbars__val--default={!mod}>{row.raw || ''}</code>
56+
</div>
57+
<div class="cbars__track">
58+
<div class="cbars__bar" style:width="{pct}%" title="{row.name}: {row.raw}"></div>
59+
{#if row.px != null}
60+
<span class="cbars__px">~{Math.round(row.px)}px</span>
61+
{/if}
62+
</div>
63+
{#if row.help}
64+
<p class="cbars__help">{row.help}</p>
65+
{/if}
66+
</div>
67+
{/each}
68+
</div>
69+
70+
<style>
71+
.cbars {
72+
display: flex; flex-direction: column; gap: 0;
73+
padding: 12px 16px; border: 1px solid var(--cfg-border);
74+
border-radius: var(--cfg-radius); background: var(--cfg-bg-2);
75+
}
76+
77+
.cbars__row {
78+
padding: 10px 0;
79+
border-bottom: 1px solid var(--cfg-border);
80+
}
81+
.cbars__row:last-child { border-bottom: none; }
82+
.cbars__row--mod { box-shadow: inset 3px 0 0 var(--cfg-accent-strong); padding-left: 6px; }
83+
84+
.cbars__meta {
85+
display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
86+
margin-bottom: 6px;
87+
}
88+
.cbars__label {
89+
font-size: 12px; font-weight: 700; color: var(--cfg-text);
90+
}
91+
.cbars__val {
92+
font-size: 11px; color: var(--cfg-accent-strong);
93+
}
94+
.cbars__val--default { color: var(--cfg-text-faint); }
95+
96+
.cbars__track {
97+
position: relative; height: 12px; background: var(--cfg-border);
98+
border-radius: 999px; overflow: visible; display: flex; align-items: center;
99+
}
100+
101+
.cbars__bar {
102+
height: 100%; border-radius: 999px;
103+
background: var(--cfg-accent-strong); opacity: 0.75;
104+
transition: width 0.25s ease;
105+
min-width: 4px;
106+
}
107+
.cbars__row--mod .cbars__bar { opacity: 1; }
108+
109+
.cbars__px {
110+
position: absolute; left: calc(100% + 6px);
111+
font-size: 9.5px; color: var(--cfg-text-faint); white-space: nowrap;
112+
}
113+
114+
.cbars__help {
115+
margin: 4px 0 0; font-size: 11px; color: var(--cfg-text-faint);
116+
}
117+
</style>

configurator/src/components/DomainPanel.svelte

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@
3737
import StylePresetRow from './StylePresetRow.svelte';
3838
import ColorAssignments from './ColorAssignments.svelte';
3939
import ShadeRamp from './ShadeRamp.svelte';
40-
import DomainPreview from './DomainPreview.svelte';
4140
import SmartSettings from './SmartSettings.svelte';
41+
import HeadingEditor from './HeadingEditor.svelte';
42+
import RadiusEditor from './RadiusEditor.svelte';
43+
import ContainerBars from './ContainerBars.svelte';
4244
import Icon from './Icon.svelte';
43-
import { DOMAIN_PREVIEWS } from '../lib/domainPreviews.js';
4445
4546
/** @type {{ domain: { id:string, label:string, icon:string, blurb:string, intro?:string, scaleIntro?:string, essentials?:string[], basicGenerators?:string[], brandColors?:boolean, docsPath?:string } }} */
4647
let { domain } = $props();
@@ -105,11 +106,6 @@
105106
let showColorRoles = $state(true);
106107
let showShadeRamp = $state(false);
107108
108-
// Preview disclosure (open by default; for generator domains it appears below generators).
109-
let showPreview = $state(true);
110-
111-
const previewSpec = $derived(DOMAIN_PREVIEWS[domain.id]);
112-
113109
const BRAND_PRIMARY = BRAND_COLOR_KEYS.filter((c) => ['base', 'neutral', 'primary'].includes(c.key));
114110
const BRAND_SECONDARY = BRAND_COLOR_KEYS.filter((c) => ['secondary', 'tertiary', 'action'].includes(c.key));
115111
const BRAND_STATUS = BRAND_COLOR_KEYS.filter((c) => c.group === 'status');
@@ -231,47 +227,31 @@
231227
{@render catalogue()}
232228
{:else}
233229
234-
<!-- ── ZONE 1: LIVE PREVIEW (always leads the panel) ─────────────────
235-
Colors: Semantic-roles swatch grid.
236-
All token domains: DomainPreview card, open by default.
237-
For generator domains (typography/spacing) the generators are
238-
placed immediately BELOW the preview so the specimen updates in
239-
direct visual response to Apply — no scrolling required.
230+
<!-- ── ZONE 1: CONTROLS (live preview now lives in the right Preview Hub) ──
231+
Colors: Semantic-roles swatch grid (editing UI, not just preview).
232+
Generators (typography/spacing): collapsible ScaleGenerator.
233+
All domains: QuickKnobs (scaling multipliers) if present.
240234
─────────────────────────────────────────────────────────────────────── -->
241235
242236
{#if domain.brandColors}
243237
<details class="cfg-card panel__card panel__card--lead" bind:open={showColorRoles}>
244238
{@render expandSummary('Semantic roles', 'How your brand colors surface')}
245239
<ColorAssignments />
246240
</details>
247-
<!-- Contrast/focus knobs right after the roles they control -->
248241
{#if knobs.length}
249242
<QuickKnobs {knobs} title="Scaling" blurb={domain.scaleIntro ?? ''} />
250243
{/if}
251244
252-
{:else if previewSpec}
253-
<!-- Preview leads for every token domain — generator or not -->
254-
<details class="cfg-card panel__card panel__card--lead" bind:open={showPreview}>
255-
{@render expandSummary('Preview', previewSpec.blurb)}
256-
<DomainPreview domain={domain.id} />
257-
</details>
258-
259-
<!-- Generator domains: scale generators immediately below the preview
260-
so the specimen is in direct view while the user tunes the ramp. -->
261-
{#if hasGenerators}
262-
{#each generators as g (g)}
263-
<ScaleGenerator kinds={[g]} />
264-
{/each}
265-
<!-- Scaling knobs follow the generator controls -->
266-
{#if knobs.length}
267-
<QuickKnobs {knobs} title="Scaling" blurb={domain.scaleIntro ?? ''} />
268-
{/if}
269-
{:else}
270-
<!-- Non-generator domains: knobs follow the preview directly -->
271-
{#if knobs.length}
272-
<QuickKnobs {knobs} title="Scaling" blurb={domain.scaleIntro ?? ''} />
273-
{/if}
245+
{:else if hasGenerators}
246+
{#each generators as g (g)}
247+
<ScaleGenerator kinds={[g]} collapsible />
248+
{/each}
249+
{#if knobs.length}
250+
<QuickKnobs {knobs} title="Scaling" blurb={domain.scaleIntro ?? ''} />
274251
{/if}
252+
253+
{:else if knobs.length}
254+
<QuickKnobs {knobs} title="Scaling" blurb={domain.scaleIntro ?? ''} />
275255
{/if}
276256
277257
<!-- ── ZONE 2: SETTINGS (inputs-first) ────────────────────────────── -->
@@ -349,6 +329,18 @@
349329
<ShadeRamp />
350330
</details>
351331
332+
{:else if domain.id === 'typography'}
333+
<!-- Heading-level tab editor replaces flat basicGroups -->
334+
<HeadingEditor />
335+
336+
{:else if domain.id === 'borders'}
337+
<!-- Radius level tab editor with shape specimens -->
338+
<RadiusEditor />
339+
340+
{:else if domain.id === 'layout'}
341+
<!-- Container width comparison bars -->
342+
<ContainerBars />
343+
352344
{:else if basicGroups.length}
353345
<!-- Curated groups — each group is now a collapsible card -->
354346
{#each basicGroups as group (group.title)}

0 commit comments

Comments
 (0)