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
62 changes: 45 additions & 17 deletions configurator/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import type { PreviewTemplate, PresetTheme, SlashedToken } from './types';
import { SlidersHorizontal, Eye } from 'lucide-svelte';
import type { PreviewTemplate, SlashedToken } from './types';
import StudioHeader from './components/shell/StudioHeader.svelte';
import SidebarNav from './components/shell/SidebarNav.svelte';
import StatusBar from './components/shell/StatusBar.svelte';
Expand Down Expand Up @@ -37,6 +38,9 @@

let domain = $state("home");
let showPalette = $state(false);
// On narrow screens the controls panel and the live preview can't both fit, so
// we show one at a time and let the user fold between them (desktop shows both).
let mobileView = $state<"controls" | "preview">("controls");
let previewTheme = $state<"light" | "dark">("light");
let previewWidth = $state<"fluid" | "mobile" | "tablet" | "desktop">("fluid");
let previewMotion = $state<"normal" | "slow" | "none">("normal");
Expand Down Expand Up @@ -122,12 +126,9 @@
});
}

function handleApplyTheme(theme: PresetTheme) {
if (theme.id === "default") {
setOverrides({});
} else {
handleBulkChange(theme.overrides as Record<string, string | null>);
}
// Applying a saved theme replaces the entire override set with the snapshot.
function handleApplyTheme(themeOverrides: Record<string, string>) {
setOverrides({ ...themeOverrides });
}

function handleResetAll() {
Expand Down Expand Up @@ -239,15 +240,17 @@

<!-- Main body: sidebar + left panel + preview -->
<div class="flex flex-1 min-h-0">
<!-- Icon nav rail -->
<SidebarNav
activeId={domain}
onSelect={(d) => { domain = d; }}
overridesByDomain={domainBadges}
/>
<!-- Icon nav rail — hidden on mobile while the preview is folded open -->
<div class={`shrink-0 ${mobileView === "preview" ? "hidden md:flex" : "flex"}`}>
<SidebarNav
activeId={domain}
onSelect={(d) => { domain = d; }}
overridesByDomain={domainBadges}
/>
</div>

<!-- Left domain panel — fixed 360px -->
<div class="w-[360px] shrink-0 bg-[#0c0c15] border-r border-white/8 flex flex-col min-h-0">
<!-- Left domain panel — full width on mobile, fixed 360px on desktop -->
<div class={`w-full md:w-[360px] shrink-0 bg-[#0c0c15] border-r border-white/8 flex-col min-h-0 ${mobileView === "preview" ? "hidden md:flex" : "flex"}`}>
<!-- Panel heading -->
<div class="h-9 flex items-center px-4 border-b border-white/6 shrink-0">
<span data-testid="panel-heading" class="text-[11px] font-bold text-slate-300 uppercase tracking-widest flex-1">
Expand All @@ -270,8 +273,8 @@
</div>
</div>

<!-- Right: live preview -->
<div class="flex-1 flex flex-col min-h-0 min-w-0">
<!-- Right: live preview — full screen on mobile when folded open -->
<div class={`flex-1 flex-col min-h-0 min-w-0 ${mobileView === "controls" ? "hidden md:flex" : "flex"}`}>
<PreviewPanel
{overrides}
{previewTheme}
Expand All @@ -286,6 +289,31 @@
</div>
</div>

<!-- Mobile fold toggle: switch between the controls panel and the live preview -->
<div class="md:hidden flex items-stretch border-t border-white/8 bg-[#0d0d14] shrink-0">
<button
onclick={() => { mobileView = "controls"; }}
aria-pressed={mobileView === "controls"}
class={`flex-1 flex items-center justify-center gap-1.5 py-2.5 text-[11px] font-bold transition-colors cursor-pointer ${
mobileView === "controls" ? "text-indigo-300 bg-indigo-500/10" : "text-slate-500 hover:text-slate-300"
}`}
>
<SlidersHorizontal class="w-3.5 h-3.5" /> Controls
</button>
<button
onclick={() => { mobileView = "preview"; }}
aria-pressed={mobileView === "preview"}
class={`flex-1 flex items-center justify-center gap-1.5 py-2.5 text-[11px] font-bold transition-colors cursor-pointer ${
mobileView === "preview" ? "text-indigo-300 bg-indigo-500/10" : "text-slate-500 hover:text-slate-300"
}`}
>
<Eye class="w-3.5 h-3.5" /> Preview
{#if overridesCount > 0}
<span class="w-4 h-4 bg-indigo-600 text-white rounded-full flex items-center justify-center text-[8px] font-black">{overridesCount > 9 ? "9+" : overridesCount}</span>
{/if}
</button>
</div>

<!-- Status bar -->
<StatusBar
{overridesCount}
Expand Down
10 changes: 5 additions & 5 deletions configurator/src/components/DomainPanel.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { SlidersHorizontal, List } from 'lucide-svelte';
import type { SlashedToken, PresetTheme } from '../types';
import type { SlashedToken } from '../types';
import { DOMAIN_PATTERNS } from '../lib/domains';
import HomePanel from './panels/HomePanel.svelte';
import ColorsPanel from './panels/ColorsPanel.svelte';
Expand All @@ -27,7 +27,7 @@
onSet: (name: string, value: string) => void;
onReset: (name: string) => void;
onBulkChange: (patch: Record<string, string | null>) => void;
onApplyTheme: (theme: PresetTheme) => void;
onApplyTheme: (overrides: Record<string, string>) => void;
onSelectDomain: (d: string) => void;
onResetAll: () => void;
} = $props();
Expand Down Expand Up @@ -91,13 +91,13 @@
{:else if domain === "typography"}
<TypographyPanel {tokens} {overrides} {onSet} {onReset} {onBulkChange} />
{:else if domain === "spacing"}
<SpacingPanel {tokens} {overrides} {onSet} {onReset} {onBulkChange} />
<SpacingPanel {tokens} {overrides} {onSet} {onReset} />
{:else if domain === "layout"}
<LayoutPanel {overrides} {onSet} {onReset} {onBulkChange} />
{:else if domain === "borders"}
<BordersPanel {overrides} {onSet} {onReset} {onBulkChange} />
<BordersPanel {overrides} {onSet} {onReset} />
{:else if domain === "shadows"}
<ShadowsPanel {overrides} {onSet} {onReset} {onBulkChange} />
<ShadowsPanel {overrides} {onSet} {onReset} />
{:else if domain === "motion"}
<MotionPanel {overrides} {onSet} {onReset} {onBulkChange} />
{:else if domain === "effects"}
Expand Down
3 changes: 1 addition & 2 deletions configurator/src/components/inputs/PowerKnobRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
<div class="group">
<div class="flex items-center justify-between mb-1.5">
<div class="flex items-center gap-2">
<span class="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-400 font-bold font-mono">⚡</span>
<span class="text-[11px] font-semibold text-slate-200">{knob.label}</span>
<span class="text-[11px] font-semibold text-slate-300">{knob.label}</span>
{#if knob.driving !== undefined}
<span class="text-[9px] text-slate-600 font-mono">→ {knob.driving} tokens</span>
{/if}
Expand Down
46 changes: 0 additions & 46 deletions configurator/src/components/inputs/StylePresetCards.svelte

This file was deleted.

64 changes: 28 additions & 36 deletions configurator/src/components/panels/BordersPanel.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
<script lang="ts">
import { KNOBS_BY_DOMAIN } from '../../lib/powerKnobs';
import { CORNER_PRESETS } from '../../lib/stylePresets';
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 }: {
let { overrides, onSet, onReset }: {
overrides: Record<string, string>;
onSet: (name: string, value: string) => void;
onReset: (name: string) => void;
onBulkChange: (patch: Record<string, string | null>) => void;
} = $props();

const BORDER_STYLES = ["solid", "dashed", "dotted"];
Expand Down Expand Up @@ -87,16 +84,6 @@

<div class="p-4 space-y-5">

<!-- Corner style presets -->
<StylePresetCards
label="Corner style"
presets={CORNER_PRESETS}
{overrides}
onApply={onBulkChange}
/>

<div class="h-px bg-white/6"></div>

<!-- BORDER COLOR -->
<section class="space-y-3">
<button
Expand Down Expand Up @@ -327,28 +314,8 @@

<div class="h-px bg-white/6"></div>

<!-- RADIUS SCALE KNOB -->
<div>
<button
onclick={() => { showRadiusScale = !showRadiusScale; }}
aria-expanded={showRadiusScale}
class="w-full flex items-center justify-between cursor-pointer"
>
<div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Radius scale</div>
<span class="text-[10px] text-slate-500">{showRadiusScale ? "▲" : "▼"}</span>
</button>
{#if showRadiusScale}
<div class="mt-3 space-y-4">
{#each knobs as k (k.name)}
<PowerKnobRow
knob={k}
{overrides}
onChange={(name, val) => val === null ? onReset(name) : onSet(name, val)}
/>
{/each}
</div>
{/if}
</div>
<!-- RADIUS -->
<div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Radius</div>

<!-- Fine-tune radius steps collapsible -->
<div>
Expand Down Expand Up @@ -440,4 +407,29 @@
</div>
{/if}
</div>

<div class="h-px bg-white/6"></div>

<!-- ADVANCED (power knob, de-emphasised, at end) -->
<div>
<button
onclick={() => { showRadiusScale = !showRadiusScale; }}
aria-expanded={showRadiusScale}
class="w-full flex items-center justify-between text-slate-600 hover:text-slate-400 transition-colors cursor-pointer"
>
<div class="text-[10px] font-semibold uppercase tracking-widest">Advanced</div>
<span class="text-[10px]">{showRadiusScale ? "▲" : "▼"}</span>
</button>
{#if showRadiusScale}
<div class="mt-3 space-y-4">
{#each knobs as k (k.name)}
<PowerKnobRow
knob={k}
{overrides}
onChange={(name, val) => val === null ? onReset(name) : onSet(name, val)}
/>
{/each}
</div>
{/if}
</div>
</div>
45 changes: 23 additions & 22 deletions configurator/src/components/panels/EffectsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
{ label: "To right", value: "to right" },
];

const TEXT_SHADOW_PRESETS = [
{ label: "None", patch: { "--sf-text-shadow-s": "none", "--sf-text-shadow-m": "none", "--sf-text-shadow-l": "none" } },
{ label: "Subtle", patch: null },
{ label: "Strong", patch: { "--sf-shadow-strength": "calc(0.25 + var(--sf-is-dark) * 0.17)" } },
const TEXT_SHADOW_TOKENS = [
{ label: "Small", token: "--sf-text-shadow-s", default: "0 1px 2px oklch(…)" },
{ label: "Medium", token: "--sf-text-shadow-m", default: "0 2px 4px oklch(…)" },
{ label: "Large", token: "--sf-text-shadow-l", default: "0 4px 8px oklch(…)" },
];

function parseNum(val: string | undefined, fallback: number, strip?: string): number {
Expand Down Expand Up @@ -274,25 +274,26 @@
{#if showTextShadow}
<p class="text-[10px] text-slate-600 leading-relaxed">
Controls text legibility over images. Applied via <code class="text-slate-400">text-shadow: var(--sf-text-shadow-m)</code>.
Edit each token directly — use <code class="text-slate-400">none</code> to disable.
</p>
<div class="flex gap-2">
{#each TEXT_SHADOW_PRESETS as p (p.label)}
<button
onclick={() => {
if (p.patch === null) {
onReset("--sf-text-shadow-s"); onReset("--sf-text-shadow-m"); onReset("--sf-text-shadow-l");
} else {
for (const [k, v] of Object.entries(p.patch)) onSet(k, v);
}
}}
class={`flex-1 py-2 rounded-lg text-[10px] border transition-all cursor-pointer ${
p.label === "None" && overrides["--sf-text-shadow-s"] === "none"
? "bg-indigo-500/15 border-indigo-500/40 text-indigo-200"
: p.patch === null && !("--sf-text-shadow-s" in overrides)
? "bg-indigo-500/15 border-indigo-500/40 text-indigo-200"
: "border-white/8 text-slate-400 hover:bg-white/5 hover:text-slate-200"
}`}
>{p.label}</button>
<div class="space-y-2">
{#each TEXT_SHADOW_TOKENS as t (t.token)}
<div class="flex items-center gap-2">
<div class="text-[10px] font-semibold text-slate-400 w-16 shrink-0">{t.label}</div>
<input
type="text"
value={overrides[t.token] ?? ""}
placeholder={t.default}
oninput={(e) => {
const v = (e.target as HTMLInputElement).value;
v.trim() ? onSet(t.token, v) : 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"
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{#if t.token in overrides}
<button onclick={() => onReset(t.token)} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer shrink-0">reset</button>
{/if}
</div>
{/each}
</div>
{/if}
Expand Down
Loading