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
104 changes: 104 additions & 0 deletions configurator/src/components/inputs/ColorInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<script lang="ts">
import { resolveColor, previewVersion } from '../../lib/previewResolver.svelte';

let {
token,
value,
placeholder,
isOverridden,
onSet,
onReset,
}: {
token: string;
value: string;
placeholder?: string;
isOverridden: boolean;
onSet: (v: string) => void;
onReset: () => void;
} = $props();

let editing = $state(false);
let cancelBlur = $state(false);

// Bare "--token" is a UI shorthand; normalize to "var(--token)" before resolving or storing.
function normalize(v: string): string {
const t = v.trim();
return t.startsWith("--") && !t.startsWith("var(") ? `var(${t})` : t;
}

function paint(expr: string): string {
void previewVersion.value;
const norm = normalize(expr);
return resolveColor(norm) || norm || "transparent";
}

let swatchColor = $derived(paint(value || `var(${token})`));

// Detect if the current value is a CSS variable reference (can't use native picker)
let isVar = $derived(value.trim().startsWith("var(") || value.trim().startsWith("--"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Convert a resolved rgb(...) or a raw hex string to a #RRGGBB seed for the native picker.
function toHex(color: string): string {
// Pass through hex colors directly (3- or 6-digit)
const hex6 = color.match(/^#([0-9a-fA-F]{6})$/);
if (hex6) return color.toLowerCase();
const hex3 = color.match(/^#([0-9a-fA-F]{3})$/);
if (hex3) return "#" + hex3[1].split("").map(c => c + c).join("");
// Parse computed rgb()/rgba() strings from resolveColor
const m = color.match(/rgb\w*\((\d+)[,\s]+(\d+)[,\s]+(\d+)/);
if (m) return "#" + [m[1], m[2], m[3]].map(n => parseInt(n).toString(16).padStart(2, "0")).join("");
return "#6366f1";
}
</script>

<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>
{#if !isVar}
<input
type="color"
value={toHex(swatchColor)}
oninput={(e) => onSet((e.target as HTMLInputElement).value)}
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
tabindex="-1"
/>
{/if}
</div>

<!-- Text display / editable input -->
{#if editing}
<input
value={value}
autofocus
onblur={(e) => {
if (cancelBlur) { cancelBlur = false; editing = false; return; }
const v = normalize((e.target as HTMLInputElement).value);
if (!v) onReset();
else onSet(v);
editing = false;
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}}
onkeydown={(e) => {
if (e.key === "Enter") (e.currentTarget as HTMLInputElement).blur();
if (e.key === "Escape") { cancelBlur = true; editing = false; }
}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
placeholder={placeholder ?? "default"}
class="flex-1 bg-white/8 border border-indigo-500/50 rounded px-1.5 py-0.5 text-[10px] font-mono text-slate-200 focus:outline-none"
/>
{:else}
<button
onclick={() => { editing = true; }}
class="flex-1 text-left text-[9px] font-mono text-slate-500 hover:text-slate-200 truncate cursor-pointer transition-colors"
>
{#if isOverridden}
<span class="text-indigo-300">{value}</span>
{:else}
{placeholder ?? "default"}
{/if}
</button>
{/if}

{#if isOverridden}
<button onclick={onReset} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer shrink-0">reset</button>
{/if}
</div>
10 changes: 9 additions & 1 deletion configurator/src/components/inputs/TokenRow.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import type { SlashedToken } from '../../types';
import { resolveColor, previewVersion } from '../../lib/previewResolver.svelte';

let { token, overrideValue, onSet, onReset }: {
token: SlashedToken;
Expand All @@ -23,6 +24,13 @@
let isOverridden = $derived(overrideValue !== undefined);
let type = $derived(guessType(token));
let shortName = $derived(token.name.replace("--sf-", ""));

function paintSwatch(expr: string): string {
void previewVersion.value;
return resolveColor(expr) || resolveColor(`var(${token.name})`) || expr;
}
// Only subscribe to previewVersion for color tokens to avoid unnecessary recomputation.
let swatchColor = $derived(type === "color" ? paintSwatch(displayValue) : "");
</script>

<div class={`flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors group ${isOverridden ? "bg-indigo-500/8 border border-indigo-500/15" : "hover:bg-white/4"}`}>
Expand All @@ -31,7 +39,7 @@
{#if type === "color"}
<div
class="w-4 h-4 rounded-sm border border-white/10 shrink-0"
style={`background: ${displayValue}`}
style={`background: ${swatchColor}`}
></div>
{:else}
<div class="w-4 h-4 shrink-0"></div>
Expand Down
53 changes: 23 additions & 30 deletions configurator/src/components/panels/BordersPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import PowerKnobRow from '../inputs/PowerKnobRow.svelte';
import StylePresetCards from '../inputs/StylePresetCards.svelte';
import SliderRow from '../inputs/SliderRow.svelte';
import ColorInput from '../inputs/ColorInput.svelte';

let { overrides, onSet, onReset, onBulkChange }: {
overrides: Record<string, string>;
Expand Down Expand Up @@ -103,18 +104,14 @@
<span class="text-[10px] text-slate-500">{showBorderColor ? "▲" : "▼"}</span>
</button>
{#if showBorderColor}
<div class="flex items-center gap-2">
<input
type="color"
value={borderColor || "#e2e8f0"}
oninput={(e) => onSet("--sf-color-border", (e.target as HTMLInputElement).value)}
class="w-8 h-8 rounded border border-white/10 bg-transparent cursor-pointer"
/>
<span class="text-[9px] font-mono text-slate-400 flex-1">{borderColor || "auto-derived"}</span>
{#if "--sf-color-border" in overrides}
<button onclick={() => onReset("--sf-color-border")} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer">reset</button>
{/if}
</div>
<ColorInput
token="--sf-color-border"
value={borderColor}
placeholder="auto-derived"
isOverridden={"--sf-color-border" in overrides}
onSet={(v) => onSet("--sf-color-border", v)}
onReset={() => onReset("--sf-color-border")}
/>
<p class="text-[9px] text-slate-600">
Drives --sf-color-border--strong, --subtle, --translucent automatically.
</p>
Expand Down Expand Up @@ -242,16 +239,14 @@
{#if showDividers}
<div class="flex items-center gap-2">
<div class="text-[10px] font-semibold text-slate-400 w-24 shrink-0">Color</div>
<input
type="color"
value={dividerColor || "#e2e8f0"}
oninput={(e) => onSet("--sf-divider-color", (e.target as HTMLInputElement).value)}
class="w-7 h-7 rounded border border-white/10 bg-transparent cursor-pointer"
<ColorInput
token="--sf-divider-color"
value={dividerColor}
placeholder="inherits border"
isOverridden={"--sf-divider-color" in overrides}
onSet={(v) => onSet("--sf-divider-color", v)}
onReset={() => onReset("--sf-divider-color")}
/>
<span class="text-[9px] font-mono text-slate-500 flex-1 truncate">{dividerColor || "inherits border"}</span>
{#if "--sf-divider-color" in overrides}
<button onclick={() => onReset("--sf-divider-color")} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer">reset</button>
{/if}
</div>
<SliderRow
label="Width" value={dividerWidth} min={0.5} max={4} step={0.5} unit="px"
Expand Down Expand Up @@ -299,16 +294,14 @@
/>
<div class="flex items-center gap-2">
<div class="text-[10px] font-semibold text-slate-400 w-24 shrink-0">Ring color</div>
<input
type="color"
value={focusRingColor || "#6366f1"}
oninput={(e) => onSet("--sf-focus-ring-color", (e.target as HTMLInputElement).value)}
class="w-7 h-7 rounded border border-white/10 bg-transparent cursor-pointer"
<ColorInput
token="--sf-focus-ring-color"
value={focusRingColor}
placeholder="default (action)"
isOverridden={"--sf-focus-ring-color" in overrides}
onSet={(v) => onSet("--sf-focus-ring-color", v)}
onReset={() => onReset("--sf-focus-ring-color")}
/>
<span class="text-[9px] font-mono text-slate-500 flex-1 truncate">{focusRingColor || "default (action)"}</span>
{#if "--sf-focus-ring-color" in overrides}
<button onclick={() => onReset("--sf-focus-ring-color")} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer">reset</button>
{/if}
</div>
<!-- Focus ring preview -->
<div class="bg-white/4 rounded-xl border border-white/8 p-4 flex items-center justify-center">
Expand Down
26 changes: 9 additions & 17 deletions configurator/src/components/panels/ColorsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import OklchColorDesk from '../inputs/OklchColorDesk.svelte';
import PowerKnobRow from '../inputs/PowerKnobRow.svelte';
import SliderRow from '../inputs/SliderRow.svelte';
import ColorInput from '../inputs/ColorInput.svelte';

let { tokens, overrides, onSet, onReset, onBulkChange, onSelectDomain }: {
tokens: SlashedToken[];
Expand Down Expand Up @@ -956,23 +957,14 @@
{@const resolved = paint(`var(${s.name})`, "")}
<div>
<div class="text-[10px] font-semibold text-slate-400 mb-1">{s.label}</div>
<div class="flex items-center gap-2">
<span class="relative w-7 h-7 rounded border border-white/10 shrink-0 overflow-hidden" style={`background:${(s.name in overrides ? "" : resolved) || "transparent"}`}>
<input
type="color"
value={overrides[s.name] ?? "#6366f1"}
oninput={(e) => onSet(s.name, (e.target as HTMLInputElement).value)}
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
{#if s.name in overrides}
<span class="absolute inset-0" style={`background:${overrides[s.name]}`}></span>
{/if}
</span>
<span class="text-[9px] font-mono text-slate-500 flex-1 truncate">{overrides[s.name] ?? (resolved ? `auto-derived · ${resolved}` : "auto-derived")}</span>
{#if s.name in overrides}
<button onclick={() => onReset(s.name)} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer shrink-0">reset</button>
{/if}
</div>
<ColorInput
token={s.name}
value={overrides[s.name] ?? ""}
placeholder={resolved ? `auto-derived · ${resolved}` : "auto-derived"}
isOverridden={s.name in overrides}
onSet={(v) => onSet(s.name, v)}
onReset={() => onReset(s.name)}
/>
</div>
{/each}
</div>
Expand Down
37 changes: 17 additions & 20 deletions configurator/src/components/panels/EffectsPanel.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import SliderRow from '../inputs/SliderRow.svelte';
import ColorInput from '../inputs/ColorInput.svelte';

let { overrides, onSet, onReset }: {
overrides: Record<string, string>;
Expand Down Expand Up @@ -152,16 +153,14 @@
<!-- Scrim color -->
<div class="flex items-center gap-2">
<div class="text-[10px] font-semibold text-slate-400 w-16 shrink-0">Color</div>
<input
type="color"
value={scrimColor || "#000000"}
oninput={(e) => onSet("--sf-scrim-color", (e.target as HTMLInputElement).value)}
class="w-7 h-7 rounded border border-white/10 bg-transparent cursor-pointer"
<ColorInput
token="--sf-scrim-color"
value={scrimColor}
placeholder="default (base color)"
isOverridden={"--sf-scrim-color" in overrides}
onSet={(v) => onSet("--sf-scrim-color", v)}
onReset={() => onReset("--sf-scrim-color")}
/>
<span class="text-[9px] font-mono text-slate-500 flex-1 truncate">{scrimColor || "default (base color)"}</span>
{#if "--sf-scrim-color" in overrides}
<button onclick={() => onReset("--sf-scrim-color")} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer">reset</button>
{/if}
</div>

<div>
Expand Down Expand Up @@ -212,21 +211,19 @@
</button>
{#if showScrollbar}
{#each [
{ label: "Thumb color", token: "--sf-scrollbar-thumb", val: scrollbarThumb, default: "#6366f1" },
{ label: "Track color", token: "--sf-scrollbar-track", val: scrollbarTrack, default: "transparent" },
{ label: "Thumb color", token: "--sf-scrollbar-thumb", val: scrollbarThumb },
{ label: "Track color", token: "--sf-scrollbar-track", val: scrollbarTrack },
] as row (row.token)}
<div class="flex items-center gap-2">
<div class="text-[10px] font-semibold text-slate-400 w-20 shrink-0">{row.label}</div>
<input
type="color"
value={row.val || (row.default === "transparent" ? "#000000" : row.default)}
oninput={(e) => onSet(row.token, (e.target as HTMLInputElement).value)}
class="w-7 h-7 rounded border border-white/10 bg-transparent cursor-pointer"
<ColorInput
token={row.token}
value={row.val}
placeholder="default"
isOverridden={row.token in overrides}
onSet={(v) => onSet(row.token, v)}
onReset={() => onReset(row.token)}
/>
<span class="text-[9px] font-mono text-slate-500 flex-1 truncate">{row.val || "default"}</span>
{#if row.token in overrides}
<button onclick={() => onReset(row.token)} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer">reset</button>
{/if}
</div>
{/each}
<!-- Scrollbar strip preview -->
Expand Down
Loading
Loading