Appearance
Is it really too ad-hoc?
Of all the options available in css, the app uses a lot. Most of them hard wired with some number that made sense at the moment. now, quite a bit of code later, it smells to me -- too ad hoc, fragile, missing a mental model. it is time to heft our maintenance cost, going forward.
proposal
goal: tighten the pipeline. don't rebuild — clean up the seams.
- audit magic numbers — grep for hardcoded px values in
<style>blocks. each one either gets pulled into Constants.ts or justified with a comment. - close the color leaks — every color should flow through Colors.ts → CSS vars. find hardcoded color strings in components and route them through the system.
- clarify the inline-vs-var boundary — if a value changes at runtime, inline style:. if it's a design token, CSS var. move stray tokens to vars.
- pseudo-element review — look at each ::before/::after usage. some are genuine, others replaceable. decide case by case.
- document the contract — short comment block at top of Constants.ts and Colors.ts explaining what they provide and how components consume them.
- work on style.md
- bring up to date with what we did here
order: 1 → 2 → 3 → 4 → 5. each step is independent and small.
done (step 1)
new constants added (replaced across ~11 files):
| constant | CSS var | value | instances |
|---|---|---|---|
k.thickness.border | --th-border | 0.5px | 25 |
k.corner.input | --corner-input | 2px | 8 |
k.layout.gap_small | --l-gap-small | 6px | 4 |
k.layout.padding_small | --l-padding-small | 8px | |
k.layout.letter_spacing | --l-letter-spacing | 0.5px | 3 |
remaining magic numbers
candidates for future tokens (appear in 3+ files):
| value | used for | files | candidate |
|---|---|---|---|
0 8px padding shorthand | side padding on small buttons/cells | D_Preferences, D_Library, P_Angles, Graph | --l-padding-small (needs manual handling) |
justified geometry (component-specific, keep with comment when touched):
| value | used for | file |
|---|---|---|
-2px, -6px, 7px, -1.1px | slider thumb/track positioning math | Slider |
10px, 14px, 3px | graph overlay positions (breadcrumbs, assist, zoom) | Graph |
10px | face label font in canvas (smaller than common) | Graph |
50px, 120px | BuildNotes button widths | BuildNotes |
81px | assist slider height | Graph |
3px border-radius | input-wrap, between --corner-input and --corner-box | P_Selected |
1.5px height | snap-line in controls | Controls |
0.25px | gradient stop hairlines | P_Attributes, P_Selected |
-0.5px transform | sub-pixel alignment tweak | P_Repeat |
0px | explicit reset | P_Repeat, Hideable |
done (step 2)
new CSS vars injected in App.svelte onMount:
| var | source | value |
|---|---|---|
--c-white | colors.background | 'white' |
--c-black | colors.default | 'black' |
--c-focus | colors.focus | 'cornflowerblue' |
--c-thumb | colors.thumb | '#007aff' |
--c-track | colors.track | '#ccc' |
--c-slider-border | colors.border | 'darkgray' |
--focus-outline | computed | '1.5px solid cornflowerblue' |
--font-edit | constant | '12px sans-serif' |
swept across 12 files (Graph, Controls, D_Library, Details, P_Selected, P_Angles, D_Parts, P_Attributes, P_Repeat, P_Constants, D_Preferences, Slider):
white→var(--c-white)(~50 instances)black→var(--c-black)(~25 instances)1.5px solid cornflowerblue→var(--focus-outline)(5 files)#007aff→var(--c-thumb)/var(--c-thumb)fallback (D_Preferences, P_Repeat, Slider)#ccc→var(--c-track)(P_Angles)darkgray→colors.borderreference (Slider TS)12px sans-serif→var(--font-edit)(Graph)
covered already, not addressed further here
- z-index — fully in Constants
- main layout gaps/corners — used via CSS vars
- color derivation pipeline — Colors.ts handles accent/bg/text/selected
- font sizes — converted to
--h-font-small/--h-font-commonCSS vars - border thickness —
--th-border(0.5px) - input corner radius —
--corner-input(2px) - small gap / small padding / letter spacing —
--l-gap-small,--l-padding-small,--l-letter-spacing - static colors —
--c-white,--c-black,--c-focus,--c-thumb,--c-track,--c-slider-border(via Colors.ts) - focus ring —
--focus-outline(1.5px solid cornflowerblue) - inline editor font —
--font-edit(12px sans-serif) - inline-vs-var boundary — static props moved to scoped CSS; runtime-computed values kept as
style:
leaks (remaining)
graph overlays input widths (60px, 80px), offsets (10px, 14px), face label font 10px — self-contained but brittle. low priority.
architecture
three-tier hybrid
TS constants → CSS custom properties → component styles
text
Constants.ts (design tokens: all measurements derived from common_size)
↓
App.svelte onMount (injects --l-gap, --h-controls, --corner-common, --text, --accent, --bg onto :root)
↓
Component-level scoped <style> blocks (use var(--x) for theming/spacing)
+ inline style: directives (style:width, style:height — for reactive computed dimensions)
+ class: bindings (class:active — for state-driven presentation)
+ data-hit attributes (set by Hit_Target.ts — CSS responds with attribute selectors)layout: JS state → $derived values → inline styles colors: Colors.ts stores → $effect in App.svelte → CSS vars → scoped CSS hover: hit detection → data-hit attribute → CSS attribute selectors spacing/corners/z-index: Constants.ts → CSS vars → scoped CSS
no external .css files. everything scoped inside .svelte components.
original questions
is this a typical software architecture? yes, pretty typical for this kind of app. having a TS runtime as source of truth that feeds CSS is the standard pattern in Svelte/React/Vue apps that need layout responsive to application state. the three-tier flow (tokens → CSS vars → scoped styles) is essentially what design-system tooling formalizes.
what's less typical is doing it without a framework/library layer — Constants.ts + Colors.ts + manual root.setProperty() instead of a CSS-in-JS library. leaner, but discipline has to come from convention rather than tooling.
does it need a rethink? not a full rethink. the bones are good. steps 1–3 addressed the accumulated friction incrementally.
done (step 3)
the rule: if a value changes at runtime → style:. if it's static/design token → scoped CSS or CSS var.
Graph.svelte — removed style:--crumb-bg = 'var(--bg)' CSS var indirection; .crumb:hover and .crumb.current now reference var(--bg) directly.
Controls.svelte — removed style:z-index={T_Layer.action} from hamburger button; added z-index: var(--z-action) to .hamburger scoped CSS rule.
P_Repeat.svelte — moved 5 static inline styles to scoped CSS:
position: relative→ added to.repeater-option-rowflex: 1→ added to.range-sliderpointer-events: auto→ new.sp-inputclass on the spacing range inputflex-shrink: 0,position: relative,top: -5.5px→ new.fireblocks-btnmodifier class with comment
BuildNotes.svelte — removed style:color='var(--text)' and style:background='var(--bg)'; moved both into .modal scoped CSS rule.
not touched: style:height='Xpx' spacers (low value), dynamic style:-- var injections (correct pattern), style:z-index={z_layer} in Separator (prop varies per instance).
done (step 4)
All five pseudo-elements across four files are genuine — no replacements made.
| pseudo | file | purpose |
|---|---|---|
.snap-off::after | Controls | diagonal strikethrough over 🧲 when snap is off |
.separator::before | Separator | extends accent background behind SVG flares |
.horizontal::before / .vertical::before | Separator | positions bleed per orientation |
.banner::before | Hideable | radial gradient overlay as separate layer; [data-hit]::before swaps it to solid var(--bg) |
.banner-zone::after | Details | rounded var(--bg) cap at bottom of accent zone |
done (step 5)
Added header comment blocks to both files.
Constants.ts — explains common_size as the single scale root, the three-tier pipeline (Constants → App.svelte injection → CSS vars → scoped styles), and the static-vs-inline rule.
Colors.ts — replaces the old one-liner with a structured explanation of the two tiers (static properties vs reactive stores), how each flows to CSS, and the rule that components must not import colors for CSS values — only App.svelte and canvas drawing code are valid direct consumers.