zudo-text

検索したい単語を入力

いつでも検索バーを開ける

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 _temp-resource/3957-spacing-rearchitecture/spacing-spec.md(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 .claude/skills/l-design-system/SKILL.md for the quick-lookup token tables this page expands on.

Base scale — unchanged

The named spacing scale in packages/ui-components/src/tokens.css 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=48

Renaming 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-min is 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/ui-components/src/__tests__/tokens-spacing-role-parity.test.ts) 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.

PrefixGeneratesCurrent role
--spacing-*Spacing, sizing, and gap utilities such as p-md, w-icon-md, and gap-lgNamed 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-edgeTier 3 utility aliases to runtime --theme-* values.
--text-*Font-size utilities such as text-sm and text-baseProject 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-serifProject sans, mono, and restored inherited serif stacks.
--font-weight-*Font-weight utilities such as font-normal and font-semiboldThe five project weights from 300 through 700.
--leading-*Line-height utilities such as leading-tight, leading-snug, and leading-content-tightUtility ratios stay pinned to Tailwind's prior values: tight 1.25, normal 1.5, relaxed 1.625, and snug 1.375. The --leading-content-tight/normal/relaxed 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-wideRestored inherited ratios from -0.05em through 0.1em; values remain font-relative and are not display-scaled.
--radius-* and --radiusBorder-radius utilities such as rounded, rounded-md, and rounded-3xlBare 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 namespaceThe available project names are dialog, floating-panel, and popover; the generic Tailwind shadow scale is reset.
--inset-shadow-*, --drop-shadow-*No project utilitiesThese 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-* resolutionTailwind's imported container scale supplies named container sizes.
--max-width-*Compiler-supported max-w-* lookupCompatibility aliases to --container-*, not a core theme namespace; see the collision note below.
--animate-*Animation utilities such as animate-toast-slide-inNamed app animations.
--default-transition-*No named utility by itselfTailwind special settings for default transition duration and timing.
--palette-*, --theme-*, --cold-*No utility directlyRuntime 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 directlyRuntime 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/ui-components/src/tailwind.css and packages/ui-components/src/tokens.css 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/migration/token-emission-baseline.json, 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)

TokenBase:root scaled valueReplaces
--spacing-close-btn28pxcalc(28px * var(--display-scale))~19–35 copy-pasted w-[28px] h-[28px] close-X sites
--spacing-hint-indent28pxcalc(28px * var(--display-scale))~25 ml-[28px] settings hint indents
--spacing-accent-bar3pxmax(2px, round(3px * var(--display-scale), 1px))3px priority stripe / drag pill / insertion indicator
--spacing-icon-btn-mini22pxcalc(22px * var(--display-scale))table-helper 22px mini buttons; shortcut remove-button 24→22
--spacing-card-gap16pxcalc(16px * var(--display-scale))gap between bordered board cards (timeline/pile)
--spacing-section-gap28pxcalc(28px * var(--display-scale))between-group boundary — names the settings 10↔28 (2.8×) north-star
--spacing-tree-indent-base12pxcalc(12px * var(--display-scale))tree depth-0 content indent
--spacing-tree-indent-step16pxcalc(16px * var(--display-scale))tree per-depth increment
--spacing-tree-connector1px(FIXED — declared once, no override)tree guide-line width
--spacing-touch-min44px44pxraw 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-bar floors at 2px so the bar never goes sub-visible at 0.75× (round(2.25) = 2 before 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) / -Math.round(bar / 2)), never a hardcoded ±1.5.

  • --spacing-touch-min is 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-indent keeps the shipped 28px (checkbox 16 + gap-md 8 + 4px optical fudge) rather than "correcting" it to 24 — the 4px fudge is intentional and already the established rendered position.

  • --spacing-section-gap names the existing settings rhythm; adopting it must not shift the rendered value off 28.

  • --overlay-offset-top unifies command-palette/spotlight/item-detail from 5/10/20vh onto the middle value, 10vh. It has no @theme entry — it's not a Tailwind spacing utility, so it's declared :root-only.

Shared class constants (@takazudo/ui-components)

ConstantClassesRole
dialogHeaderClsflex items-center justify-between px-3xl py-xl border-b border-edgeDialog header chrome
dialogBodyClspx-3xl py-3xlDialog body chrome
dialogFooterClsflex items-center justify-end gap-md px-3xl py-xl border-t border-edgeDialog footer chrome
menuItemClspx-md py-xs max-mobile:min-h-touch-minTier A floating-menu row
pickerRowClspx-lg py-sm min-h-touch-minTier B search-overlay row
toolbarBarClsflex items-center gap-sm px-lg py-smSecondary board/control bar
chipPadClspx-sm py-2xsChip/pill/badge padding
bannerRowClspx-lg py-sm mb-md bg-bg-altSub-header banner row
hintIndentClsml-hint-indentSettings 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 _temp-resource/3957-spacing-rearchitecture/spacing-spec.md (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.