@qijenchen/design-system 0.1.0-beta.20 → 0.1.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -60,6 +60,7 @@ export * from './components/TimePicker/index';
60
60
  export * from './components/Toast/index';
61
61
  export * from './components/Tooltip/index';
62
62
  export * from './components/TreeView/index';
63
+ export * from './patterns/element-anatomy/index';
63
64
  export * from './patterns/horizontal-overflow/index';
64
65
  export * from './patterns/overlay-surface/index';
65
66
  export * from './patterns/resize-handle/index';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,8BAA8B,CAAA;AAC5C,cAAc,0BAA0B,CAAA;AACxC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,2BAA2B,CAAA;AACzC,cAAc,0BAA0B,CAAA;AACxC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA;AACzC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,0BAA0B,CAAA;AACxC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,qCAAqC,CAAA;AACnD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,oCAAoC,CAAA;AAClD,cAAc,2BAA2B,CAAA;AACzC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,0BAA0B,CAAA;AACxC,cAAc,0BAA0B,CAAA;AACxC,cAAc,sCAAsC,CAAA;AACpD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,0BAA0B,CAAA;AACxC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,sCAAsC,CAAA;AACpD,cAAc,iCAAiC,CAAA;AAC/C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qCAAqC,CAAA;AACnD,cAAc,2BAA2B,CAAA;AACzC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qCAAqC,CAAA;AACnD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,0BAA0B,CAAA;AACxC,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAG3C,cAAc,sCAAsC,CAAA;AACpD,cAAc,kCAAkC,CAAA;AAChD,cAAc,gCAAgC,CAAA;AAG9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,4BAA4B,CAAA;AAG1C,cAAc,mBAAmB,CAAA;AACjC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,8BAA8B,CAAA;AAC5C,cAAc,0BAA0B,CAAA;AACxC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,2BAA2B,CAAA;AACzC,cAAc,0BAA0B,CAAA;AACxC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA;AACzC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,0BAA0B,CAAA;AACxC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,qCAAqC,CAAA;AACnD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,oCAAoC,CAAA;AAClD,cAAc,2BAA2B,CAAA;AACzC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,0BAA0B,CAAA;AACxC,cAAc,0BAA0B,CAAA;AACxC,cAAc,sCAAsC,CAAA;AACpD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,0BAA0B,CAAA;AACxC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,sCAAsC,CAAA;AACpD,cAAc,iCAAiC,CAAA;AAC/C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qCAAqC,CAAA;AACnD,cAAc,2BAA2B,CAAA;AACzC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,qCAAqC,CAAA;AACnD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,2BAA2B,CAAA;AACzC,cAAc,0BAA0B,CAAA;AACxC,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,0BAA0B,CAAA;AACxC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,6BAA6B,CAAA;AAM3C,cAAc,kCAAkC,CAAA;AAChD,cAAc,sCAAsC,CAAA;AACpD,cAAc,kCAAkC,CAAA;AAChD,cAAc,gCAAgC,CAAA;AAG9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,4BAA4B,CAAA;AAG1C,cAAc,mBAAmB,CAAA;AACjC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -60,6 +60,7 @@ import { TimePicker, timePickerMeta } from "./components/TimePicker/time-picker.
60
60
  import { Toaster, toast, toastMeta } from "./components/Toast/toast.js";
61
61
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, tooltipMeta } from "./components/Tooltip/tooltip.js";
62
62
  import { TreeItem, TreeView, treeItemVariants, treeViewMeta } from "./components/TreeView/tree-view.js";
63
+ import { AVATAR_SIZE, ICON_SIZE, INLINE_ACTION_HOVER_BG_SIZE, ItemAvatar, ItemContent, ItemIcon, ItemInlineAction, ItemInlineActionButton, ItemLabel, ItemPrefix, ItemSuffix, ROW_PADDING_BY_SIZE, RowSizeProvider, getUniformPrefixSlotStyle, itemPrefixAlignVariants, useRowSize } from "./patterns/element-anatomy/item-anatomy.js";
63
64
  import { ARROW_BUTTON_WIDTH, FADE_WIDTH, OverflowMenuTriggerButton, OverflowScrollArrow, SCROLL_PAGE_RATIO, buildFadeMask, useScrollByPage } from "./patterns/horizontal-overflow/horizontal-overflow.js";
64
65
  import { SurfaceBody, SurfaceFooter, SurfaceHeader } from "./patterns/overlay-surface/overlay-surface.js";
65
66
  import { ResizeHandle } from "./patterns/resize-handle/resize-handle.js";
@@ -73,6 +74,7 @@ import { cn } from "./lib/utils.js";
73
74
  import { PEOPLE_PICKER_LENGTH1_WRAPPER_CLASS, getPeoplePickerTagWrapperClass } from "./components/PeoplePicker/people-picker-helpers.js";
74
75
  export {
75
76
  ARROW_BUTTON_WIDTH,
77
+ AVATAR_SIZE,
76
78
  Accordion,
77
79
  AccordionContent,
78
80
  AccordionItem,
@@ -168,7 +170,17 @@ export {
168
170
  HoverCard,
169
171
  HoverCardContent,
170
172
  HoverCardTrigger,
173
+ ICON_SIZE,
174
+ INLINE_ACTION_HOVER_BG_SIZE,
171
175
  Input,
176
+ ItemAvatar,
177
+ ItemContent,
178
+ ItemIcon,
179
+ ItemInlineAction,
180
+ ItemInlineActionButton,
181
+ ItemLabel,
182
+ ItemPrefix,
183
+ ItemSuffix,
172
184
  LinkInput,
173
185
  MIN_COLUMN_WIDTH,
174
186
  MenuFooter,
@@ -194,10 +206,12 @@ export {
194
206
  PopoverTitle,
195
207
  PopoverTrigger,
196
208
  ProgressBar,
209
+ ROW_PADDING_BY_SIZE,
197
210
  RadioGroup,
198
211
  RadioGroupItem,
199
212
  Rating,
200
213
  ResizeHandle,
214
+ RowSizeProvider,
201
215
  SCROLL_PAGE_RATIO,
202
216
  SUBTLE_ICON_COLOR,
203
217
  ScrollArea,
@@ -312,9 +326,11 @@ export {
312
326
  formatDate,
313
327
  formatNumber,
314
328
  getPeoplePickerTagWrapperClass,
329
+ getUniformPrefixSlotStyle,
315
330
  hoverCardMeta,
316
331
  inputMeta,
317
332
  isReorderNoop,
333
+ itemPrefixAlignVariants,
318
334
  linkInputMeta,
319
335
  menuItemMeta,
320
336
  menuItemVariants,
@@ -369,6 +385,7 @@ export {
369
385
  useIsNarrowViewport,
370
386
  useIsTouchDevice,
371
387
  useOverflowIndices,
388
+ useRowSize,
372
389
  useScrollByPage,
373
390
  useScrollEdges,
374
391
  useSidebar
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qijenchen/design-system",
3
- "version": "0.1.0-beta.20",
3
+ "version": "0.1.0-beta.22",
4
4
  "private": false,
5
5
  "description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
6
6
  "type": "module",
@@ -610,12 +610,12 @@ Sheet 開啟狀態**不持久化**。
610
610
 
611
611
  | Token | 預設值 | 用途 |
612
612
  |-------|--------|------|
613
- | `--sidebar-width` | `17rem` | 展開寬度 |
613
+ | `--sidebar-width` | `15rem` (240px) | 展開寬度(2026-05-21 v11 user 拍板 240px;對齊 Linear/Notion/Figma/Polaris/Material Drawer 主流 240–256,5/8 家 = 240。也是 `--sidebar-width-min` AppShell Aside floor 共識值)|
614
614
  | `--sidebar-width-icon` | `calc(2 * var(--layout-space-loose) + var(--sidebar-menu-icon-size))` | icon 模式寬度(2026-05-21 v3 撤回 `3rem` hardcode,改 geometry formula 保證 icon center x = loose + icon/2 在展開/收合一致;md=48 / lg=64,跟 chrome-header-height 解耦)|
615
615
  | `--sidebar-menu-icon-size` | `1rem` (16px) | sidebar menu icon 大小(per ICON_SIZE.sm/md=16);size=lg 罕見 case override `1.25rem` |
616
616
  | `--sidebar-width-mobile` | `18rem` | Mobile sheet 寬度 |
617
617
 
618
- **用 rem**:跟隨 root font-size,支援 accessibility 等比例縮放。**不用 layoutSpace / uiSize 推導**:sidebar width 是容器維度,不是間距或元件內高度,語意不同;耦合會讓寬度跟間距連動。**16rem / 3rem / 18rem** shadcn + 多數 SaaS 的事實標準,不要為獨特而改。
618
+ **用 rem**:跟隨 root font-size,支援 accessibility 等比例縮放。**不用 layoutSpace / uiSize 推導**:sidebar width 是容器維度,不是間距或元件內高度,語意不同;耦合會讓寬度跟間距連動。**15rem / 18rem** 對齊 Linear/Notion/Figma/Polaris/Material Drawer 240–288 主流寬度,`--sidebar-width-icon` 用 geometry formula(2026-05-21 v3)`calc(2*loose + icon-size)` 保證 collapse/expand icon center x 一致。
619
619
 
620
620
  ---
621
621
 
package/src/index.ts CHANGED
@@ -66,6 +66,10 @@ export * from './components/Tooltip/index'
66
66
  export * from './components/TreeView/index'
67
67
 
68
68
  // ─── Patterns ─────────────────────────────────────────────────────────────
69
+ // 2026-05-27 fix(per user 抓 Avatar drift):element-anatomy 漏 public export →
70
+ // consumer 被迫用 <Avatar size=32(default)> 代替 <ItemAvatar size=24(scanning row)> → 8px drift
71
+ // Note:action-bar 為 spec-only pattern(無實作 tsx),無 index 可 export
72
+ export * from './patterns/element-anatomy/index'
69
73
  export * from './patterns/horizontal-overflow/index'
70
74
  export * from './patterns/overlay-surface/index'
71
75
  export * from './patterns/resize-handle/index'
@@ -1,5 +1,7 @@
1
1
  # tokens/ Charter
2
2
 
3
+ > **跨 family canonical SSOT** → `token-system.spec.md`(5-layer 架構 / 命名 family-scoped 規則 / token vs hardcode 判斷 / co-location rationale)。每個 family spec.md 只 codify 自家具體規則,本 README 列 family 居住地圖,SSOT 集中在 `token-system.spec.md` 避免 drift。
4
+
3
5
  ## 這裡只收:design token 定義 + spec + stories
4
6
 
5
7
  每個 token 類別一個 folder:
@@ -0,0 +1,243 @@
1
+ <!-- @benchmark-cited: world-class layered token architecture — Polaris/Material/Atlassian/Carbon/Apple HIG (URLs in body table) -->
2
+
3
+ # Token System 設計原則(SSOT,跨所有 token family)
4
+
5
+ > **Foundational SSOT rationale**(cap 1200,per CLAUDE.md「foundational SSOT 例外 ≤ 800-1200」):
6
+ > 本 spec 是**所有 token family 共同遵循的上游 canonical**——5-layer 架構、命名 family-scoped 規則、token vs hardcode 判斷、cross-family co-location rationale、SSOT consumer scope。改變本 spec = 影響全 DS 30+ 元件 + 200+ token。改一處不對齊就 drift,所以集中在這裡為 SSOT;個別 family spec(color / typography / uiSize 等)只 codify 自家具體規則,不重述上游架構。
7
+
8
+ ## 架構流派定位
9
+
10
+ 業界世界級 DS 都採「**分層 token 架構**」(layered token architecture),底層 raw value,上層 semantic intent。各家命名不同但概念一致:
11
+
12
+ | 流派 | 代表系統 | 層命名 | 對應我們 |
13
+ |---|---|---|---|
14
+ | **3-layer reference / system / component** | Material 3 | `md-ref-*` → `md-sys-*` → `md-comp-*` | Primitive / Semantic / Internal |
15
+ | **2-layer base / semantic + token-per-component** | Polaris | `--p-color-blue-500` → `--p-color-bg-primary` | Primitive / Semantic |
16
+ | **3-layer foundation / semantic / specific** | Atlassian DS | `color.text.brand` → 直接 raw oklch in foundation | Primitive / Semantic |
17
+ | **2-layer global / alias + size scale** | Carbon | `$blue-60` → `$button-primary` | Primitive / Semantic |
18
+ | **2-layer semantic + dynamic** | Apple HIG | `systemBlue` → `UIColor.label`(adaptive) | Primitive / Semantic |
19
+
20
+ **我們選 5-layer**(Primitive / Semantic / Family / Layout / Internal),擴充 Material 3 流派加兩層處理結構化常數:
21
+ - **Family**(Material 3 `md-sys-*` 子分支):cross-component 共用的具體尺寸 family(`--field-height-*` / `--table-row-*` / `--tab-height-*`)
22
+ - **Layout**(Material `md-comp-layout` 對應):跨層佈局常數(`--layout-space-loose` / `--sidebar-width` / `--chrome-header-height`)
23
+ - **Internal**(Material `md-comp-*` 對應):單元件內部不對 consumer 公開(目前 0 個——所有跨元件都 promote 到 Family,單元件 hardcode in tsx)
24
+
25
+ `tokens/README.md`「Public vs Internal token」表 cross-reference 本 spec 此段。
26
+
27
+ ---
28
+
29
+ ## 5-Layer 架構
30
+
31
+ | Layer | 抽象度 | 例子 | 居住檔案 | Consumer scope |
32
+ |---|---|---|---|---|
33
+ | **L1 Primitive** | 原始 raw value(無 mode 知識 / 無 component 知識) | `--color-blue-6` / `--color-neutral-9-opaque` / `--black-a45` / `--elevation-100` | `color/primitives.css` | **Internal**——只供 semantic 層 alias,或 Tag/Avatar 等「按色分類」元件直接消費(per `color.spec.md` 流派定位) |
34
+ | **L2 Semantic** | 表達**意圖** / 封裝 mode swap | `--primary` / `--error` / `--canvas` / `--surface` / `--foreground` / `--fg-disabled` / `--bg-disabled` | `color/semantic.css` | **Public**——consumer 直接 `bg-primary` / `text-foreground` |
35
+ | **L3 Family**(cross-component 共用尺寸) | family-scoped semantic value | `--field-height-md` / `--table-row-md` / `--tab-height-md` / `--tree-indent-md` | `uiSize/uiSize.css` | **Internal**——DS 內部 primitive 消費,consumer 用 `<Button size="sm">` 不直接寫 token |
36
+ | **L4 Layout**(全 app 結構常數) | 全域 layout primitive 尺寸 | `--sidebar-width` / `--sidebar-width-mobile` / `--chrome-header-height` / `--layout-space-loose` | `uiSize/uiSize.css` + `layoutSpace/layoutSpace.css` | **Public**——AppShell / Sidebar / ChromeHeader 等 layout primitive 元件直接消費 |
37
+ | **L5 Internal**(單元件內部) | 單元件 only | (目前 0 個) | — | 不開——若只 1 個 consumer hardcode in tsx 即可,2+ consumer 才升 L3/L4 |
38
+
39
+ **Layer 判斷流程**:
40
+
41
+ ```
42
+ 新增值 / 公式 / 結構常數時 ──→
43
+ 是 raw color/shadow/alpha 沒 component 意圖? ─→ L1 Primitive
44
+ 是「意圖」/ semantic state? ─────────────────→ L2 Semantic(必經 L1 alias)
45
+ 跨 component(Button/Input/Field 等都吃)? ──→ L3 Family
46
+ 全 app 結構(AppShell / Header / Sidebar)? ─→ L4 Layout
47
+ 單 component 內部? ─────────────────────────→ 不開 token,hardcode in tsx
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Naming Convention(family-scoped,3 條硬規則)
53
+
54
+ ### 規則 1:`--<family>-<measurement>[-<size>]` 嚴格 family-scoped
55
+
56
+ 每個 token name 一定先帶 family prefix,讓 grep 即知「這 token 屬哪 family / 哪 layer」:
57
+
58
+ | Family prefix | Layer | 範例 |
59
+ |---|---|---|
60
+ | `--color-*` | L1 Primitive | `--color-blue-6` / `--color-neutral-9-opaque` |
61
+ | `--brand` / `--primary` / `--error` / `--info` / `--success` / `--warning` | L2 Semantic(色 family,history 沒寫 `--color-` 前綴是 Atlassian 流派 idiom) | `--primary-hover` / `--error-subtle` |
62
+ | `--canvas` / `--surface*` / `--foreground` / `--fg-*` | L2 Semantic(shadcn idiom 沿用——surface tone family + fg state family 各自獨立) | `--canvas` / `--surface-raised` / `--fg-disabled` |
63
+ | `--bg-*` | L2 Semantic(state pair family,跟 `--fg-*` 配對) | `--bg-disabled`(配 `--fg-disabled`) |
64
+ | `--neutral-hover` / `--neutral-selected*` | L2 Semantic(neutral interaction family) | `--neutral-selected-hover` |
65
+ | `--<hue>-hover` / `--<hue>-active` | L2 Semantic(色相互動 family) | `--blue-hover` / `--red-active` |
66
+ | `--font-<role>-<measurement>` | L3 Family | `--font-h1-size` / `--font-body-size` |
67
+ | `--field-height-<size>` / `--table-row-<size>` / `--tab-height-<size>` / `--tree-indent-<size>` | L3 Family | `--field-height-md` |
68
+ | `--<part>-width` / `--<part>-height` / `--<part>-<measurement>` | L4 Layout | `--sidebar-width` / `--chrome-header-height` |
69
+ | `--layout-space-<role>` | L4 Layout | `--layout-space-loose` / `--layout-space-tight` |
70
+ | `--radius-<size>` | L3 Family(structural) | `--radius-md` |
71
+ | `--elevation-<step>[-hover]` | L1 Primitive(住 primitives.css 跟色一起,理由見下) | `--elevation-100` / `--elevation-200-hover` |
72
+ | `--opacity-<role>` | L1 Primitive(structural,單一值) | `--opacity-disabled` |
73
+ | `--<part>-<measurement>-<size>` | L4 Layout(panel widths 等)| `--data-table-sort-panel-width` |
74
+
75
+ ### 規則 2:`--<size>` suffix 跟 family 共用 size vocabulary
76
+
77
+ 所有 size suffix 統一 `xs / sm / md / lg`(elevation 例外用 `100 / 200` Material idiom,因 elevation 不跟 density 走)。**禁** 自創 `tiny` / `small` / `regular` / `large` 等同義詞。
78
+
79
+ ### 規則 3:Tailwind utility bridge 命名同步
80
+
81
+ 每個 token 若提供 utility(`text-h1` / `bg-primary` / `h-field-md`)必經 `@theme inline` 或 `@utility`,utility 名 = token name 去 `--` 前綴 + 必要的 utility prefix(`bg-` / `text-` / `h-`)。**禁** 在 bridge 中改名(`--font-body-size` 不可橋成 `text-paragraph`)。
82
+
83
+ ---
84
+
85
+ ## Token vs Hardcode 判斷
86
+
87
+ **標準:同值在 ≥ 2 處需要保持同步 → 必開 token。** 1 處 = hardcode + 註解 why(若非常識值);2+ 處 = token。
88
+
89
+ ### Token 化必過 3 題
90
+
91
+ 1. **這值會在 ≥ 2 處同步出現嗎?**(2 處不一定要 sync 也不算——須有「同步義務」,例如「Button 跟 Input 必同高」)
92
+ 2. **找得到既有 family 可鏡射嗎?**(命中 → 用既有 family 加 size variant;沒命中 → 走規則 1 命名,別自創孤立 family)
93
+ 3. **改值時所有 consumer 都該跟著變嗎?**(YES → token;NO → hardcode 因為各 consumer 各自意圖)
94
+
95
+ 任一 NO → hardcode in code + 註解 why。
96
+
97
+ ### Hardcode 合法情境
98
+
99
+ - 單一 consumer 的 padding / gap / size(eg. Tooltip arrow `4px`)
100
+ - 跟設計上下文耦合的值(eg. 圖表 axis tick gap)
101
+ - 數學 / 物理常數(eg. `Math.PI`,動畫 `easing` 公式參數)
102
+
103
+ **禁止以「省工」「之後再開」「先寫死試試」為由拒絕開 token。**(違 mindset #1)
104
+
105
+ ### Hardcode 必註解(若不是視覺常識值)
106
+
107
+ ```tsx
108
+ // hardcode 24px:Tooltip arrow geometry,只此一處 consumer + 跟 Radix arrow lib 物理綁定
109
+ <TooltipArrow width={24} />
110
+ ```
111
+
112
+ ---
113
+
114
+ ## 跨 family co-location 規則(2026-05-26 codify)
115
+
116
+ 某些 token 雖然名前綴看似屬一 family,但實際 co-located 在另一 family 檔案——**有意的依賴關係,不是命名 drift**:
117
+
118
+ ### Co-location 1:`--elevation-*` 住 `color/primitives.css`
119
+
120
+ **Why**:elevation 跟 color 共用 light/dark mode 切換 trigger(`[data-theme="dark"]`)。把 elevation 跟 color primitives 放同一個 `[data-theme="dark"]` block,**改 mode 設定一處全聯動**;若拆出去 `elevation/elevation.css` 各有 dark override block → 兩處 mode trigger,容易「改 a 壞 b」。
121
+
122
+ **對齊**:Material 3 `md-sys-elevation` 直接定義在 `theme` 範圍內(跟 color theme 同 root);Atlassian DS 「shadow + color in same theme primitive layer」;Polaris `--p-shadow-*` 與 `--p-color-*` 同住 theme css。
123
+
124
+ **拆分例外條件**:若未來 elevation 需要獨立 dark mode trigger(eg. high-contrast mode 不變色但加重 shadow)→ 才拆。目前無此需求。
125
+
126
+ ### Co-location 2:`--canvas` / `--surface` 跟 `--bg-<state>` 是 **2 個 family**(不是 drift)
127
+
128
+ | Family | 命名前綴 | 用途 | Sourced from |
129
+ |---|---|---|---|
130
+ | **Surface tone family** | `--canvas` / `--surface` / `--surface-raised` / `--surface-strong` / `--overlay` / `--tooltip` | 容器層級的**底色 tone**——不同 layer 不同視覺深度 | shadcn idiom(`--background` family),DS 沿用避免破壞 shadcn compat 命名 |
131
+ | **State pair family** | `--bg-<state>` 配 `--fg-<state>` | 互動元件 disabled / hover / active **state 底色**——跟前景文字 state 成對 | DS 自家 idiom,確保 bg + fg 一定一對一 sync(`--bg-disabled` 配 `--fg-disabled`) |
132
+
133
+ **Why 2 個 family**:Surface tone 是「容器深度」概念,跟 state 無關;State pair 是「元件當下互動 state」概念,跟容器深度無關。**強行統一**(eg. 命名全部 `--bg-canvas` / `--bg-disabled`)→ 失去語義邊界,consumer 寫 code 時不知該選哪個。
134
+
135
+ **對齊**:Polaris `--p-color-bg`(tone family)vs `--p-color-bg-disabled`(state pair);Material 3 `md-sys-color-surface*`(tone)vs `md-sys-color-on-disabled`(state);Atlassian `color.background.neutral`(tone)vs `color.background.disabled`(state)— 三家世界級全部 2 family 拆,我們對齊。
136
+
137
+ ### Co-location 3:`--layout-space-*`(layoutSpace/)vs `--sidebar-*` / `--chrome-header-*` / `--*-panel-width`(uiSize/)
138
+
139
+ **Layer 同為 L4 Layout**,但兩個 family 拆兩個檔案(`layoutSpace/` vs `uiSize/`):
140
+
141
+ | Family | 檔案 | 用途 |
142
+ |---|---|---|
143
+ | `--layout-space-*` | `layoutSpace.css` | **抽象** spacing rhythm(loose / tight / bottom)——多 consumer 共用 |
144
+ | `--sidebar-*` / `--chrome-header-*` / `--data-table-*-width` | `uiSize.css` | **具體** layout primitive 尺寸——對應特定 layout primitive 元件 |
145
+
146
+ **Why 拆**:抽象 spacing rhythm 改值 → 整 app rhythm 動;具體 primitive 尺寸改值 → 單個 primitive 動。兩個影響半徑不同,拆檔案讓 grep / blame 直接看出。
147
+
148
+ ### Co-location 4:`--opacity-disabled` 是 L1 Primitive 不是 L2 Semantic
149
+
150
+ 雖然名含「disabled」看似 semantic,但實際是**單一 structural value**(0.45)沒 mode swap、沒 hover/active variant——當 L1 Primitive 處理。`@utility opacity-disabled` 直接 expose 給 consumer。
151
+
152
+ **對齊**:Atlassian Pragmatic guideline opacity scale 也住 primitive layer。
153
+
154
+ ---
155
+
156
+ ## Internal vs Public token(consumer scope)
157
+
158
+ | Layer | Scope | Consumer who reads token? |
159
+ |---|---|---|
160
+ | **L1 Primitive** | Internal | Tag / Avatar(per color.spec.md 混合存取流派);其他禁直接 `--color-blue-6` |
161
+ | **L2 Semantic** | **Public** | 所有 consumer(app / explorations / patterns)直接 `bg-primary` / `text-foreground` |
162
+ | **L3 Family**(`--field-height-*` 等) | Internal | DS 內部 primitive(Button / Input / DataTable)消費;**consumer 必走 component prop**(`<Button size="sm">`)不直接 raw token |
163
+ | **L4 Layout**(`--sidebar-width` / `--layout-space-*`) | **Public** | Layout primitive 元件(AppShell / Sidebar / ChromeHeader)+ consumer 寫自家 layout 直接消費 |
164
+ | **L5 Internal** | Internal | 單 component 內部(目前 0 個) |
165
+
166
+ **Stories 義務**:
167
+ - Public token → **必補 Storybook stories**(色票 / 字級 / 間距對照展示)
168
+ - Internal token → spec.md codify family 即可,**免 stories**(consumer 不直接看)
169
+
170
+ `tokens/README.md`「Public vs Internal token」表 cross-reference 本段。
171
+
172
+ ---
173
+
174
+ ## 跨 family `@theme inline` bridge(Tailwind v4 spec)
175
+
176
+ Tailwind v4 `@theme inline` 把 CSS variable 升級成 utility class。每 family CSS 檔末段必有 `@theme inline` block 橋接 token → utility:
177
+
178
+ ```css
179
+ @theme inline {
180
+ --spacing-field-md: var(--field-height-md); /* `h-field-md` utility */
181
+ --color-primary: var(--primary); /* `bg-primary` utility */
182
+ }
183
+ ```
184
+
185
+ **禁** 在 bridge 中改 token 值或重新命名(`--spacing-field-md = 32px` 直接 hardcode = drift);**必** 用 `var()` 引用既有 L1/L2 token。
186
+
187
+ ---
188
+
189
+ ## SSOT auto-sync 義務(2026-05-23 user 永久 directive)
190
+
191
+ 新增 / 改 / 廢 token 時:
192
+ 1. **Spec.md** codify rationale(本 spec 上游 + family spec.md 下游)
193
+ 2. **CSS file** 寫定義
194
+ 3. **Tailwind bridge** `@theme inline` / `@utility` 補完
195
+ 4. **Consumer 改寫**:若 rename / retire → grep DS-wide consumer 一次改完(M10 proactive scan)
196
+ 5. **Hook / audit** 對齊(若新 family invariant → 補 hook;若新 dim → 補 audit skill)
197
+ 6. **Stories**(if Public token)
198
+
199
+ **禁** 留「下個 session 補 stories」「之後再 grep consumer」(M33 retired but principle absorbed by M20 sub-rule)。
200
+
201
+ ---
202
+
203
+ ## 世界級對照 citation
204
+
205
+ | 概念 | 對應世界級 DS | source |
206
+ |---|---|---|
207
+ | 分層 token 架構 | Material 3 reference / system / component | <https://m3.material.io/foundations/design-tokens/overview> |
208
+ | Primitive + Semantic 2-layer | Polaris `--p-color-*` | <https://polaris.shopify.com/tokens/colors> |
209
+ | Semantic state token(hover / active / disabled) | Atlassian DS foundations | <https://atlassian.design/foundations/color-new> |
210
+ | Family scoped naming(`--<family>-<size>`) | Carbon `$button-primary-active` | <https://carbondesignsystem.com/elements/color/tokens/> |
211
+ | Adaptive token(light/dark co-location) | Apple HIG `UIColor.systemBlue` adaptive | <https://developer.apple.com/design/human-interface-guidelines/color> |
212
+ | Surface tone vs state pair 2 family | Polaris `--p-color-bg` vs `--p-color-bg-disabled` | <https://polaris.shopify.com/tokens/colors> |
213
+ | Elevation 跟 theme co-location | Material 3 `md-sys-elevation` in theme | <https://m3.material.io/styles/elevation/tokens> |
214
+
215
+ ---
216
+
217
+ ## 反 pattern(禁)
218
+
219
+ | 禁 | 原因 |
220
+ |---|---|
221
+ | 自創新命名 family 不過 family-scoped 規則 | 1 個 token 屬 N 個 family = consumer 認知衝突 |
222
+ | 1 處 hardcode 立即開 token | <2 處同步 = token 過度抽象,Linear/Polaris 都警告 token bloat |
223
+ | 為 1 consumer 開 L3 Family token | Family 抽象服務 cross-component,1 consumer 該 hardcode in tsx |
224
+ | 拆 elevation 出 primitives.css 另做 dark override | 兩處 mode trigger,「改 a 壞 b」(M10 違反) |
225
+ | L2 Semantic 直接 raw oklch 不 alias L1 | 跳層 = 失去 primitive scale benefit + 改值要動兩處 |
226
+ | L4 Layout token 不 publish 到 npm tokens aggregator | Consumer install DS 拿不到 → 跑版(2026-05-26 AppShell 事件 root cause) |
227
+ | Internal token(`--field-height-*`)出現在 consumer code | 該走 component prop 不直接 raw token |
228
+
229
+ ---
230
+
231
+ ## 被引用(auto-maintained,Dim 3 reciprocal audit)
232
+
233
+ > 本節由 `scripts/add-reciprocal-pointers.mjs` 自動維護。
234
+
235
+ - `tokens/README.md`(Public vs Internal token 表 cross-reference)
236
+ - `color/color.spec.md`(L1/L2 上游 SSOT)
237
+ - `uiSize/uiSize.spec.md`(L3 Family + L4 Layout 上游 SSOT)
238
+ - `layoutSpace/layoutSpace.spec.md`(L4 Layout 上游 SSOT)
239
+ - `elevation/elevation.spec.md`(L1 co-location rationale)
240
+ - `opacity/opacity.spec.md`(L1 classification)
241
+ - `radius/radius.spec.md`(L3 Family classification)
242
+ - `typography/typography.spec.md`(L3 Family classification)
243
+ - `.claude/rules/ui-development.md`「Token 命名 4 條硬規則」(下游 lint-style summary)
@@ -49,6 +49,19 @@
49
49
  不寫成 calc() 公式因為 lg 無法完美遵守(54 → 56 為了 8px grid 取整),hardcode 比假公式誠實。 */
50
50
  --chrome-header-height: 3rem; /* 48px */
51
51
 
52
+ /* Sidebar / Layout primitive sizing(2026-05-26 ship 進 published per consumer SSOT gap):
53
+ 原本 declared in DS repo `src/globals.css`,沒進 npm-published tokens aggregator
54
+ → consumer install DS + import `@qijenchen/design-system/styles/tokens` 拿不到
55
+ → AppShell + Sidebar gap div 用 `var(--sidebar-width)` undefined → 解析 0px → flex 跑版
56
+ 對齊 docs:`components/Sidebar/sidebar.spec.md` L613-614 token table
57
+ Why uiSize.css:layout primitive widths 是 ui sizing semantic 範疇(對齊 --field-height / --chrome-header)
58
+ */
59
+ --sidebar-width: 15rem; /* 240px expanded sidebar */
60
+ --sidebar-width-min: 240px; /* 最小寬,同時是 AppShell Aside floor(ASIDE_WIDTH_MIN) */
61
+ --sidebar-width-mobile: 18rem; /* 288px mobile sheet 抽屜 */
62
+ --sidebar-menu-icon-size: 1rem; /* 16px sm/md row(per ICON_SIZE.sm/md);size=lg 罕見 case 下方 override 1.25rem(20px) */
63
+ --sidebar-width-icon: calc(2 * var(--layout-space-loose, 1rem) + var(--sidebar-menu-icon-size));
64
+
52
65
  /* Tree Indent — TreeView + DataTable nested rows(SSOT,2026-05-04)
53
66
  公式:chevronSize + gap-2 = 16+8(sm/md)/ 20+8(lg)
54
67
  Consumer:TreeView depth indent / DataTable nested row depth indent */
@@ -67,6 +80,9 @@
67
80
 
68
81
  [data-ui-size="lg"],
69
82
  [data-density="lg"] {
83
+ /* Sidebar lg override(per src/globals.css 原 declaration:size=lg 罕見 case)*/
84
+ --sidebar-menu-icon-size: 1.25rem; /* 20px(lg)*/
85
+
70
86
  --field-height-sm: 2rem; /* 32px */
71
87
  --field-height-md: 2.25rem; /* 36px */
72
88
  --field-height-lg: 2.5rem; /* 40px */