Spacing Tokens & Shared Primitives
Canonical reference for the spacing role-token layer and shared dialog/row/tree primitives introduced by the Spacing Rearchitecture epic (#3957). Distills the ratio doctrine, the new --spacing-* component tokens, the shared class constants, and the DialogCloseButton / treeIndentStyle primitives every consumer builds on.
Note
This page distills the doctrine locked in _(Sub #3961, Spacing Rearchitecture epic #3957) into a durable public reference. The token/primitive layer described here landed in wave 4 (#3962); site-by-site adoption across the six wave-5 rollout areas and the final _permanent.json lint sweep (wave 6, #3970) are tracked separately. See . for the quick-lookup token tables this page expands on.
Base scale — unchanged
The named spacing scale in packages/ is not touched by this epic:
0, 1px, 2xs=2, xs=4, sm=6, md=8, lg=12, xl=16, 2xl=20, 3xl=24, 4xl=32, 5xl=48Renaming or removing a step would force edits across ~3,000 existing usages. The governing rule for everything below: spend complexity only where the pixels are visibly broken — where a diagnosis found no visible defect, unify to one value and stop.
Display-scale contract
Every visual dimension must scale with --display-scale, with three fixed exceptions:
--spacing-1px/--spacing-tree-connector— hairlines, always 1px.--overlay-offset-top—vh, viewport-relative by design.--spacing-touch-minis fixed at 44px at every display scale.
Tokens.css declares every scaled token twice: the unscaled base lives in the @theme block (so Tailwind can generate the utility class), and the calc(Npx * var(--display-scale)) override lives in a later :root block (which wins the cascade at runtime). The split is load-bearing — @theme cannot carry var(--display-scale) — so both blocks must be updated together. A unit test (packages/) asserts the @theme and :root --spacing-* key sets match, modulo the fixed-token allowlist, as a durable guard against the "declared twice, forget one block" footgun.
Tailwind v4 token namespaces
Tailwind v4 uses supported prefixes declared in @theme to generate matching utility families. A declaration in @theme does not make every custom property a utility: special settings and custom properties such as --z-* remain values for var() or app code. Runtime color values can live in :root or mode-specific rules while a --color-* utility alias points to them through var(). This namespace normalization preserves the before computed styles; the locked deliberate visible-change list is empty.
| Prefix | Generates | Current role |
|---|---|---|
--spacing-* | Spacing, sizing, and gap utilities such as p-md, w-icon-md, and gap-lg | Named layout scale and role tokens. Scaled values are overridden in :root; --spacing-touch-min stays fixed at 44px. The restored --spacing: 0.25rem base powers numeric utilities such as p-4 (16px). |
--color-* | Color utilities such as bg-bg, text-fg, and border-edge | Tier 3 utility aliases to runtime --theme-* values. |
--text-* | Font-size utilities such as text-sm and text-base | Project size scale; runtime :root values apply display scale. There is no --text-2xs. |
--font-* | Font-family utilities such as font-sans, font-mono, and font-serif | Project sans, mono, and restored inherited serif stacks. |
--font-weight-* | Font-weight utilities such as font-normal and font-semibold | The five project weights from 300 through 700. |
--leading-* | Line-height utilities such as leading-tight, leading-snug, and leading-content-tight | Utility ratios stay pinned to Tailwind's prior values: tight 1.25, normal 1.5, relaxed 1.625, and snug 1.375. The - values separately retain 1/1.5/1.75 for existing direct CSS var() consumers; those names can also compile as leading-content-* utilities. |
--tracking-* | Letter-spacing utilities such as tracking-wide | Restored inherited ratios from -0.05em through 0.1em; values remain font-relative and are not display-scaled. |
--radius-* and --radius | Border-radius utilities such as rounded, rounded-md, and rounded-3xl | Bare and extended radius values are restored alongside the existing project sizes. Radii are not display-scaled. |
--shadow-* | Box-shadow utilities for names declared in the namespace | The available project names are dialog, floating-panel, and popover; the generic Tailwind shadow scale is reset. |
--inset-shadow-*, --drop-shadow-* | No project utilities | These namespaces are reset with no project values. |
--breakpoint-* | Responsive variants such as mobile: and max-mobile: | The mobile breakpoint is 640px. max-mobile: has an exclusive upper boundary; product viewport queries keep their inclusive 640px rule. |
--container-* | Container-size utilities, including max-w-* resolution | Tailwind's imported container scale supplies named container sizes. |
--max-width-* | Compiler-supported max-w-* lookup | Compatibility aliases to --container-*, not a core theme namespace; see the collision note below. |
--animate-* | Animation utilities such as animate-toast-slide-in | Named app animations. |
--default-transition-* | No named utility by itself | Tailwind special settings for default transition duration and timing. |
--palette-*, --theme-*, --cold-* | No utility directly | Runtime palette, semantic colors, and cold-start fallbacks. --color-* aliases expose the intended colors to Tailwind. |
--z-*, --dialog-*, --display-scale, --duration-*, --overlay-*, --safe-*, --status-*, --toolbar-* | No utility directly | Runtime layering, viewport geometry, scale, motion, overlay, and native safe-area values consumed with CSS var() or by app code. Use z-[var(--z-…)] for a token-tier utility; z-auto remains a static utility. |
Names such as --font-family-*, --line-height-*, --letter-spacing-*, and --border-radius-* are not the Tailwind v4 namespaces for those utilities. Use --font-*, --leading-*, --tracking-*, and --radius-*. Direct content ratios use --leading-content-*; the old --line-height-* properties have been removed.
Why --max-width-* aliases remain
Tailwind's installed max-w-<name> lookup checks --max-width-* before the same-name --spacing-* fallback and --container-*. Since this project has --spacing-md: 8px, removing --max-width-md: var(--container-md) would make max-w-md resolve to 8px instead of the imported 28rem container size. Keep all ten --max-width-2xs through --max-width-5xl aliases pointed at their matching --container-* values to preserve the intended widths.
Tailwind utility emission guard (#7046)
Run pnpm check:tailwind-utilities from the repository root. The guard checks emitted semantics for bare rounded and shadow, plus the rounded-*, leading-*, tracking-*, shadow-*, text-*, max-w-*, font-*, and z-* candidates extracted from production .js, .jsx, .ts, and .tsx under tauri-app/renderer/** and packages/*/src/**. It excludes dependencies, build output, tests, stories, fixtures, generated output, and declaration files. Candidate extraction covers class props, class strings, helper calls, and class-bearing object properties, including supported negative utility forms. It also checks all original @theme declarations in packages/ and packages/ for the legacy --line-height-*, --letter-spacing-*, --border-radius-*, and --font-family-* prefixes, even when a declaration is unused.
Current production candidates must emit CSS. Historical candidates are compared with styleguide/, and the guard verifies SHA-256 provenance for that compile baseline and the immutable token-computed-baseline.json against token-namespace-decisions.json. It also rejects production leading-* utilities that resolve through --spacing-*, and requires max-w-md to resolve through --container-md. The leading-md probe is deliberately kept outside production source so it can demonstrate the 8px spacing fallback as a diagnostic.
This comparison permits a new candidate when Tailwind emits it and it passes the collision checks, and a historical candidate whose final source use was removed. It does not freeze candidate or occurrence counts. The 43 dead-class removals and 65 z-index replacements in the migration audit are one-time migration counts, not ongoing guard targets.
Ratio doctrine — two interleaved ×2 ladders
The base scale contains two clean doubling ladders plus two off-ladder fine-tuning steps:
Ladder A (×2): xs 4 → md 8 → xl 16 → 4xl 32
Ladder B (×2): sm 6 → lg 12 → 3xl 24 → 5xl 48
Off-ladder: 2xs 2 (below the ladders), 2xl 20 (between xl 16 and 3xl 24)The rule, applied by review + the squint test (lint cannot see ratio): at any grouping boundary, pick the within-group gap and the between-group gap from the same ladder, with the between-group gap at least one rung higher (exactly 2×) than the within-group gap. Adjacent-step pairings (xs↔sm, sm↔md, md↔lg, lg↔xl, xl↔2xl, 2xl↔3xl, 3xl↔4xl) read as one group and are the exact failure the initial diagnosis measured on the timeline view. Off-ladder steps (2xs, 2xl) are for absolute fitting — never a within/between grouping partner against a neighbour.
North-stars validated by the diagnosis: kanban card 6↔26 (4.3×), settings body 10↔28 (2.8×). New groupings should land in the 2×–3× band.
Semantic role-token layer
Two delivery mechanisms, chosen per role:
New
--spacing-*component tokens — for roles whose value is not on the base scale (28, 22, 44, 3, a 16px gap) or that need independent scaling behavior (a touch floor, a rounded/floored bar).Shared class constants — for roles that are a pairing of existing base-scale tokens (a header is px+py+border). A single CSS var can't express a padding shape, but a named class can.
New --spacing-* tokens (both @theme and :root, unless FIXED)
| Token | Base | :root scaled value | Replaces |
|---|---|---|---|
--spacing-close-btn | 28px | calc(28px * var(--display-scale)) | ~19–35 copy-pasted w-[28px] h-[28px] close-X sites |
--spacing-hint-indent | 28px | calc(28px * var(--display-scale)) | ~25 ml-[28px] settings hint indents |
--spacing-accent-bar | 3px | max(2px, round(3px * var(--display-scale), 1px)) | 3px priority stripe / drag pill / insertion indicator |
--spacing-icon-btn-mini | 22px | calc(22px * var(--display-scale)) | table-helper 22px mini buttons; shortcut remove-button 24→22 |
--spacing-card-gap | 16px | calc(16px * var(--display-scale)) | gap between bordered board cards (timeline/pile) |
--spacing-section-gap | 28px | calc(28px * var(--display-scale)) | between-group boundary — names the settings 10↔28 (2.8×) north-star |
--spacing-tree-indent-base | 12px | calc(12px * var(--display-scale)) | tree depth-0 content indent |
--spacing-tree-indent-step | 16px | calc(16px * var(--display-scale)) | tree per-depth increment |
--spacing-tree-connector | 1px | (FIXED — declared once, no override) | tree guide-line width |
--spacing-touch-min | 44px | 44px | raw 44/56 hit areas (iOS HIG floor) |
--overlay-offset-top | (:root only, not a spacing utility) | 10vh (FIXED, vh) | modal overlay vertical anchor |
Notes on the trickier ones:
--spacing-accent-barfloors at 2px so the bar never goes sub-visible at 0.75× (round(2.25) = 2before the floor even engages) and stays a crisp integer width at every other scale. JS-computed bars (drag pill, spreadsheet insertion indicators) mirror the same floor:Math.max(2, Math.round(3 * scale)); centering offsets derive from the token (calc(var(--spacing-accent-bar) / -2)/-), never a hardcodedMath. round(bar / 2) ±1.5.--spacing-touch-minis a physical-ergonomics constant (44px, iOS HIG), and remains 44px at 0.75×, 1×, 1.5× and 2×.--spacing-icon-btn-mini(22) is distinct from the existing--spacing-frame-ctrl-btn(also 22, frame chrome) — different roles, do not rename or merge.--spacing-hint-indentkeeps the shipped 28px (checkbox 16 + gap-md 8 + 4pxoptical fudge) rather than "correcting" it to 24 — the 4px fudge is intentional and already the established rendered position.--spacing-section-gapnames the existing settings rhythm; adopting it must not shift the rendered value off 28.--overlay-offset-topunifies command-palette/spotlight/item-detail from 5/10/20vh onto the middle value, 10vh. It has no@themeentry — it's not a Tailwind spacing utility, so it's declared:root-only.
Shared class constants (@takazudo/ui-components)
| Constant | Classes | Role |
|---|---|---|
dialogHeaderCls | flex items-center justify-between px-3xl py-xl border-b border-edge | Dialog header chrome |
dialogBodyCls | px-3xl py-3xl | Dialog body chrome |
dialogFooterCls | flex items-center justify-end gap-md px-3xl py-xl border-t border-edge | Dialog footer chrome |
menuItemCls | px-md py-xs max-mobile:min-h-touch-min | Tier A floating-menu row |
pickerRowCls | px-lg py-sm min-h-touch-min | Tier B search-overlay row |
toolbarBarCls | flex items-center gap-sm px-lg py-sm | Secondary board/control bar |
chipPadCls | px-sm py-2xs | Chip/pill/badge padding |
bannerRowCls | px-lg py-sm mb-md bg-bg-alt | Sub-header banner row |
hintIndentCls | ml-hint-indent | Settings hint indentation |
Dialog inset is single, not size-tiered. All three dialog-chrome constants share px-3xl (24) so every dialog's left/right chrome computes equal at every scale. This is the dialog content inset — a different role from the existing --dialog-inset (20, the big-dialog viewport-edge margin via --spacing-2xl), which is unchanged.
Row soup collapses to 2 tiers, not 3. menuItemCls (context-menu, dropdown-listbox, kebab, font-picker, board/spreadsheet menus) and pickerRowCls (spotlight, command-palette) are the only two floating-row densities app-wide. A third "standard listbox" tier was considered and rejected — it would reintroduce the soft menu-vs-listbox ambiguity the consolidation is meant to remove.
Card grouping (--spacing-card-gap) is a figure/ground fix, not a gap-ratio fix. The timeline/pile "cards fusing into one list" bug comes from a transparent background + a single 1px border between cards, not from an insufficient content-line gap. The fix gives each card a filled bg-surface + a full border border-edge rounded-md (all sides) plus --spacing-card-gap (16) between cards — the same recipe the kanban card already uses. Card padding stays density-tiered (kanban p-md=8 vs. timeline/pile p-lg=12) — it is not unified.
Shared primitives
<DialogCloseButton>
The close (X) button for a dialog header. Renders a w-close-btn h-close-btn box (backed by --spacing-close-btn) with the glyph inside, and follows the Close-X top-right rule (root CLAUDE.md § Adding a New Dialog — every dialog must render a close button so users can always dismiss it).
import { DialogCloseButton, dialogHeaderCls } from "@takazudo/ui-components";
<div className={dialogHeaderCls}>
<span>Dialog title</span>
<DialogCloseButton onClick={onClose} />
</div>The glyph's text-[20px] size intentionally stays an unscaled arbitrary value, matching every existing close-X site — font-scale unification for this glyph is out of this epic's scope.
treeIndentStyle(depth, scale)
The one tree-indent/connector formula every tree surface should converge on (file-tree, workspace-dir-tree, settings-sidebar-tree, directory-tree-picker, mindmap outline-view — wave 6, #3969). Backed by --spacing-tree-indent-base (12) and --spacing-tree-indent-step (16):
import { treeIndentStyle } from "@takazudo/ui-components";
const { paddingLeft, connectorLeft } = treeIndentStyle(depth, displayScale);paddingLeft = Math.round((12 + 16 * depth) * scale)connectorLeft = Math.round((12 + 16 * depth − 8) * scale)
Rounding happens here, in JS, per depth — not via CSS calc() — because fractional display scales (0.9, 1.1, 1.25, 1.75) leave the raw product on a sub-pixel value at some depths (e.g. 44 * 0.9 = 39.6), which would drift rows out of pixel alignment across a tall tree. This and the accent-bar JS floor above are the only two places rounding is applied in code; every other token relies on the browser's own calc().
Where this doctrine came from
The full ranked-problem diagnosis (timeline grouping, menu-row soup, command-palette over-roominess, header-pins divider ambiguity, sub-header banner weak bind, overlay vertical anchor drift), the per-wave-5-area migration mapping, and the wave-7 computed-style verification predicates all live in _ (epic #3957). That file is the working spec for the epic's remaining waves; this page is the durable summary for anyone landing here after the epic closes.