From 0d6aa4625ecb31ebd6ae9660a3c20fbea4f97397 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:29:49 +0000 Subject: [PATCH 1/3] feat(configurator): add token relationship model and surface roles/state in All tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce src/lib/tokenModel.ts, the configurator's first-class understanding of how tokens relate — derived from the framework manifest (role, aliasOf, namespace, value) instead of re-guessing from names: - roleOf: source | alias | output - aliasTargetOf / referencesIn / pureVarTarget - buildDependencyGraph: whole-catalogue dependsOn / usedBy - SCALE_FAMILIES + scaleShadows: generalise scale-step shadow detection to radius, border-width and motion (previously only text/space) - validateTokenValue: reject empty / CSS-breaking values (closes the "--x: ;" export hole) with optional CSS.supports probing in the browser - tokenState: default | custom | relinked | detached | invalid Wire it into the All tokens screen (TokenRow/AllTokensTab): each row now shows a role badge, its alias source ("inherits X"), a "used by N" dependents count, and an explicit Detached/Invalid warning with a one-click restore — directly addressing the audit findings that outputs were editable like knobs with no dependency context and detachment was invisible. Adds tests/tokenModel.test.ts (34 tests incl. a guard over the real baked catalogue). check, lint and all 244 unit tests pass. --- .../src/components/inputs/TokenRow.svelte | 75 +++- .../src/components/panels/AllTokensTab.svelte | 6 + configurator/src/lib/tokenModel.ts | 356 ++++++++++++++++++ configurator/tests/tokenModel.test.ts | 234 ++++++++++++ 4 files changed, 669 insertions(+), 2 deletions(-) create mode 100644 configurator/src/lib/tokenModel.ts create mode 100644 configurator/tests/tokenModel.test.ts diff --git a/configurator/src/components/inputs/TokenRow.svelte b/configurator/src/components/inputs/TokenRow.svelte index 6fae647a..ad14a196 100644 --- a/configurator/src/components/inputs/TokenRow.svelte +++ b/configurator/src/components/inputs/TokenRow.svelte @@ -2,14 +2,33 @@ import type { SlashedToken } from '../../types'; import { resolveColor, previewVersion } from '../../lib/previewResolver.svelte'; import { scaleForValue } from '../../lib/variableScales'; + import { roleOf, aliasTargetOf, tokenState, ROLE_LABEL, type TokenRole, type TokenState } from '../../lib/tokenModel'; - let { token, overrideValue, onSet, onReset }: { + let { token, overrideValue, onSet, onReset, dependentsCount = 0 }: { token: SlashedToken; overrideValue?: string; onSet: (value: string) => void; onReset: () => void; + /** How many other tokens reference this one (from the dependency graph). */ + dependentsCount?: number; } = $props(); + // --- Token model: role, alias target and override state ------------------- + // These make the row honest about *what kind of token* is being edited and + // whether the current override quietly disconnects it from the system that + // produces it (a generated output/alias/scale step). See lib/tokenModel.ts. + let role = $derived(roleOf(token)); + let aliasTarget = $derived(aliasTargetOf(token)); + let overrideState = $derived( + overrideValue === undefined ? "default" : tokenState(token, { [token.name]: overrideValue }) + ); + const ROLE_STYLE: Record = { + source: "text-slate-500 dark:text-slate-400 bg-black/5 dark:bg-white/8", + alias: "text-sky-700 dark:text-sky-300 bg-sky-500/10", + output: "text-violet-700 dark:text-violet-300 bg-violet-500/10", + }; + let aliasShort = $derived(aliasTarget ? aliasTarget.replace("--sf-", "") : ""); + const CUSTOM = "__sf_custom__"; let expanded = $state(false); @@ -73,9 +92,22 @@ style:background={swatchColor} > {/if} -
+
{shortName}
+ + + {ROLE_LABEL[role]} + {#if isOverridden}
{/if} + + {#if aliasTarget || dependentsCount > 0} +
+ {#if aliasTarget} + ↳ inherits {aliasShort} + {/if} + {#if dependentsCount > 0} + + used by {dependentsCount} + + {/if} +
+ {/if} + + + {#if overrideState === "detached"} +
+ Detached + + {role === "alias" + ? `frozen — no longer follows ${aliasShort}.` + : role === "output" + ? "frozen — no longer derived from its source tokens." + : "a generated scale step is pinned; its source knob won't move it."} + + +
+ {:else if overrideState === "invalid"} +
+ Invalid + + this value can't be applied safely. + + +
+ {/if} +
{#if showScalePicker && scaleOpts}