diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index 03109463..f92482a7 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -7,6 +7,7 @@ import PreviewPanel from './components/shell/PreviewPanel.svelte'; import DomainPanel from './components/DomainPanel.svelte'; import { Ja, Ga, fa } from './lib/codec'; + import { domainOf } from './lib/domains'; import tokensRaw from './data/api-index.generated.json'; import CommandPalette from './components/CommandPalette.svelte'; @@ -15,8 +16,8 @@ const DOMAIN_LABELS: Record = { home: "Home", colors: "Colors", typography: "Typography", spacing: "Spacing", layout: "Layout", borders: "Borders", shadows: "Shadows", motion: "Motion", - effects: "Effects", misc: "Misc", themes: "Themes", wcag: "WCAG", - setup: "Install", cheatsheet: "Classes", + effects: "Effects", macros: "Macros", misc: "Misc", themes: "Themes", + wcag: "WCAG", setup: "Install", cheatsheet: "Classes", }; function initOverrides(): Record { @@ -35,16 +36,7 @@ function overridesByDomain(ov: Record): Record { const map: Record = {}; for (const k of Object.keys(ov)) { - const dom = - k.includes("color") ? "colors" : - k.includes("font") || k.includes("text") || k.includes("leading") ? "typography" : - k.includes("space") || k.includes("section") ? "spacing" : - k.includes("layout") || k.includes("container") || k.includes("grid") ? "layout" : - k.includes("radius") || k.includes("border") ? "borders" : - k.includes("shadow") ? "shadows" : - k.includes("motion") || k.includes("duration") || k.includes("easing") ? "motion" : - k.includes("blur") || k.includes("filter") || k.includes("opacity") ? "effects" : - "misc"; + const dom = domainOf(k); map[dom] = (map[dom] ?? 0) + 1; } return map; diff --git a/configurator/src/components/CommandPalette.svelte b/configurator/src/components/CommandPalette.svelte index 2fe8838e..96967d9d 100644 --- a/configurator/src/components/CommandPalette.svelte +++ b/configurator/src/components/CommandPalette.svelte @@ -14,14 +14,16 @@ let inputEl = $state(null); const DOMAIN_MAP: Record = { - colors: "colors", colour: "colors", color: "colors", + macros: "macros", prose: "macros", "flow-space": "macros", "line-clamp": "macros", + aspect: "macros", "scroll-shadow": "macros", scrim: "macros", "surface-color": "macros", + colors: "colors", colour: "colors", color: "colors", gradient: "colors", lumlocker: "colors", "palette-mix": "colors", contrast: "colors", typography: "typography", font: "typography", text: "typography", leading: "typography", tracking: "typography", spacing: "spacing", space: "spacing", gap: "spacing", section: "spacing", layout: "layout", container: "layout", grid: "layout", center: "layout", sidebar: "layout", header: "layout", bento: "layout", sticky: "layout", borders: "borders", border: "borders", radius: "borders", focus: "borders", divider: "borders", ring: "borders", shadows: "shadows", shadow: "shadows", elevation: "shadows", glow: "shadows", motion: "motion", duration: "motion", ease: "motion", easing: "motion", animation: "motion", delay: "motion", transition: "motion", - effects: "effects", blur: "effects", opacity: "effects", scrim: "effects", scrollbar: "effects", scroll: "effects", + effects: "effects", blur: "effects", opacity: "effects", scrollbar: "effects", scroll: "effects", misc: "misc", }; @@ -35,7 +37,8 @@ const DOMAIN_LABELS: Record = { colors: "Colors", typography: "Typography", spacing: "Spacing", layout: "Layout", - borders: "Borders", shadows: "Shadows", motion: "Motion", effects: "Effects", misc: "Misc", + borders: "Borders", shadows: "Shadows", motion: "Motion", effects: "Effects", + macros: "Macros", misc: "Misc", }; let results = $derived((() => { diff --git a/configurator/src/components/DomainPanel.svelte b/configurator/src/components/DomainPanel.svelte index 71ccedaa..c2bdd2cf 100644 --- a/configurator/src/components/DomainPanel.svelte +++ b/configurator/src/components/DomainPanel.svelte @@ -11,6 +11,7 @@ import ShadowsPanel from './panels/ShadowsPanel.svelte'; import MotionPanel from './panels/MotionPanel.svelte'; import EffectsPanel from './panels/EffectsPanel.svelte'; + import MacrosPanel from './panels/MacrosPanel.svelte'; import MiscPanel from './panels/MiscPanel.svelte'; import ThemesPanel from './panels/ThemesPanel.svelte'; import ExportPanel from './panels/ExportPanel.svelte'; @@ -98,6 +99,8 @@ {:else if domain === "effects"} + {:else if domain === "macros"} + {:else if domain === "misc"} {:else} diff --git a/configurator/src/components/inputs/ClampField.svelte b/configurator/src/components/inputs/ClampField.svelte index 21f40681..dfab4656 100644 --- a/configurator/src/components/inputs/ClampField.svelte +++ b/configurator/src/components/inputs/ClampField.svelte @@ -16,6 +16,8 @@ max, step, unit = "rem", + minLabel = "Min", + maxLabel = "Max", onMinChange, onMaxChange, overridden = false, @@ -40,6 +42,8 @@ max: number; step: number; unit?: string; + minLabel?: string; + maxLabel?: string; onMinChange: (v: number) => void; onMaxChange: (v: number) => void; overridden?: boolean; @@ -96,13 +100,13 @@
{/each} @@ -471,21 +635,39 @@ - +
-
Curve shape preview
-
+
+ Palette & curve preview +
+ {#each ["light", "dark"] as side (side)} + + {/each} +
+
+
{#each MIX_STEPS as s (s.name)} {@const val = getMixValue(s)} -
+ {@const step = s.label} + {@const swatch = paintTheme(`var(--sf-color-primary-${step})`, curvePreviewSide, `var(--sf-color-primary-${step})`)} +
+ {Math.round(val)}
- {s.label} + {step}
{/each}
+

Bar height = mix amount · fill = the resolved primary palette swatch at each step

@@ -502,30 +684,81 @@ {#if showGradients}

- Auto-derived from your brand colors. Edit the raw value to fully customise. + Auto-derived from your brand colors. Use the angle/direction and stop + controls, or edit the raw value for full control.

{#each ["brand", "fade"] as grp (grp)}
{grp === "brand" ? "Brand & surface" : "Edge fades"}
{#each GRADIENT_TOKENS.filter((g) => g.group === grp) as g (g.name)} {@const expr = `var(${g.name})`} + {@const e = gradEdit(g)}
-
- {g.label} +
+
+ {g.label} + {#if g.name in overrides} + + {/if} +
+ + + {#if g.kind === "dir"} +
+ Dir + {#each ["top", "right", "bottom", "left"] as d (d)} + + {/each} +
+ {:else} +
+ Angle + setGradientPart(g, "angle", parseInt((ev.target as HTMLInputElement).value))} + class="flex-1 accent-indigo-500" + /> + {e.angle}° +
+ {/if} + +
+ Stops + setGradientPart(g, "stop1", (ev.target as HTMLInputElement).value.trim())} + class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded px-1.5 py-0.5 text-[8px] font-mono text-slate-300 focus:outline-none focus:border-indigo-500" + /> + setGradientPart(g, "stop2", (ev.target as HTMLInputElement).value.trim())} + class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded px-1.5 py-0.5 text-[8px] font-mono text-slate-300 focus:outline-none focus:border-indigo-500" + /> +
+ + +
+ default: {g.formula} +
+ + { - const v = (e.target as HTMLInputElement).value.trim(); + placeholder="raw override (auto-derived if blank)" + onchange={(ev) => { + const v = (ev.target as HTMLInputElement).value.trim(); + delete gradientEdits[g.name]; v ? onSet(g.name, v) : onReset(g.name); }} - class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded px-1.5 py-0.5 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500" + class="w-full bg-white/5 border border-white/10 rounded px-1.5 py-0.5 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500" /> - {#if g.name in overrides} - - {/if}
{/each} diff --git a/configurator/src/components/panels/LayoutPanel.svelte b/configurator/src/components/panels/LayoutPanel.svelte index 0dbff2a7..481352e5 100644 --- a/configurator/src/components/panels/LayoutPanel.svelte +++ b/configurator/src/components/panels/LayoutPanel.svelte @@ -36,12 +36,34 @@ ]; let showBento = $state(false); + let showMore = $state(false); function getBentoVal(t: typeof BENTO_TOKENS[0]): number { const raw = overrides[t.token]; if (!raw) return t.default; return parseFloat(raw); } + + // Additional layout primitives (switcher, sidebar, equal, cover, frame, reel, + // imposter, content-grid, alternate) — tokens exist but had no UI. + let switcherThreshold = $derived(parseRem(overrides["--sf-switcher-threshold"], 30)); + let sidebarMinWidth = $derived(parseRem(overrides["--sf-sidebar-min-width"], 50)); + let equalMinCol = $derived(parseRem(overrides["--sf-equal-min-col"], 16)); + let coverMinHeight = $derived(parseRem(overrides["--sf-cover-min-height"], 100)); + let imposterMargin = $derived(parseRem(overrides["--sf-imposter-margin"], 1)); + let breakoutWidth = $derived(parseRem(overrides["--sf-breakout-width"], 90)); + let contentWidth = $derived(parseRem(overrides["--sf-content-width"], 75)); + let alternateInnerGap = $derived(parseRem(overrides["--sf-alternate-inner-gap"], 1)); + let frameRatio = $derived(overrides["--sf-frame-ratio"] ?? "16 / 9"); + let reelItemWidth = $derived(overrides["--sf-reel-item-width"] ?? "max-content"); + + const FRAME_PRESETS = ["16 / 9", "4 / 3", "3 / 2", "1 / 1", "21 / 9"]; + const REEL_PRESETS = [ + { label: "auto", value: "max-content" }, + { label: "12rem", value: "12rem" }, + { label: "16rem", value: "16rem" }, + { label: "20rem", value: "20rem" }, + ];
@@ -275,4 +297,150 @@
{/if}
+ +
+ + +
+ + {#if showMore} +
+ + +
+
Switcher
+ onSet("--sf-switcher-threshold", `${v}rem`)} + onReset={() => onReset("--sf-switcher-threshold")} + /> +
+ + +
+
Sidebar content
+ onSet("--sf-sidebar-min-width", `${v}%`)} + onReset={() => onReset("--sf-sidebar-min-width")} + /> +
+ + +
+
Equal grid
+ onSet("--sf-equal-min-col", `${v}rem`)} + onReset={() => onReset("--sf-equal-min-col")} + /> +
+ + +
+
Cover
+ onSet("--sf-cover-min-height", `${v}dvh`)} + onReset={() => onReset("--sf-cover-min-height")} + /> +
+ + +
+
Frame ratio
+

--sf-frame-ratio — aspect ratio for .sf-frame

+
+ {#each FRAME_PRESETS as r (r)} + + {/each} +
+
+ + +
+
Reel
+

--sf-reel-item-width — width of each item in a horizontal .sf-reel

+
+ {#each REEL_PRESETS as p (p.value)} + + {/each} +
+
+ + +
+
Imposter
+ onSet("--sf-imposter-margin", `${v}rem`)} + onReset={() => onReset("--sf-imposter-margin")} + /> +
+ + +
+
Content grid (breakout)
+ onSet("--sf-content-width", `${v}rem`)} + onReset={() => onReset("--sf-content-width")} + /> + onSet("--sf-breakout-width", `${v}rem`)} + onReset={() => onReset("--sf-breakout-width")} + /> +
+ + +
+
Alternate (zigzag)
+ onSet("--sf-alternate-inner-gap", `${v}rem`)} + onReset={() => onReset("--sf-alternate-inner-gap")} + /> +
+
+ {/if} +
diff --git a/configurator/src/components/panels/MacrosPanel.svelte b/configurator/src/components/panels/MacrosPanel.svelte new file mode 100644 index 00000000..9a18737e --- /dev/null +++ b/configurator/src/components/panels/MacrosPanel.svelte @@ -0,0 +1,261 @@ + + +
+ +

+ Tokens for the framework's utility macros — .sf-prose, + .sf-flow, .sf-line-clamp, + .sf-aspect, .sf-scroll-shadow + and .sf-scrim. +

+ + +
+
Flow rhythm
+ onSet("--sf-flow-space", `${v}rem`)} + onReset={() => onReset("--sf-flow-space")} + /> +
+ {#each [0, 1, 2] as i (i)} +
+ {/each} +
+
+ +
+ + +
+
Line clamp
+ onSet("--sf-line-clamp", String(v))} + onReset={() => onReset("--sf-line-clamp")} + /> +
+

+ SLASHED is a token-first CSS framework. This sample paragraph repeats so you + can see exactly how many lines survive the clamp before the ellipsis kicks in. + Drag the slider above and watch the visible line count change in real time. + Extra filler text keeps the block taller than the clamp at every setting. +

+
+
+ +
+ + +
+
Aspect ratio
+

--sf-aspect — default ratio for .sf-aspect / .sf-frame

+
+ {#each ASPECT_PRESETS as p (p.value)} + + {/each} +
+
+
{aspectVal}
+
+
+ +
+ + +
+
Scroll shadow
+ onSet("--sf-scroll-shadow-size", `${v}rem`)} + onReset={() => onReset("--sf-scroll-shadow-size")} + /> +
+
+
+
+ +
+ + +
+
Scrim overlay
+

--sf-scrim-color / --sf-scrim-direction — darkening gradient for .sf-scrim

+
+
Color
+ { + const v = (e.target as HTMLInputElement).value.trim(); + v ? onSet("--sf-scrim-color", v) : onReset("--sf-scrim-color"); + }} + class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded px-1.5 py-1 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500" + /> + {#if "--sf-scrim-color" in overrides} + + {/if} +
+
+
Direction
+
+ {#each SCRIM_DIRECTIONS as d (d.value)} + + {/each} +
+
+
+
+ Caption over scrim +
+
+ +
+ + +
+ + {#if showProse} +

Spacing tokens for .sf-prose. Defaults track the space scale; sliders write rem overrides.

+
+ {#each PROSE_SPACE as t (t.token)} + onSet(t.token, `${v}rem`)} + onReset={() => onReset(t.token)} + /> + {/each} + onSet("--sf-prose-media-radius", `${v}px`)} + onReset={() => onReset("--sf-prose-media-radius")} + /> +
+
Marker color
+ { + const v = (e.target as HTMLInputElement).value.trim(); + v ? onSet("--sf-prose-marker-color", v) : onReset("--sf-prose-marker-color"); + }} + class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded px-1.5 py-1 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500" + /> + {#if "--sf-prose-marker-color" in overrides} + + {/if} +
+
+ {/if} +
+ +
+

+ All tokens tab — edit every prose, scrim and surface token directly. +

+
+
diff --git a/configurator/src/components/panels/MiscPanel.svelte b/configurator/src/components/panels/MiscPanel.svelte index 6ce3ab4c..9c73a713 100644 --- a/configurator/src/components/panels/MiscPanel.svelte +++ b/configurator/src/components/panels/MiscPanel.svelte @@ -43,6 +43,7 @@ let caretColor = $derived(overrides["--sf-caret-color"] ?? ""); let underlineOffset = $derived(parseNum(overrides["--sf-link-underline-offset"]?.replace("em",""), 0.15)); let underlineThickness = $derived(overrides["--sf-link-underline-thickness"] ?? "auto"); + let focusRingStyle = $derived(overrides["--sf-focus-ring-style"] ?? "solid"); function getSizeValue(t: typeof SIZE_TOKENS[0]): number { return parseNum(overrides[t.token]?.replace("rem",""), t.default); @@ -188,6 +189,15 @@ {/each}
+ +
+
+ Focus ring · {focusRingStyle} +
+
@@ -272,5 +282,34 @@ {/each}
+ + + +
+ + +
+

+ Selection colors, caret, links, focus ring, borders and sizes all render in the + live preview (try the Components template). + Scroll behavior applies to the page + itself and can't be shown in the static canvas. +

+
diff --git a/configurator/src/components/panels/SpacingPanel.svelte b/configurator/src/components/panels/SpacingPanel.svelte index 87e5c9ca..1abad417 100644 --- a/configurator/src/components/panels/SpacingPanel.svelte +++ b/configurator/src/components/panels/SpacingPanel.svelte @@ -42,7 +42,11 @@ let baseMax = $derived(num("--sf-space-base-max", 2)); let spaceScale = $derived(num("--sf-space-scale", 1)); let gapVal = $derived(num("--sf-gap", 1)); + let gutterVal = $derived(num("--sf-gutter", 1.5)); let contentGapVal = $derived(num("--sf-content-gap", 0.5)); + // Shared fluid viewport endpoints (rem, unitless). Min = mobile, max = desktop. + let vwMin = $derived(num("--sf-fluid-min-vw", 22.5)); + let vwMax = $derived(num("--sf-fluid-max-vw", 90)); let activeRatio = $derived(RATIO_PRESETS.find( (p) => Math.abs(p.value - ratioMin) < 0.0015 && Math.abs(p.value - ratioMax) < 0.0015 @@ -77,6 +81,13 @@ onChange={(v) => onSet("--sf-content-gap", `${v}rem`)} onReset={() => onReset("--sf-content-gap")} /> + onSet("--sf-gutter", `${v}rem`)} + onReset={() => onReset("--sf-gutter")} + />
@@ -105,12 +116,28 @@
-
Fluid scale
+
Modular scale (Mobile → Desktop)
+

+ Utopia-style fluid scale — viewport width, base unit and ratio per endpoint. + The viewport range is shared with the type scale. +

+ + { onReset("--sf-fluid-min-vw"); onReset("--sf-fluid-max-vw"); }} + onMinChange={(v) => onSet("--sf-fluid-min-vw", String(v))} + onMaxChange={(v) => onSet("--sf-fluid-max-vw", String(v))} + /> { onReset("--sf-space-base-min"); onReset("--sf-space-base-max"); }} diff --git a/configurator/src/components/panels/TypographyPanel.svelte b/configurator/src/components/panels/TypographyPanel.svelte index 62d58ba5..908220ef 100644 --- a/configurator/src/components/panels/TypographyPanel.svelte +++ b/configurator/src/components/panels/TypographyPanel.svelte @@ -151,6 +151,9 @@ let dispMax = $derived(num("--sf-text-display-base-max", 3)); let taper = $derived(num("--sf-leading-taper", 0)); let textScale = $derived(num("--sf-text-scale", 1)); + // Shared fluid viewport endpoints (rem, unitless). Min = mobile, max = desktop. + let vwMin = $derived(num("--sf-fluid-min-vw", 22.5)); + let vwMax = $derived(num("--sf-fluid-max-vw", 90)); let activeRatio = $derived(RATIO_PRESETS.find( (p) => Math.abs(p.value - ratioMin) < 0.0015 && Math.abs(p.value - ratioMax) < 0.0015 @@ -562,12 +565,29 @@
-
Fine-tune
+
Modular scale (Mobile → Desktop)
+

+ Utopia-style fluid scale. Each endpoint has three values — viewport width, + base size, and modular ratio. Sizes interpolate fluidly between mobile and desktop. +

+ { onReset("--sf-fluid-min-vw"); onReset("--sf-fluid-max-vw"); }} + onMinChange={(v) => onSet("--sf-fluid-min-vw", String(v))} + onMaxChange={(v) => onSet("--sf-fluid-max-vw", String(v))} + /> + + { onReset("--sf-text-base-min"); onReset("--sf-text-base-max"); }} diff --git a/configurator/src/components/shell/PreviewPanel.svelte b/configurator/src/components/shell/PreviewPanel.svelte index eb2bebbc..7f2f94ba 100644 --- a/configurator/src/components/shell/PreviewPanel.svelte +++ b/configurator/src/components/shell/PreviewPanel.svelte @@ -3,6 +3,7 @@ import type { PreviewTemplate } from '../../types'; import { fa } from '../../lib/codec'; import { registerPreviewDoc, bumpPreviewVersion } from '../../lib/previewResolver.svelte'; + import { lumlockerPreview } from '../../lib/lumlockerPreview.svelte'; // Import the built framework CSS at Vite compile time — always in sync with badges/. import frameworkCSSStatic from '../../../../badges/slashed.full.css?raw'; @@ -75,7 +76,7 @@ } const MARKETING_BODY = ` -
+
SlashedUI
-
+
🎨

OKLCH Colors

Perceptually uniform color ramps with auto dark-mode derivation.

-
+
📐

Fluid Scales

Type and space that scales smoothly from mobile to 4K.

-
+

Zero JS

Pure CSS custom properties — works with any framework.

@@ -128,7 +129,7 @@ const DOCS_BODY = `
-
`; const DASHBOARD_BODY = `
-
@@ -295,7 +307,7 @@
Typography
-
+
The quick brown fox
jumps over the lazy dog
Display S — heading family
@@ -361,7 +373,7 @@
-
+
🎨
Feature card
Design tokens that adapt to your brand and context automatically.
@@ -372,9 +384,9 @@
↑ 8.4% this week
-
+
- +
@@ -400,9 +412,9 @@
.sf-grid
-
Grid item 1
-
Grid item 2
-
Grid item 3
+
Grid item 1
+
Grid item 2
+
Grid item 3
@@ -415,7 +427,7 @@ items
-
+
Main content area — the sidebar holds its width while this region fills the rest, collapsing to a single column when space runs out.
@@ -424,7 +436,7 @@
Spacing scale
-
+
3xs
2xs
xs
@@ -527,6 +539,7 @@ ${BODIES[template]} const _ov = overrides; const _theme = previewTheme; const _count = loadCount; + const _lock = lumlockerPreview.value; const iframe = iframeEl; if (_count === 0 || !iframe) return; @@ -544,6 +557,10 @@ ${BODIES[template]} // light-dark()), NOT a class — keep this in sync with buildIframeHTML. doc.documentElement.setAttribute("data-theme", _theme); + // Luminance lock preview — mirrors :root[data-lumlocker] in core/themes.css. + if (_lock) doc.documentElement.setAttribute("data-lumlocker", ""); + else doc.documentElement.removeAttribute("data-lumlocker"); + // This single-mode iframe is the canonical resolver source. registerPreviewDoc(doc); bumpPreviewVersion(); @@ -553,14 +570,21 @@ ${BODIES[template]} const _ov = overrides; const _lightCount = splitLightLoadCount; const _darkCount = splitDarkLoadCount; + const _lock = lumlockerPreview.value; const css = fa(_ov, { mode: "root", banner: false }); + const applyLock = (doc: Document) => { + if (_lock) doc.documentElement.setAttribute("data-lumlocker", ""); + else doc.documentElement.removeAttribute("data-lumlocker"); + }; + if (splitLightEl && _lightCount > 0) { const doc = splitLightEl.contentDocument; if (doc) { const styleEl = doc.getElementById("slashed-overrides"); if (styleEl) styleEl.textContent = css; injectFontsIntoDoc(doc, _ov); + applyLock(doc); // In split mode the light pane is the canonical resolver source. registerPreviewDoc(doc); } @@ -571,6 +595,7 @@ ${BODIES[template]} const styleEl = doc.getElementById("slashed-overrides"); if (styleEl) styleEl.textContent = css; injectFontsIntoDoc(doc, _ov); + applyLock(doc); } } bumpPreviewVersion(); diff --git a/configurator/src/components/shell/SidebarNav.svelte b/configurator/src/components/shell/SidebarNav.svelte index ae74f744..2cea2cb1 100644 --- a/configurator/src/components/shell/SidebarNav.svelte +++ b/configurator/src/components/shell/SidebarNav.svelte @@ -1,7 +1,7 @@