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
12 changes: 11 additions & 1 deletion configurator/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,17 @@
if (file.name.endsWith(".json")) {
try {
const data = JSON.parse(text);
if (typeof data === "object") setOverrides(data);
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
// Restrict to real token-name keys too, not just string values — an
// imported JSON file is untrusted input and its keys end up as
// object property names downstream (CodeQL: remote-property-injection).
const safe = Object.fromEntries(
Object.entries(data as Record<string, unknown>).filter(
([k, v]) => typeof v === "string" && /^--sf-[\w-]+$/.test(k)
)
) as Record<string, string>;
if (Object.keys(safe).length > 0) setOverrides(safe);
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch {}
} else {
const parsed: Record<string, string> = {};
Expand Down
2 changes: 1 addition & 1 deletion configurator/src/components/inputs/ColorInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<div class="flex items-center gap-2">
<!-- Swatch / native picker trigger -->
<div class="relative shrink-0 w-7 h-7 rounded border border-white/10 overflow-hidden cursor-pointer">
<div class="absolute inset-0" style={`background: ${swatchColor}`}></div>
<div class="absolute inset-0" style:background={swatchColor}></div>
{#if !isVar}
<input
type="color"
Expand Down
2 changes: 1 addition & 1 deletion configurator/src/components/inputs/OklchColorDesk.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
>
<div
class="w-8 h-8 rounded-lg border border-white/10 shrink-0 shadow-lg"
style={`background: ${swatchColor}`}
style:background={swatchColor}
></div>
<div class="flex-1 text-left min-w-0">
<div class="text-[11px] font-semibold text-slate-200">{label}</div>
Expand Down
2 changes: 1 addition & 1 deletion configurator/src/components/inputs/TokenRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
{#if type === "color"}
<div
class="w-4 h-4 rounded-sm border border-white/10 shrink-0"
style={`background: ${swatchColor}`}
style:background={swatchColor}
></div>
{:else}
<div class="w-4 h-4 shrink-0"></div>
Expand Down
27 changes: 16 additions & 11 deletions configurator/src/components/panels/ColorsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@
return overrides[source.name] ?? sourceTokenMap[source.name]?.value ?? source.default;
}

function sourceByName(name: string): ColorSource | undefined {
return ALL_SOURCES.find((s) => s.name === name);
}

let activeCurvePreset = $derived(CURVE_PRESETS.find((p) =>
Object.entries(p.patch).every(([k, v]) =>
v === null ? !(k in overrides) : overrides[k] === v
Expand Down Expand Up @@ -343,24 +347,25 @@
// Endpoint colors for palette step computation — approximated from source tokens so we
// can build concrete color-mix() expressions without relying on the themed probe.
function getLightSurface(): string {
return overrides["--sf-color-base-source-light"] ?? "oklch(0.96 0.006 250)";
return sourceValue(sourceByName("--sf-color-base-source-light"));
}

function getDarkSurface(): string {
const custom = overrides["--sf-color-base-source-dark"];
if (custom) return custom;
return deriveDarkFromLight(overrides["--sf-color-base-source-light"] ?? "oklch(0.96 0.006 250)", "base");
const darkSource = sourceByName("--sf-color-base-source-dark");
const hasExplicitDark = darkSource && (overrides[darkSource.name] ?? sourceTokenMap[darkSource.name]?.value) !== undefined;
if (hasExplicitDark) return sourceValue(darkSource);
return deriveDarkFromLight(sourceValue(sourceByName("--sf-color-base-source-light")), "base");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function getLightText(): string {
const n = overrides["--sf-color-neutral-source-light"] ?? "oklch(0.52 0.025 260)";
const n = sourceValue(sourceByName("--sf-color-neutral-source-light"));
const { l, c, h, valid } = parseOklch(n);
if (!valid) return "oklch(0.12 0.02 260)";
return stringifyOklch(Math.max(0.05, Math.min(l - 0.38, 0.3)), c * 0.8, h);
}

function getDarkText(): string {
const n = overrides["--sf-color-neutral-source-dark"] ?? "oklch(0.69 0.0225 260)";
const n = sourceValue(sourceByName("--sf-color-neutral-source-dark"));
const { l, c, h, valid } = parseOklch(n);
if (!valid) return "oklch(0.92 0.02 260)";
return stringifyOklch(Math.min(1.0, Math.max(l + 0.22, 0.88)), c * 0.8, h);
Expand Down Expand Up @@ -530,7 +535,7 @@
{@const resolved = paletteSwatch(light.colorKey, lightSrcVal, step, lSurface, lText)}
<div
class="w-5 h-3 rounded-t border-x border-t border-white/10"
style={`background: ${resolved}`}
style:background={resolved}
title={`${light.colorKey}-${step} (light) — ${resolved}`}
></div>
{/each}
Expand All @@ -543,7 +548,7 @@
{@const resolved = paletteSwatch(light.colorKey, darkSrcVal, step, dSurface, dText)}
<div
class="w-5 h-3 rounded-b border-x border-b border-white/10"
style={`background: ${resolved}`}
style:background={resolved}
title={`${light.colorKey}-${step} (dark) — ${resolved}`}
></div>
{/each}
Expand All @@ -570,7 +575,7 @@
<div class="flex items-center gap-1.5 text-[9px] text-slate-600 pl-1">
<span
class="w-3.5 h-3.5 rounded border border-white/10 shrink-0"
style={`background: ${paint(derivedDark, derivedDark)}`}
style:background={paint(derivedDark, derivedDark)}
title={derivedDark}
></span>
Dark: auto-derived ({derivedDark})
Expand Down Expand Up @@ -697,7 +702,7 @@
<div class="flex items-center gap-1.5 text-[9px] text-slate-600 pl-1">
<span
class="w-3.5 h-3.5 rounded border border-white/10 shrink-0"
style={`background: ${paint(derivedDark, derivedDark)}`}
style:background={paint(derivedDark, derivedDark)}
title={derivedDark}
></span>
Dark: auto-derived ({derivedDark})
Expand All @@ -718,7 +723,7 @@
{@const resolved = computePaletteSwatch(srcVal as string, step, sfc as string, txt as string)}
<div
class="w-5 h-3 rounded-sm border border-white/10"
style={`background: ${resolved}`}
style:background={resolved}
title={`${light.colorKey}-${step} (${side}) — ${resolved}`}
></div>
{/each}
Expand Down
7 changes: 4 additions & 3 deletions configurator/src/components/panels/EffectsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@
{/each}
<!-- Scrollbar strip preview -->
<div class="flex items-center gap-2 p-2 rounded-lg bg-white/4 border border-white/8">
<div class="w-16 h-3 rounded-full" style={`background: ${scrollbarTrack || "rgba(255,255,255,0.08)"}`}>
<div class="w-6 h-3 rounded-full" style={`background: ${scrollbarThumb || "oklch(0.52 0.025 260)"}`}></div>
<div class="w-16 h-3 rounded-full" style:background={scrollbarTrack || "rgba(255,255,255,0.08)"}>
<div class="w-6 h-3 rounded-full" style:background={scrollbarThumb || "oklch(0.52 0.025 260)"}></div>
</div>
<span class="text-[9px] text-slate-600">Scrollbar preview</span>
</div>
Expand Down Expand Up @@ -286,7 +286,8 @@
placeholder={t.default}
oninput={(e) => {
const v = (e.target as HTMLInputElement).value;
v.trim() ? onSet(t.token, v) : onReset(t.token);
const trimmed = v.trim();
trimmed ? onSet(t.token, trimmed) : onReset(t.token);
}}
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"
/>
Expand Down
6 changes: 3 additions & 3 deletions configurator/src/components/panels/MotionPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
let scale = $derived((() => { const v = parseFloat(overrides["--sf-motion-scale"] ?? "1"); return isFinite(v) ? v : 1; })());
let motionDisabled = $derived(overrides["--sf-motion-scale"] === "0");
let themeTransition = $derived((() => { const v = parseFloat(overrides["--sf-theme-transition-duration"]?.replace("ms","") ?? String(300 * scale)); return isFinite(v) ? v : Math.round(300 * scale); })());
let staggerBase = $derived(() => {
let staggerBase = $derived.by(() => {
const raw = overrides[STAGGER_TOKENS[0]];
if (raw) return parseFloat(raw.replace("ms",""));
return 75 * scale;
Expand Down Expand Up @@ -292,7 +292,7 @@
{/if}
</div>
<SliderRow
label="" value={Math.round(staggerBase())} min={0} max={200} step={5} unit="ms"
label="" value={Math.round(staggerBase)} min={0} max={200} step={5} unit="ms"
help="Base unit for --sf-animation-delay-1 through -5"
overridden={STAGGER_TOKENS.some(t => t in overrides)}
onChange={(v) => setStaggerBase(v)}
Expand All @@ -301,7 +301,7 @@
</div>
<div class="bg-white/4 rounded-xl border border-white/8 p-3 space-y-1">
{#each [1,2,3,4,5] as n (n)}
{@const delayMs = Math.round(staggerBase() * n)}
{@const delayMs = Math.round(staggerBase * n)}
<div class="flex items-center gap-2">
<span class="text-[9px] font-mono text-slate-600 w-6">–{n}</span>
<div class="flex-1 h-1.5 bg-white/8 rounded-full">
Expand Down
2 changes: 1 addition & 1 deletion configurator/src/components/panels/SpacingPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
{#each SPACE_STEPS as step, i (step)}
{@const midBase = (baseMin + baseMax) / 2}
{@const ratio = (ratioMin + ratioMax) / 2}
{@const offset = i - 4}
{@const offset = i - 3}
{@const rawRem = offset >= 0 ? midBase * Math.pow(ratio, offset) : midBase / Math.pow(ratio, -offset)}
{@const scaled = rawRem * spaceScale}
{@const barWidth = Math.min(scaled * 28, 240)}
Expand Down
2 changes: 1 addition & 1 deletion configurator/src/components/panels/ThemesPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
{#each Object.entries(theme.overrides).slice(0, 5) as [k, v] (k)}
<div class="flex items-center gap-1 bg-white/4 rounded px-1.5 py-0.5">
{#if k.includes("color") || k.includes("source")}
<div class="w-2.5 h-2.5 rounded-full border border-white/10 shrink-0" style={`background: ${v}`}></div>
<div class="w-2.5 h-2.5 rounded-full border border-white/10 shrink-0" style:background={v}></div>
{/if}
<span class="text-[8px] font-mono text-slate-600">{k.replace("--sf-", "")}</span>
</div>
Expand Down
7 changes: 6 additions & 1 deletion configurator/src/lib/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,12 @@ export function da(e: string | null | undefined): string {

export function fa(e: Record<string, string>, t: { mode?: "layer" | "root"; banner?: boolean } = {}): string {
const { mode = "layer", banner = true } = t;
const i = Object.keys(e).sort((a, b) => a.localeCompare(b));
// Keys land here from several untrusted-ish entry points (imported JSON,
// shared URL hash, localStorage, WP hydration) — unlike values, they were
// never sanitized before being interpolated into the emitted CSS, so a
// crafted key could break out of its declaration. Every real token name
// (source or derived) is --sf-<word-chars/hyphens>; anything else is dropped.
const i = Object.keys(e).filter((key) => /^--sf-[\w-]+$/.test(key)).sort((a, b) => a.localeCompare(b));
if (i.length === 0) return "";

const a = i.map((key) => `${key}: ${da(e[key])};`);
Expand Down
3 changes: 2 additions & 1 deletion configurator/src/lib/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ function getNum(ov: Record<string, string>, key: string, def: number): number {
const v = ov[key];
if (v === undefined) return def;
const n = parseFloat(v);
return isNaN(n) ? def : n;
return Number.isFinite(n) ? n : def;
}

function fmt(n: number): string {
if (!Number.isFinite(n)) return '0';
const s = n.toFixed(6);
const trimmed = s.replace(/\.?0+$/, '');
return trimmed === '' || trimmed === '-' ? '0' : trimmed;
Expand Down