diff --git a/configurator/src/components/BrandColorRow.svelte b/configurator/src/components/BrandColorRow.svelte
index e6d0df59..ed948a90 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, setProbeContext } 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(() => {
+ for (const k in overrides) void overrides[k];
+ void ui.previewTheme;
+ queueMicrotask(() => {
+ setProbeContext({ overrides, theme: ui.previewTheme });
+ 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..f4c28a4e
--- /dev/null
+++ b/configurator/src/components/HeadingEditor.svelte
@@ -0,0 +1,203 @@
+
+
+
+
+ {#each TABS as tab (tab.id)}
+ {@const modified = tabHasOverride(tab.id)}
+ (activeTab = tab.id)}
+ >{tab.label}{#if modified} {/if}
+ {/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..2dd16780 100644
--- a/configurator/src/components/Preview.svelte
+++ b/configurator/src/components/Preview.svelte
@@ -46,15 +46,40 @@
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',
+ };
+
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'
@@ -1031,6 +1056,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
+
+
+
+
+ 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)}
+
+ {/each}
+
+
+
+
+ Focus ring — --sf-focus-ring-*
+
+ Primary focused
+ Outline focused
+ Ghost focused
+
+
+ --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
+
+
+
+
+
+
+
+
+ Inner shadow — --sf-shadow-inner
+
+
+
+ {/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)}
+
+ {/each}
+
+
+
+
+ Easing functions — --sf-ease-* (bar sweeps with each easing)
+
+ {#each ['in','out','in-out','spring','bounce'] as e (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)}
+
+ {/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)}
+
+ {/each}
+
+
+
+
+ Scrim / overlay — --sf-scrim · --sf-overlay
+
+
+
+ --sf-scrim overlay
+
+
+
+
+
+
+
+ Frosted glass — backdrop-filter + surface
+
+
+
+
Glass surface
+
backdrop-filter: blur + translucent surface color
+
+
+
+
+
+ {/if}
+
@@ -1248,4 +1598,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..fe3f4bb0
--- /dev/null
+++ b/configurator/src/components/RadiusEditor.svelte
@@ -0,0 +1,193 @@
+
+
+
+
+ {#each TABS as tab (tab.id)}
+ {@const modified = tabHasOverride(tab.id)}
+ (activeTab = tab.id)}
+ >{tab.label}{#if modified} {/if}
+ {/each}
+
+
+
+ {#if activeTab === 'all'}
+
+ {#each LEVELS.filter((l) => exists(l.token)) as lvl (lvl.id)}
+ {@const [w, h] = DEMO_SIZES[lvl.id]}
+
+ {/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]}
+
+
+
+ {#each LEVELS.filter((l) => exists(l.token)) as l (l.id)}
+ {@const [sw, sh] = DEMO_SIZES[l.id]}
+
(activeTab = l.id)}
+ title={l.token}
+ style:width="{Math.min(sw, 44)}px"
+ style:height="{Math.min(sh, 44)}px"
+ style:border-radius="var({l.token})"
+ >
+ {/each}
+
+
+
+
+
+ {/if}
+ {/if}
+
+
+
+
diff --git a/configurator/src/components/ScaleGenerator.svelte b/configurator/src/components/ScaleGenerator.svelte
index b482eec1..a5cd8c07 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, false) : true);
/**
* The framework's display ramp: --sf-text-display-{s,m,l}, a separate modular
@@ -178,6 +185,14 @@
+ {#if collapsible}
+
{ open = !open; setFold(foldKey, open); }}
+ aria-expanded={open}
+ aria-label={open ? 'Collapse scale generator' : 'Expand scale generator'}
+ >›
+ {/if}
Scale generator
{#if kinds.length > 1}
@@ -195,6 +210,7 @@
+ {#if !collapsible || open}
@@ -291,6 +307,7 @@
{/each}
+ {/if}