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-topvh, viewport-relative by design.

  • The floor portion of --spacing-touch-min (44px never shrinks below the floor, even at 0.75×).

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.

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-min44pxmax(44px, calc(44px * var(--display-scale)))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-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), so max() makes it an absolute floor that only grows: 0.75×→44, 1.0×→44, 1.5×→66, 2.0×→88.

  • --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.