Skip to content

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.

  1. audit magic numbers — grep for hardcoded px values in <style> blocks. each one either gets pulled into Constants.ts or justified with a comment.
  2. 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.
  3. 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.
  4. pseudo-element review — look at each ::before/::after usage. some are genuine, others replaceable. decide case by case.
  5. document the contract — short comment block at top of Constants.ts and Colors.ts explaining what they provide and how components consume them.
  6. work on style.md
    1. 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):

constantCSS varvalueinstances
k.thickness.border--th-border0.5px25
k.corner.input--corner-input2px8
k.layout.gap_small--l-gap-small6px4
k.layout.padding_small--l-padding-small8px
k.layout.letter_spacing--l-letter-spacing0.5px3

remaining magic numbers

candidates for future tokens (appear in 3+ files):

valueused forfilescandidate
0 8px padding shorthandside padding on small buttons/cellsD_Preferences, D_Library, P_Angles, Graph--l-padding-small (needs manual handling)

justified geometry (component-specific, keep with comment when touched):

valueused forfile
-2px, -6px, 7px, -1.1pxslider thumb/track positioning mathSlider
10px, 14px, 3pxgraph overlay positions (breadcrumbs, assist, zoom)Graph
10pxface label font in canvas (smaller than common)Graph
50px, 120pxBuildNotes button widthsBuildNotes
81pxassist slider heightGraph
3px border-radiusinput-wrap, between --corner-input and --corner-boxP_Selected
1.5px heightsnap-line in controlsControls
0.25pxgradient stop hairlinesP_Attributes, P_Selected
-0.5px transformsub-pixel alignment tweakP_Repeat
0pxexplicit resetP_Repeat, Hideable

done (step 2)

new CSS vars injected in App.svelte onMount:

varsourcevalue
--c-whitecolors.background'white'
--c-blackcolors.default'black'
--c-focuscolors.focus'cornflowerblue'
--c-thumbcolors.thumb'#007aff'
--c-trackcolors.track'#ccc'
--c-slider-bordercolors.border'darkgray'
--focus-outlinecomputed'1.5px solid cornflowerblue'
--font-editconstant'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):

  • whitevar(--c-white) (~50 instances)
  • blackvar(--c-black) (~25 instances)
  • 1.5px solid cornflowerbluevar(--focus-outline) (5 files)
  • #007affvar(--c-thumb) / var(--c-thumb) fallback (D_Preferences, P_Repeat, Slider)
  • #cccvar(--c-track) (P_Angles)
  • darkgraycolors.border reference (Slider TS)
  • 12px sans-serifvar(--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-common CSS 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-row
  • flex: 1 → added to .range-slider
  • pointer-events: auto → new .sp-input class on the spacing range input
  • flex-shrink: 0, position: relative, top: -5.5px → new .fireblocks-btn modifier 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.

pseudofilepurpose
.snap-off::afterControlsdiagonal strikethrough over 🧲 when snap is off
.separator::beforeSeparatorextends accent background behind SVG flares
.horizontal::before / .vertical::beforeSeparatorpositions bleed per orientation
.banner::beforeHideableradial gradient overlay as separate layer; [data-hit]::before swaps it to solid var(--bg)
.banner-zone::afterDetailsrounded 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.