theme.css generated from the design system — and 28 things it found #92

Merged
bvandeusen merged 2 commits from dev into main 2026-07-31 12:45:07 -04:00
6 changed files with 351 additions and 184 deletions
+319 -161
View File
@@ -1,149 +1,307 @@
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Inter:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:ital,wght@0,400;1,400&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Inter:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:ital,wght@0,400;1,400&display=swap');
/* ==========================================================================
GENERATED FROM THE DESIGN SYSTEM — Scribe (design system 2), which inherits
the FabledSword house style (design system 1).
Do not hand-edit the --fs-* block below. Edit the design system in the app
and regenerate: /design-systems -> Master stylesheet -> Copy.
DARK IS THE BASE LAYER. The kit is dark-mode-first, so :root carries the dark
palette and [data-theme="light"] overrides it. That is the inverse of how this
file used to read, and it is deliberate: the light palette was never specified
by any rule, so it is recorded as a departure rather than as the default.
Only 12 tokens differ between modes. Everything else — spacing, type, motion,
radius, and every derived colour — is stated once, because a value built with
var() resolves where it is USED, not where it is written.
========================================================================== */
:root { :root {
/* Light mode — warm parchment palette */ /* accent */
--color-bg: #F5F1E8; --fs-accent: #5B4A8A; /* Scribe's signature colour */
--color-bg-secondary: #FBF8F0; --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent); /* Pill, tag and badge backgrounds */
--color-bg-card: #FBF8F0; --fs-accent-faint: color-mix(in srgb, var(--fs-accent) 8%, transparent); /* The faintest accent wash */
--color-surface: #EFEAE0; --fs-accent-deep: color-mix(in srgb, var(--fs-accent) 70%, black); /* The accent, darkened */
--color-text: #14171A; --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 22%, transparent); /* Heaviest accent tint */
--color-text-secondary: #5A5852; --fs-gradient-cta: linear-gradient(135deg, var(--fs-accent), var(--fs-accent-deep));
--color-text-muted: #9A9890; --fs-glow-cta: 0 2px 10px color-mix(in srgb, var(--fs-accent) 35%, transparent);
--color-border: #D9D6CE; --fs-glow-cta-hover: 0 4px 24px color-mix(in srgb, var(--fs-accent) 65%, transparent);
--color-input-border: #D9D6CE;
--color-primary: #5B4A8A;
--color-danger: #C04A1F;
--color-tag-bg: rgba(91, 74, 138, 0.12);
--color-tag-text: #5B4A8A;
--color-shadow: rgba(0, 0, 0, 0.08);
--color-toast-success: #4A5D3F;
--color-toast-error: #C04A1F;
--color-status-todo: #3F4651;
--color-status-todo-bg: rgba(63, 70, 81, 0.10);
--color-status-in-progress: #5B4A8A;
--color-status-in-progress-bg: rgba(91, 74, 138, 0.12);
--color-status-done: #4A5D3F;
--color-status-done-bg: rgba(74, 93, 63, 0.12);
--color-priority-low: #3D5A6E;
--color-priority-low-bg: rgba(61, 90, 110, 0.12);
--color-priority-medium: #8B6F1E;
--color-priority-medium-bg: rgba(139, 111, 30, 0.12);
--color-priority-high: #C04A1F;
--color-priority-high-bg: rgba(192, 74, 31, 0.12);
--color-wikilink: #5B4A8A;
--color-wikilink-bg: rgba(91, 74, 138, 0.12);
--color-overdue: #C04A1F;
--color-code-bg: #EBEDF0;
--color-code-inline-bg: #EBEDF0;
--color-table-stripe: rgba(20, 23, 26, 0.025);
--color-success: #4A5D3F;
--color-warning: #8B6F1E;
--color-input-bar-bg: #EFEAE0;
--color-input-bar-text: #14171A;
--color-input-bar-placeholder: rgba(20, 23, 26, 0.4);
--color-overlay: rgba(0, 0, 0, 0.45);
--color-bubble-user-bg: transparent;
--color-bubble-user-border: #D9D6CE;
--color-bubble-user-text: #5A5852;
--color-bubble-asst-shadow: 0 2px 14px rgba(91, 74, 138, 0.06), 0 1px 4px rgba(0, 0, 0, 0.05);
--color-primary-solid: #5B4A8A;
--color-primary-deep: #3F3560;
--gradient-cta: linear-gradient(135deg, var(--color-primary-solid), var(--color-primary-deep));
--glow-cta: 0 2px 10px rgba(91, 74, 138, 0.35);
--glow-cta-hover: 0 4px 20px rgba(91, 74, 138, 0.55);
--glow-soft: 0 0 16px rgba(91, 74, 138, 0.35);
--color-primary-faint: rgba(91, 74, 138, 0.08);
--color-primary-tint: rgba(91, 74, 138, 0.12);
--color-primary-wash: rgba(91, 74, 138, 0.20);
/* Action color set — Hybrid rule: action buttons use these, accent reserved for brand moments */ /* action */
--color-action-primary: #4A5D3F; --fs-action-primary: #4A5D3F; /* Save, Submit, Confirm */
--color-action-primary-hover: #5A6F4D; --fs-action-secondary: #8B7355; /* Non-destructive alternates */
--color-action-secondary: #8B7355; --fs-action-tertiary: var(--fs-border-color); /* Ghost / outline actions */
--color-action-secondary-hover: #A0876A; --fs-action-destructive: var(--fs-destructive);
--color-action-destructive: #6B2118; --fs-action-primary-hover: color-mix(in srgb, var(--fs-action-primary) 88%, white);
--color-action-destructive-hover: #7E2A1F; --fs-action-secondary-hover: color-mix(in srgb, var(--fs-action-secondary) 88%, white);
--color-action-ghost-border: #3F4651; --fs-action-destructive-hover: color-mix(in srgb, var(--fs-action-destructive) 88%, white);
--radius-sm: 6px; /* border */
--radius-md: 12px; --fs-border-color: #3F4651; /* Borders, dividers and ghost outlines */
--radius-lg: 18px; --fs-border: 0.5px solid var(--fs-border-color);
--radius-pill: 9999px; --fs-border-hover: 0.5px solid color-mix(in srgb, var(--fs-text-secondary) 30%, transparent);
--focus-ring: 0 0 0 2px rgba(91, 74, 138, 0.5); --fs-border-active: 2px solid var(--fs-accent); /* Selected card or active tab only */
/* Layout */
--page-max-width: 1200px; /* editor */
--page-padding-x: 1rem; --fs-wikilink: var(--fs-accent);
--sidebar-width: 260px;
--chat-reading-width: min(1200px, 100%); /* elevation */
--chat-context-sidebar-width: 220px; --fs-shadow-1: 0 1px 0 rgba(0,0,0,0.4); /* Hairline lift */
--fs-shadow-2: 0 4px 12px rgba(0,0,0,0.35); /* Dropdowns, popovers */
--fs-shadow-3: 0 16px 40px rgba(0,0,0,0.5); /* Modals */
/* focus */
--fs-focus-ring: 0 0 0 2px var(--fs-accent);
/* font */
--fs-font-display: 'Fraunces', Georgia, serif;
--fs-font-body: 'Inter', system-ui, sans-serif;
--fs-font-mono: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
/* icon */
--fs-icon-stroke: 1.5px; /* at 24px */
--fs-icon-stroke-sm: 1px; /* at 16px */
/* layout */
--fs-layout-page-max: 1200px;
--fs-layout-page-pad: 1rem;
--fs-layout-sidebar: 260px;
--fs-layout-header: 52px;
/* motion */
--fs-ease: cubic-bezier(0.2, 0.6, 0.2, 1); /* the one curve */
--fs-dur-fast: 120ms;
--fs-dur-base: 180ms;
--fs-dur-slow: 280ms;
/* priority */
--fs-priority-low: var(--fs-info);
--fs-priority-low-bg: color-mix(in srgb, var(--fs-priority-low) 12%, transparent);
--fs-priority-medium: var(--fs-warning);
--fs-priority-medium-bg: color-mix(in srgb, var(--fs-priority-medium) 12%, transparent);
--fs-priority-high: var(--fs-error);
--fs-priority-high-bg: color-mix(in srgb, var(--fs-priority-high) 12%, transparent);
/* radius */
--fs-radius-sm: 4px; /* pills, tags, code spans */
--fs-radius-md: 8px; /* buttons, inputs, small cards */
--fs-radius-lg: 12px; /* cards, panels, modals */
--fs-radius-xl: 16px; /* hero containers */
--fs-radius-pill: 9999px;
/* semantic */
--fs-success: var(--fs-action-primary);
--fs-warning: #8B6F1E;
--fs-error: #C04A1F;
--fs-info: #3D5A6E;
--fs-destructive: #6B2118; /* irreversible — deliberately not the error colour */
/* space */
--fs-space-1: 4px;
--fs-space-2: 8px;
--fs-space-3: 12px;
--fs-space-4: 16px;
--fs-space-5: 20px;
--fs-space-6: 24px;
--fs-space-7: 32px;
--fs-space-8: 48px;
--fs-space-9: 64px;
--fs-space-10: 96px;
/* state */
--fs-disabled-opacity: 0.5;
--fs-overlay: rgba(0, 0, 0, 0.65);
/* status */
--fs-status-todo: var(--fs-border-color);
--fs-status-todo-bg: color-mix(in srgb, var(--fs-status-todo) 12%, transparent);
--fs-status-in-progress: var(--fs-accent);
--fs-status-in-progress-bg: color-mix(in srgb, var(--fs-status-in-progress) 12%, transparent);
--fs-status-done: var(--fs-success);
--fs-status-done-bg: color-mix(in srgb, var(--fs-status-done) 12%, transparent);
--fs-overdue: var(--fs-error);
--fs-status-cancelled: var(--fs-text-tertiary); /* set aside, not failed */
/* surface */
--fs-surface-page: #14171A; /* page bg, deepest surface */
--fs-surface-raised: #1E2228; /* cards, raised elements */
--fs-surface-hover: #2C313A; /* hovered surfaces */
--fs-surface-code: var(--fs-surface-page);
--fs-surface-code-inline: var(--fs-surface-raised);
--fs-table-stripe: color-mix(in srgb, var(--fs-text-primary) 3%, transparent);
/* text */
--fs-text-primary: #E8E4D8; /* also the text colour on filled action buttons */
--fs-text-secondary: #C2BFB4;
--fs-text-tertiary: #9C9A92;
/* type */
--fs-size-display: 40px;
--fs-size-h1: 32px;
--fs-size-h2: 24px;
--fs-size-h3: 18px;
--fs-size-body: 15px;
--fs-size-body-sm: 13px;
--fs-size-label: 12px;
--fs-size-code: 13px;
--fs-size-tiny: 11px; /* the only ALL CAPS, the only non-default tracking */
--fs-weight-regular: 400;
--fs-weight-medium: 500; /* the heaviest the system goes */
--fs-leading-heading: 1.3;
--fs-leading-body: 1.5;
--fs-leading-longform: 1.7;
--fs-tracking-tiny: 0.08em;
} }
[data-theme="dark"] { [data-theme="light"] {
/* Dark mode — Obsidian / Iron / Pewter */ /* accent */
--color-bg: #14171A; --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 20%, transparent);
--color-bg-secondary: #1E2228; --fs-glow-cta-hover: 0 4px 20px color-mix(in srgb, var(--fs-accent) 55%, transparent);
--color-bg-card: #1E2228;
--color-surface: #2C313A; /* border */
--color-text: #E8E4D8; --fs-border-color: #D9D6CE;
--color-text-secondary: #C2BFB4;
--color-text-muted: #9C9A92; /* state */
--color-border: #3F4651; --fs-overlay: rgba(0, 0, 0, 0.45);
--color-input-border: #3F4651;
--color-primary: #5B4A8A; /* surface */
--color-danger: #C04A1F; --fs-surface-page: #F5F1E8;
--color-tag-bg: rgba(91, 74, 138, 0.15); --fs-surface-raised: #FBF8F0;
--color-tag-text: #5B4A8A; --fs-surface-hover: #EFEAE0;
--fs-surface-code: #EBEDF0;
--fs-surface-code-inline: #EBEDF0;
/* text */
--fs-text-primary: #14171A;
--fs-text-secondary: #5A5852;
--fs-text-tertiary: #9A9890;
}
/* ==========================================================================
COMPATIBILITY ALIASES — the app's historical names, pointing at the system.
These exist so ~55 components keep working while they migrate to --fs-*
one at a time. Every one is a plain var() reference, which is what lets this
block be declared ONCE: when [data-theme="light"] moves --fs-surface-page,
--color-bg follows, because the alias resolves at use time.
That is why this file lost 48 of its 60 dark-mode overrides — they were all
restating relationships the aliases now express directly.
Removing this block is a rename sweep across the components, tracked
separately. Nothing new should reference a --color-* name.
========================================================================== */
:root {
/* surfaces */
--color-bg: var(--fs-surface-page);
--color-bg-secondary: var(--fs-surface-raised);
--color-bg-card: var(--fs-surface-raised);
--color-surface: var(--fs-surface-hover);
--color-code-bg: var(--fs-surface-code);
--color-code-inline-bg: var(--fs-surface-code-inline);
--color-table-stripe: var(--fs-table-stripe);
--color-overlay: var(--fs-overlay);
/* text */
--color-text: var(--fs-text-primary);
--color-text-secondary: var(--fs-text-secondary);
--color-text-muted: var(--fs-text-tertiary);
/* lines */
--color-border: var(--fs-border-color);
--color-input-border: var(--fs-border-color);
--focus-ring: var(--fs-focus-ring);
/* brand */
--color-primary: var(--fs-accent);
--color-primary-solid: var(--fs-accent);
--color-primary-deep: var(--fs-accent-deep);
--color-primary-faint: var(--fs-accent-faint);
--color-primary-tint: var(--fs-accent-soft);
--color-primary-wash: var(--fs-accent-wash);
--color-tag-bg: var(--fs-accent-soft);
--color-tag-text: var(--fs-accent);
--color-wikilink: var(--fs-wikilink);
--color-wikilink-bg: var(--fs-accent-soft);
--gradient-cta: var(--fs-gradient-cta);
--glow-cta: var(--fs-glow-cta);
--glow-cta-hover: var(--fs-glow-cta-hover);
/* actions */
--color-action-primary: var(--fs-action-primary);
--color-action-primary-hover: var(--fs-action-primary-hover);
--color-action-secondary: var(--fs-action-secondary);
--color-action-secondary-hover: var(--fs-action-secondary-hover);
--color-action-destructive: var(--fs-action-destructive);
--color-action-destructive-hover: var(--fs-action-destructive-hover);
/* semantic */
--color-success: var(--fs-success);
--color-warning: var(--fs-warning);
--color-danger: var(--fs-error);
--color-overdue: var(--fs-overdue);
--color-toast-success: var(--fs-success);
--color-toast-error: var(--fs-error);
--color-shadow: rgba(0, 0, 0, 0.4); --color-shadow: rgba(0, 0, 0, 0.4);
--color-toast-success: #4A5D3F;
--color-toast-error: #C04A1F;
--color-status-todo: #3F4651;
--color-status-todo-bg: rgba(63, 70, 81, 0.18);
--color-status-in-progress: #5B4A8A;
--color-status-in-progress-bg: rgba(91, 74, 138, 0.18);
--color-status-done: #4A5D3F;
--color-status-done-bg: rgba(74, 93, 63, 0.18);
--color-priority-low: #3D5A6E;
--color-priority-low-bg: rgba(61, 90, 110, 0.18);
--color-priority-medium: #8B6F1E;
--color-priority-medium-bg: rgba(139, 111, 30, 0.18);
--color-priority-high: #C04A1F;
--color-priority-high-bg: rgba(192, 74, 31, 0.18);
--color-wikilink: #5B4A8A;
--color-wikilink-bg: rgba(91, 74, 138, 0.18);
--color-overdue: #C04A1F;
--color-code-bg: #14171A;
--color-code-inline-bg: #1E2228;
--color-table-stripe: rgba(255, 255, 255, 0.025);
--color-success: #4A5D3F;
--color-warning: #8B6F1E;
--color-input-bar-bg: #1E2228;
--color-input-bar-text: #E8E4D8;
--color-input-bar-placeholder: rgba(232, 228, 216, 0.35);
--color-overlay: rgba(0, 0, 0, 0.65);
--color-bubble-user-bg: transparent;
--color-bubble-user-border: #3F4651;
--color-bubble-user-text: #C2BFB4;
--color-bubble-asst-shadow: 0 4px 28px rgba(91, 74, 138, 0.14), 0 2px 8px rgba(0, 0, 0, 0.4);
--color-primary-solid: #5B4A8A;
--color-primary-deep: #3F3560;
--gradient-cta: linear-gradient(135deg, var(--color-primary-solid), var(--color-primary-deep));
--glow-cta: 0 2px 12px rgba(91, 74, 138, 0.45);
--glow-cta-hover: 0 4px 24px rgba(91, 74, 138, 0.65);
--glow-soft: 0 0 18px rgba(91, 74, 138, 0.4);
--color-primary-faint: rgba(91, 74, 138, 0.10);
--color-primary-tint: rgba(91, 74, 138, 0.14);
--color-primary-wash: rgba(91, 74, 138, 0.22);
/* Action color set — identical across themes */ /* task status + priority */
--color-action-primary: #4A5D3F; --color-status-todo: var(--fs-status-todo);
--color-action-primary-hover: #5A6F4D; --color-status-todo-bg: var(--fs-status-todo-bg);
--color-action-secondary: #8B7355; --color-status-in-progress: var(--fs-status-in-progress);
--color-action-secondary-hover: #A0876A; --color-status-in-progress-bg: var(--fs-status-in-progress-bg);
--color-action-destructive: #6B2118; --color-status-done: var(--fs-status-done);
--color-action-destructive-hover: #7E2A1F; --color-status-done-bg: var(--fs-status-done-bg);
--color-action-ghost-border: #3F4651; --color-priority-low: var(--fs-priority-low);
--color-priority-low-bg: var(--fs-priority-low-bg);
--color-priority-medium: var(--fs-priority-medium);
--color-priority-medium-bg: var(--fs-priority-medium-bg);
--color-priority-high: var(--fs-priority-high);
--color-priority-high-bg: var(--fs-priority-high-bg);
/* geometry */
--radius-sm: var(--fs-radius-sm);
--radius-md: var(--fs-radius-lg); /* NB: the app's "md" is the system's LARGE */
--radius-lg: var(--fs-radius-xl); /* and the app's "lg" is the system's XL */
--page-max-width: var(--fs-layout-page-max);
--page-padding-x: var(--fs-layout-page-pad);
--sidebar-width: var(--fs-layout-sidebar);
--header-height: var(--fs-layout-header);
/* ------------------------------------------------------------------
Names components reference that were NEVER declared anywhere.
Each of these was reached for with a hardcoded fallback, so the page
rendered — but the fallback was what rendered, always, and several were
off-palette: --color-primary-bg fell back to an indigo, --color-destructive
to a brick that is not the oxblood, --color-status-cancelled to a grey from
no palette in this system.
Wiring them to real tokens is the whole point of the exercise. Expect small
visual shifts exactly where a fallback had drifted; that shift IS the fix.
------------------------------------------------------------------ */
--color-accent: var(--fs-accent);
--color-accent-fg: var(--fs-text-primary);
--color-hover: var(--fs-surface-hover);
--color-bg-hover: var(--fs-surface-hover);
--color-bg-tertiary: var(--fs-surface-hover);
--color-surface-2: var(--fs-surface-hover);
--color-surface-alt: var(--fs-surface-hover);
--color-surface-raised: var(--fs-surface-raised);
--color-input-bg: var(--fs-surface-page);
--color-muted: var(--fs-text-tertiary);
--color-destructive: var(--fs-destructive);
--color-primary-bg: var(--fs-accent-soft);
--color-status-cancelled: var(--fs-status-cancelled);
--font-display: var(--fs-font-display);
--font-mono: var(--fs-font-mono);
} }
/* ==========================================================================
Base element styles
========================================================================== */
*, *,
*::before, *::before,
*::after { *::after {
@@ -152,36 +310,36 @@
body { body {
margin: 0; margin: 0;
background: var(--color-bg); background: var(--fs-surface-page);
color: var(--color-text); color: var(--fs-text-primary);
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, font-family: var(--fs-font-body);
"Segoe UI", Roboto, sans-serif;
font-feature-settings: "cv11"; font-feature-settings: "cv11";
line-height: 1.5; line-height: var(--fs-leading-body);
transition: background-color 0.2s, color 0.2s; transition: background-color var(--fs-dur-base) var(--fs-ease),
color var(--fs-dur-base) var(--fs-ease);
} }
h1, h2 { h1, h2 {
font-family: 'Fraunces', Georgia, serif; font-family: var(--fs-font-display);
font-optical-sizing: auto; font-optical-sizing: auto;
font-weight: 500; font-weight: var(--fs-weight-medium);
line-height: 1.3; line-height: var(--fs-leading-heading);
} }
h3 { h3 {
font-family: 'Inter', system-ui, sans-serif; font-family: var(--fs-font-body);
font-weight: 500; font-weight: var(--fs-weight-medium);
line-height: 1.3; line-height: var(--fs-leading-heading);
} }
code, pre, kbd, samp { code, pre, kbd, samp {
font-family: 'JetBrains Mono', ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-family: var(--fs-font-mono);
font-feature-settings: "liga", "calt"; font-feature-settings: "liga", "calt";
} }
::selection { ::selection {
background: rgba(91, 74, 138, 0.3); background: var(--fs-accent-wash);
color: var(--color-text); color: var(--fs-text-primary);
} }
input:focus-visible, input:focus-visible,
@@ -190,15 +348,15 @@ select:focus-visible,
button:focus-visible, button:focus-visible,
a:focus-visible { a:focus-visible {
outline: none; outline: none;
box-shadow: var(--focus-ring); box-shadow: var(--fs-focus-ring);
border-radius: var(--radius-sm); border-radius: var(--fs-radius-sm);
} }
button:not(:disabled):active, button:not(:disabled):active,
.btn:not(:disabled):active, .btn:not(:disabled):active,
[role="button"]:not(:disabled):active { [role="button"]:not(:disabled):active {
transform: scale(0.97); transform: scale(0.97);
transition: transform 0.08s ease; transition: transform 0.08s var(--fs-ease);
} }
/* Responsive breakpoints: 480px (phone), 768px (tablet), 1024px (desktop) */ /* Responsive breakpoints: 480px (phone), 768px (tablet), 1024px (desktop) */
@@ -228,11 +386,11 @@ button:not(:disabled):active,
background: transparent; background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--color-border); background: var(--fs-border-color);
border-radius: 9999px; border-radius: var(--fs-radius-pill);
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted); background: var(--fs-text-tertiary);
} }
/* Floating inline assist button (teleported to body, cannot be scoped) */ /* Floating inline assist button (teleported to body, cannot be scoped) */
@@ -240,14 +398,14 @@ button:not(:disabled):active,
position: fixed; position: fixed;
z-index: 150; z-index: 150;
transform: translateX(-50%); transform: translateX(-50%);
background: var(--color-primary); background: var(--fs-accent);
color: #fff; color: var(--fs-text-primary);
border: none; border: none;
border-radius: 999px; border-radius: var(--fs-radius-pill);
padding: 0.3rem 0.8rem; padding: 0.3rem 0.8rem;
font-size: 0.8rem; font-size: var(--fs-size-body-sm);
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 8px var(--color-shadow); box-shadow: var(--fs-shadow-2);
white-space: nowrap; white-space: nowrap;
} }
.inline-assist-btn:hover { .inline-assist-btn:hover {
+5 -5
View File
@@ -530,7 +530,7 @@ function isColourish(value: string): boolean {
<label class="field-label" for="first-title">Title</label> <label class="field-label" for="first-title">Title</label>
<input <input
id="first-title" v-model="newTitle" class="input" type="text" id="first-title" v-model="newTitle" class="input" type="text"
placeholder="FabledSword" @keyup.enter="submitCreate" placeholder="Your house style" @keyup.enter="submitCreate"
/> />
</div> </div>
<div class="field"> <div class="field">
@@ -581,7 +581,7 @@ function isColourish(value: string): boolean {
<label class="field-label" for="new-title">Title</label> <label class="field-label" for="new-title">Title</label>
<input <input
id="new-title" v-model="newTitle" class="input" type="text" id="new-title" v-model="newTitle" class="input" type="text"
placeholder="FabledSword, or Scribe" @keyup.enter="submitCreate" placeholder="A house style, or one app in it" @keyup.enter="submitCreate"
/> />
</div> </div>
<div class="field"> <div class="field">
@@ -852,7 +852,7 @@ function isColourish(value: string): boolean {
<label class="field-label" for="token-name">Name</label> <label class="field-label" for="token-name">Name</label>
<input <input
id="token-name" v-model="tokenName" class="input mono" type="text" id="token-name" v-model="tokenName" class="input mono" type="text"
placeholder="--fs-obsidian" placeholder="--surface-page"
/> />
</div> </div>
<div class="field-row"> <div class="field-row">
@@ -864,7 +864,7 @@ function isColourish(value: string): boolean {
<label class="field-label" for="token-purpose">Purpose</label> <label class="field-label" for="token-purpose">Purpose</label>
<input <input
id="token-purpose" v-model="tokenPurpose" class="input" type="text" id="token-purpose" v-model="tokenPurpose" class="input" type="text"
placeholder="page bg, deepest surface" placeholder="page background, deepest surface"
/> />
</div> </div>
</div> </div>
@@ -873,7 +873,7 @@ function isColourish(value: string): boolean {
<label class="field-label" for="token-rationale">Why this value</label> <label class="field-label" for="token-rationale">Why this value</label>
<input <input
id="token-rationale" v-model="tokenRationale" class="input" type="text" id="token-rationale" v-model="tokenRationale" class="input" type="text"
placeholder="Success equals Moss, aligned by design" placeholder="Matches the primary action colour, deliberately"
/> />
<p class="field-hint"> <p class="field-hint">
Distinct from purpose: purpose is what the token is FOR, this is Distinct from purpose: purpose is what the token is FOR, this is
+11 -6
View File
@@ -33,7 +33,8 @@ async def create_design_system(
"""Create a design system, optionally inheriting from another. """Create a design system, optionally inheriting from another.
Args: Args:
title: What this system is, e.g. "FabledSword" or "Scribe" (required). title: What this system is — a house style, or one app within it
(required).
description: What it covers and when it applies. description: What it covers and when it applies.
guidance: The narrative a token table cannot hold — aesthetic, voice and guidance: The narrative a token table cannot hold — aesthetic, voice and
tone, what is deliberately out of scope. Markdown, free-form. tone, what is deliberately out of scope. Markdown, free-form.
@@ -226,18 +227,22 @@ async def create_design_token(
Args: Args:
design_system_id: The system that owns this token. design_system_id: The system that owns this token.
name: The custom-property name, e.g. "--fs-obsidian" (required). name: The custom-property name, e.g. "--surface-page" (required).
Name it for its PURPOSE, not its value: a name like "--obsidian"
or "--button-bg" stops being true the moment the value or the
element changes.
value_by_mode: Values keyed by mode, e.g. value_by_mode: Values keyed by mode, e.g.
{"base": "#f7f5ef", "dark": "#14171a"}. Use "base" for the value {"base": "#14171a", "light": "#f7f5ef"}. Use "base" for the value
that applies when no mode is more specific; a token that is not that applies when no mode is more specific; a token that is not
mode-dependent needs only "base". In a system WITH a parent, an mode-dependent needs only "base". In a system WITH a parent, an
omitted mode is inherited rather than blanked. omitted mode is inherited rather than blanked.
group_name: Free-text grouping — "surface", "text", "radius", whatever group_name: Free-text grouping — "surface", "text", "radius", whatever
this system's own vocabulary is. this system's own vocabulary is.
purpose: What the token is for, e.g. "page bg, deepest surface". purpose: What the token is for, e.g. "page background, deepest
surface".
rationale: WHY it is this value — a different question from purpose. rationale: WHY it is this value — a different question from purpose.
"Success equals Moss, aligned by design" is a rationale; "page bg, "Deliberately the same value as the primary action colour" is a
deepest surface" is a purpose. rationale; "page background, deepest surface" is a purpose.
supersedes: Literal values this token should be used INSTEAD OF, e.g. supersedes: Literal values this token should be used INSTEAD OF, e.g.
["#fff", "#ffffff"]. This is how a design system records what a ["#fff", "#ffffff"]. This is how a design system records what a
prohibition was trying to say — not "white is banned" but "write prohibition was trying to say — not "white is banned" but "write
+3 -2
View File
@@ -45,7 +45,8 @@ async def create_rulebook(title: str, description: str = "") -> dict:
Two ways a rulebook reaches projects, set by its always_on flag (toggle via Two ways a rulebook reaches projects, set by its always_on flag (toggle via
update_rulebook): update_rulebook):
- always_on = true -> binds EVERY one of your projects automatically. - always_on = true -> binds EVERY one of your projects automatically.
Use for universal cross-project norms (e.g. "FabledSword family"). Use for universal cross-project norms that apply across every
project, not just one.
- always_on = false -> binds only projects that subscribe - always_on = false -> binds only projects that subscribe
(subscribe_project_to_rulebook). Use for a THEMED body of rules a (subscribe_project_to_rulebook). Use for a THEMED body of rules a
category of projects shares (e.g. a design system that visual apps category of projects shares (e.g. a design system that visual apps
@@ -54,7 +55,7 @@ async def create_rulebook(title: str, description: str = "") -> dict:
to any single project. Project-specific rules go in create_project_rule. to any single project. Project-specific rules go in create_project_rule.
Args: Args:
title: Rulebook name (e.g. "FabledSword family"). title: Rulebook name.
description: Optional short description of what this rulebook covers. description: Optional short description of what this rulebook covers.
""" """
uid = current_user_id() uid = current_user_id()
+9 -7
View File
@@ -116,22 +116,24 @@ class DesignToken(Base, TimestampMixin, SoftDeleteMixin):
group_name: Mapped[str | None] = mapped_column(Text, nullable=True) group_name: Mapped[str | None] = mapped_column(Text, nullable=True)
purpose: Mapped[str | None] = mapped_column(Text, nullable=True) purpose: Mapped[str | None] = mapped_column(Text, nullable=True)
# WHY this token is this value — a different question from `purpose`, which # WHY this token is this value — a different question from `purpose`, which
# is what it is FOR. "Success equals Moss, aligned by design" is a rationale; # is what it is FOR. "Deliberately the same value as the primary action
# "page bg, deepest surface" is a purpose. Rules carry the first routinely # colour" is a rationale; "page background, deepest surface" is a purpose.
# and a token row had nowhere to put it. # Design guidance carries the first routinely and a token row had nowhere to
# put it.
rationale: Mapped[str | None] = mapped_column(Text, nullable=True) rationale: Mapped[str | None] = mapped_column(Text, nullable=True)
# Literal values this token should be used INSTEAD OF, e.g. ["#fff", # Literal values this token should be used INSTEAD OF, e.g. ["#fff",
# "#ffffff"] on a text-on-action token. # "#ffffff"] on a text-on-action token.
# #
# This is how a design system records the thing a prohibition was trying to # This is how a design system records the thing a prohibition was trying to
# say. "Pure white is never text" is the shadow of a positive fact — text is # say. "Pure white is never text" is the shadow of a positive fact — some
# Parchment — and a system that stores what things ARE has no row for a ban. # other colour IS the text colour — and a system that stores what things ARE
# has no row for a ban.
# Recording the replacement keeps the check and makes it actionable: a # Recording the replacement keeps the check and makes it actionable: a
# finding can name what to write instead of merely objecting. # finding can name what to write instead of merely objecting.
# #
# It has to be DECLARED rather than inferred, because the superseded literal # It has to be DECLARED rather than inferred, because the superseded literal
# and the token's own value are usually different colours (#fff is not # and the token's own value are usually different colours entirely. No
# #E8E4D8). No value-matching rule could ever connect them. # value-matching rule could ever connect them.
# #
# Consumed by the source lint (#2277), not by the drift panel: these # Consumed by the source lint (#2277), not by the drift panel: these
# literals live in component CSS, which the panel cannot see and says so. # literals live in component CSS, which the panel cannot see and says so.
+4 -3
View File
@@ -18,8 +18,8 @@ has recall, locations, drift checks and merge.
So the division is: this sheet says what the values MEAN; snippets say what So the division is: this sheet says what the values MEAN; snippets say what
things LOOK LIKE, in terms of those values. A token named after an element things LOOK LIKE, in terms of those values. A token named after an element
(`--fs-button-bg`) is the smell that the two have been mixed — it multiplies (`--button-bg`) is the smell that the two have been mixed — it multiplies
with every new element, where a purpose name (`--fs-action-primary`) is reused. with every new element, where a purpose name (`--action-primary`) is reused.
SAFETY SAFETY
------ ------
@@ -203,7 +203,8 @@ def duplicate_values(tokens: Sequence) -> dict[str, list[str]]:
Two names for one value are either a deliberate alias or the same idea Two names for one value are either a deliberate alias or the same idea
recorded twice — the token-level form of the duplicated-definition shape. recorded twice — the token-level form of the duplicated-definition shape.
Reported rather than refused: a design system legitimately aligns colours on Reported rather than refused: a design system legitimately aligns colours on
purpose ("Success = Moss, by design"), and a generator that rejected that purpose (one palette entry defined as equal to another), and a generator
that rejected that
would be wrong about the operator's intent. would be wrong about the operator's intent.
Compares the BASE value only. Two tokens agreeing in one mode and diverging Compares the BASE value only. Two tokens agreeing in one mode and diverging