@qijenchen/design-system 0.1.0-beta.67 → 0.1.0-beta.69

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.
Files changed (64) hide show
  1. package/dist/components/Alert/alert.d.ts.map +1 -1
  2. package/dist/components/Alert/alert.js +4 -4
  3. package/dist/components/Alert/alert.js.map +1 -1
  4. package/dist/components/Dialog/dialog.d.ts.map +1 -1
  5. package/dist/components/Dialog/dialog.js.map +1 -1
  6. package/dist/components/DropdownMenu/dropdown-menu.d.ts.map +1 -1
  7. package/dist/components/DropdownMenu/dropdown-menu.js +0 -1
  8. package/dist/components/DropdownMenu/dropdown-menu.js.map +1 -1
  9. package/dist/components/FileViewer/file-viewer.d.ts.map +1 -1
  10. package/dist/components/FileViewer/file-viewer.js +1 -2
  11. package/dist/components/FileViewer/file-viewer.js.map +1 -1
  12. package/dist/components/Popover/popover.d.ts +1 -1
  13. package/dist/components/Popover/popover.d.ts.map +1 -1
  14. package/dist/components/Popover/popover.js +9 -10
  15. package/dist/components/Popover/popover.js.map +1 -1
  16. package/dist/components/Steps/steps.d.ts.map +1 -1
  17. package/dist/components/Steps/steps.js +11 -3
  18. package/dist/components/Steps/steps.js.map +1 -1
  19. package/dist/components/Tooltip/tooltip.d.ts.map +1 -1
  20. package/dist/components/Tooltip/tooltip.js +0 -1
  21. package/dist/components/Tooltip/tooltip.js.map +1 -1
  22. package/dist/patterns/overlay-surface/index.js +2 -1
  23. package/dist/patterns/overlay-surface/overlay-surface.d.ts +8 -0
  24. package/dist/patterns/overlay-surface/overlay-surface.d.ts.map +1 -1
  25. package/dist/patterns/overlay-surface/overlay-surface.js +3 -1
  26. package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
  27. package/ds-canonical/hooks/tests/test_check_story_invariants.sh +1 -1
  28. package/ds-canonical/templates/dashboard-app.tsx +9 -4
  29. package/llms-full.txt +1 -1
  30. package/llms.txt +1 -1
  31. package/package.json +1 -1
  32. package/src/components/Alert/alert.spec.md +1 -1
  33. package/src/components/Alert/alert.tsx +7 -4
  34. package/src/components/Button/button.spec.md +2 -2
  35. package/src/components/Checkbox/checkbox.principles.stories.tsx +18 -15
  36. package/src/components/Coachmark/coachmark.principles.stories.tsx +3 -2
  37. package/src/components/DataTable/data-table-filter-panel.tsx +3 -3
  38. package/src/components/DataTable/data-table-sort-manager.tsx +3 -3
  39. package/src/components/Dialog/dialog.anatomy.stories.tsx +1 -1
  40. package/src/components/Dialog/dialog.spec.md +11 -11
  41. package/src/components/Dialog/dialog.tsx +7 -8
  42. package/src/components/DropdownMenu/dropdown-menu.tsx +4 -1
  43. package/src/components/FileItem/file-item.spec.md +1 -1
  44. package/src/components/FileItem/file-item.stories.tsx +3 -3
  45. package/src/components/FileViewer/file-viewer.anatomy.stories.tsx +4 -4
  46. package/src/components/FileViewer/file-viewer.spec.md +4 -4
  47. package/src/components/FileViewer/file-viewer.tsx +6 -3
  48. package/src/components/Notice/notice.anatomy.stories.tsx +4 -4
  49. package/src/components/Notice/notice.spec.md +1 -0
  50. package/src/components/Notice/notice.stories.tsx +4 -4
  51. package/src/components/Popover/popover.anatomy.stories.tsx +13 -11
  52. package/src/components/Popover/popover.principles.stories.tsx +10 -8
  53. package/src/components/Popover/popover.spec.md +2 -2
  54. package/src/components/Popover/popover.tsx +14 -11
  55. package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +3 -2
  56. package/src/components/Sheet/sheet.principles.stories.tsx +5 -4
  57. package/src/components/Sidebar/sidebar.spec.md +2 -0
  58. package/src/components/Steps/steps.tsx +11 -3
  59. package/src/components/Tooltip/tooltip.tsx +3 -1
  60. package/src/patterns/header-canonical/header-canonical.spec.md +3 -2
  61. package/src/patterns/overlay-surface/overlay-surface.spec.md +12 -10
  62. package/src/patterns/overlay-surface/overlay-surface.tsx +20 -8
  63. package/src/tokens/density/density.spec.md +33 -22
  64. package/src/tokens/layoutSpace/layoutSpace.stories.tsx +4 -4
@@ -10,7 +10,7 @@ import { cn } from '@/lib/utils'
10
10
  import { dragSourceStyle, dragHandleCursorClass } from '@/design-system/lib/drag-visual'
11
11
  import { Button } from '@/design-system/components/Button/button'
12
12
  import { Select, type SelectOption } from '@/design-system/components/Select/select'
13
- import { SurfaceHeader, SurfaceBody } from '@/design-system/patterns/overlay-surface/overlay-surface'
13
+ import { SurfaceHeader, SurfaceBody, COMPACT_HEADER_SLOT } from '@/design-system/patterns/overlay-surface/overlay-surface'
14
14
  import { ButtonDivider } from '@/design-system/components/Button/button-group'
15
15
  import { PopoverTitle, PopoverClose } from '@/design-system/components/Popover/popover'
16
16
  import { ItemInlineActionButton } from '@/design-system/patterns/element-anatomy/item-anatomy'
@@ -108,8 +108,8 @@ export function DataTableSortManager<TData>({
108
108
  // 2026-05-23 Phase A.4 Decision 2: w-[480px] → token `--data-table-sort-panel-width`(SSOT in uiSize.css)
109
109
  return (
110
110
  <div className={cn('flex flex-col h-full min-h-0 w-[var(--data-table-sort-panel-width)]', className)}>
111
- {/* Popover 派輕量 chrome — slot 20 匹配 PopoverTitle text-body line-height,header 自然 ~45px */}
112
- <SurfaceHeader className="[--chrome-slot-h:1.25rem]">
111
+ {/* Popover 派輕量 chrome — slot COMPACT_HEADER_SLOT(=21,衍生自 PopoverTitle text-body line-box),header 自然 ~45px */}
112
+ <SurfaceHeader className={COMPACT_HEADER_SLOT}>
113
113
  <PopoverTitle className="flex-1">排序</PopoverTitle>
114
114
  {onReset && sorting.length > 0 && (
115
115
  <>
@@ -417,7 +417,7 @@ export const ColorMatrix: Story = {
417
417
  <div className="flex flex-col gap-8">
418
418
  <div>
419
419
  <H3>Density</H3>
420
- <Desc>Dialog 繼承 page `data-density`(v5 校準,跟 Sheet 對齊)— 不自設密度 attribute。先前曾設 `data-layout-space="lg"` body 寬鬆呼吸,但跟 `--chrome-header-height` canonical(md=48)衝突,2026-04-22 撤回。世界級:Polaris Modal px 16(md loose)/ Material M3 24(lg loose)— 我方跟 page density 自動對齊。</Desc>
420
+ <Desc>Dialog `data-layout-space="lg"`(只鎖版面間距 tier)+ ui-size 繼承 page `data-density`(控件不被撐大)。modal 值得寬鬆呼吸 padding:body px-loose 24 / header py-tight 16 → header 56(宣告 lg tier)。reinstate c3d3b736 decouple(只鎖 layout-space,非當年連 ui-size 一起鎖撐高 header density)。世界級:Material M3 / Atlassian modal padding 24。</Desc>
421
421
  </div>
422
422
 
423
423
  <div>
@@ -58,17 +58,17 @@ DialogContent (fixed, centered)
58
58
 
59
59
  **Padding SSOT**:Header / Body / Footer 的 padding + 分隔線由 `patterns/overlay-surface/overlay-surface.spec.md` own——Dialog 與 Popover 共用同一套 primitive,避免 token 漂移。Dialog 特有行為:Header 的 Close 按鈕;Body 用 `<ScrollArea>` wrap(viewport-fill 專用,SSOT 見 overlay-surface.spec.md 「Body overflow canonical」節 + `components/ScrollArea/scroll-area.spec.md`)。
60
60
 
61
- ## Density(2026-04-22 v5 校準:繼承 page density,跟 Sheet 對齊)
61
+ ## Density(2026-06-16 定論:全繼承 page,不鎖)
62
62
 
63
- Dialog **繼承 page `data-density`**,不自設密度 attribute。這是 overlay primitive canonical(跟 `components/Sheet/sheet.tsx` 對齊 Sheet 不自設 density,繼承 page 層級 `html[data-density]`,見 sheet.tsx line 111 canonical)
63
+ Dialog **不自設任何 density attribute**,layout-space + ui-size 全繼承 page。效果:md page → body `px-loose`=16 / header `py-tight`=12(header 48);lg page 24 / 16(header 56),隨 page
64
64
 
65
- **歷史備忘**:先前曾設 `data-layout-space="lg"` header/body 寬鬆呼吸,但跟 `--chrome-header-height` canonical 衝突(md page dialog header 期望 48,強設 lg 會變 56)。**已於 2026-04-22 v5 撤回**,Dialog 全盤繼承 page density,header 高度 = `--chrome-header-height` 自動對齊(md=48 / lg=56)。
65
+ **為何不鎖(撤回本 session 一度加的 `data-layout-space="lg"`)**:本 `density.spec` 第 10 行親自定義 layout-space 管「dialog body padding」—— Dialog 若鎖死 layout-space,等於 override 我們自家 dial 對它點名要管的對象失效 = **自相矛盾**。有「同類 padding-density dial」的世界級 DS(SAP Fiori cozy/compact 做 `syncStyleClass` 專門把 page density 同步進 dialog;AWS Cloudscape compact「propagate to all components, in all view types」,例外只列 informational + tiny dropdown,modal 不在內)都讓 **modal page dial 走**,沒有一家把 modal 釘固定 tier。Material/Atlassian 的固定 24 是因為它們**沒有** density dial(其 24 = 我方 lg)。
66
66
 
67
- **世界級對照**:
68
- - Polaris Modal:px 16(= md loose)
69
- - Material M3 Dialog:px 24(= lg loose)
70
- - Atlassian Dialog:px 24
71
- - 我方:跟隨 page density,md=16 / lg=24,兩端都在世界級 range 內。
67
+ **「modal 要寬鬆呼吸」怎麼滿足**:lg 產品自動拿 24(Material 級);md 產品本就要緊湊(資訊密集),16 是合格 modal padding(Polaris Modal = 16,世界級下限)。**「button 不撐高 header」**由 ui-size 繼承 page 解決(button = page sm),與 layout-space 鎖不鎖無關 → 故不需鎖。
68
+
69
+ **Header 高度**:padding-based = title line-box 24 + `py-tight`×2 → md 48 / lg 56,**隨 page tier**(= `--chrome-header-height` 同 tier 對齊)
70
+
71
+ **世界級對照**:Material M3 / Atlassian 24(無 dial,= 我方 lg)/ Polaris 16(= 我方 md 下限)/ SAP Fiori + Cloudscape(有 dial → modal 跟 dial = 我方繼承)。
72
72
 
73
73
  ## Layout
74
74
 
@@ -99,8 +99,8 @@ Modal 與 viewport 四邊保持 `--layout-space-bottom`(48px)最小間距。
99
99
 
100
100
  永遠存在於 DialogHeader 右側。使用 `<Button data-dismiss iconOnly dismiss size="sm" startIcon={X} aria-label="關閉" />`,不可移除——使用者永遠需要明確的關閉手段。
101
101
 
102
- **Size canonical(v5 chrome-unbounded)**:Button native size **sm**(28 md / 32 lg),touch target 亦同。SurfaceHeader 的 `[data-unbounded]` CSS rule 自動對 text variant / dismiss 套負 my `calc((xs-sm)/2)` → **layout 佔位 = 24**(xs 固定)。效果:
103
- - Header 只有 title + close X → max layout = 24 → header = 24 + 2×tight = **48 md / 56 lg = `--chrome-header-height`**
102
+ **Size canonical(v5 chrome-unbounded)**:Button native size **sm**(隨 page density:28 md / 32 lg),touch target 亦同。SurfaceHeader 的 `[data-unbounded]` CSS rule 自動對 text variant / dismiss 套負 my → **layout 佔位 = title line-box**(slot 衍生自 `--font-body-lg-size`×1.5=24,見 `overlay-surface.spec.md` slot 段)。效果:
103
+ - Header 只有 title + close X → max layout = 24 → header = 24 + 2×`py-tight`(隨 page:md 12 / lg 16)= **48 md / 56 lg**(= `--chrome-header-height` 同 tier 對齊)
104
104
  - Header 塞 bounded primary(無 `data-unbounded`)→ header 自然長高
105
105
 
106
106
  **Canonical 來源**:Dialog 是 overlay chrome,corner close X 屬 action group region,必用 Button(非 Inline Action / 非自刻 button)。詳見 `patterns/element-anatomy/item-anatomy.spec.md`「Dismiss canonical」+ `patterns/overlay-surface/overlay-surface.spec.md`「Chrome dismiss size canonical」。
@@ -142,7 +142,7 @@ Dialog 是容器,無整體 disabled / loading / empty 狀態——這些屬於
142
142
 
143
143
  **Dark mode**:由 semantic token(`bg-surface-raised` / `border-border`)自動切換,無自訂 palette。
144
144
 
145
- **Density**:Dialog **繼承 page density**(v5 校準,跟 Sheet 對齊),見上「Density」段。
145
+ **Density**:Dialog **全繼承 page**(layout-space + ui-size 皆不自鎖),見上「Density」段。
146
146
 
147
147
  ---
148
148
 
@@ -95,14 +95,13 @@ const DialogContent = React.forwardRef<
95
95
  <DialogOverlay />
96
96
  <DialogPrimitive.Content
97
97
  ref={ref}
98
- // Density canonical(2026-04-22 v5 校準):Dialog 繼承 page density( Sheet 對齊
99
- // sheet.tsx line 111 canonical),不自設 data-layout-space="lg" data-density。
100
- //
101
- // 先前曾設 `data-layout-space="lg"` header/body 寬鬆呼吸,但跟 chrome-header-height
102
- // canonical 衝突(md page dialog header 期望 48,強設 lg 會變 56)。
103
- // 世界級對照:Polaris Modal horizontal padding 16 / Material M3 24 / Atlassian 24 — 16 是
104
- // 合理 lower bound;md page 16 loose body padding 可接受,lg page 自動 24。
105
- // 詳 overlay-surface.spec.md「Chrome dismiss size canonical」
98
+ // Density:**全繼承 page**(layout-space + ui-size 都不自鎖)。2026-06-16 定論(撤回本 session 一度加的
99
+ // data-layout-space="lg"):density.spec 10 行親自定義 layout-space 管「dialog body padding」——
100
+ // Dialog 鎖死它 = override 自家 dial 對它點名要管的對象失效 = 自相矛盾。有同類 padding-density dial 的
101
+ // 世界級(SAP Fiori syncStyleClass / AWS Cloudscape「all view types」)都讓 modal 跟 page dial 走、不鎖固定 tier。
102
+ // 效果:md page body px-loose 16 / header py-tight 12(header 48);lg page → 24 / 16(header 56),隨 page
103
+ // 「modal 要寬鬆」需求在 lg 階自然滿足(Polaris modal 16 = 世界級下限,證明 md 16 合格);「button 不撐高
104
+ // header」由 ui-size 繼承 page 解決(button=page sm),與 layout-space 鎖不鎖無關 故不需鎖。
106
105
  onOpenAutoFocus={handleOpenAutoFocus}
107
106
  className={cn(
108
107
  "fixed left-1/2 top-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
@@ -129,7 +129,10 @@ const DropdownMenuContent = React.forwardRef<
129
129
  sideOffset={sideOffset}
130
130
  collisionPadding={collisionPadding}
131
131
  align={align}
132
- data-density="md"
132
+ // Density:繼承 page density(2026-06-15 canonical)。menu item 高度 = field-height-{size},而
133
+ // field-height 隨 density 變(md 28/32/36 → lg 32/36/40)→ 鎖 data-density="md" 會把選單釘在 md-scale,
134
+ // lg page 上對不上 lg 觸發點。原 data-density 是 409b91da a11y 批次順手加(對齊 Popover),非設計
135
+ // 決策 → 移除,item 隨 page density 與觸發點一致(tier 仍由 size prop 決定)。
133
136
  // Focus return on close:不 override `onCloseAutoFocus` — 用 Radix 內建 default
134
137
  // (close 時 focus 還 trigger;outside-interaction 例外由 Radix `hasInteractedOutsideRef` 自管)。
135
138
  // W3C APG menubar「Escape: …return focus to the element…from which the menu was opened」
@@ -293,7 +293,7 @@ upload-manager 的 completed(100% bar + ✓)屬「剛完成的 upload session」
293
293
  - **它是 popover-class 浮層 surface,但不是 Radix `<Popover>`**(常駐面板:不靠 trigger 開、不 outside-click 關、用 chevron 收合非 X dismiss)→ **不包 `<Popover>`**,而是直接消費 overlay-surface 三件套 primitive。
294
294
  - **殼 + header + body 全消費 overlay-surface SSOT(禁手刻)**:
295
295
  - 殼:用 Popover 同款 chrome token `rounded-lg border border-border bg-surface-raised shadow-[var(--elevation-200)] flex flex-col`(DS 無獨立 shell primitive — `PopoverContent`/`DialogContent` 各自套這組 token;常駐面板鏡像同值)。
296
- - header:`<SurfaceHeader className="justify-between [--chrome-slot-h:1.25rem]">` + `<PopoverTitle>`(輕量浮層 header SSOT,padding = px-loose py-tight + border-b + unbounded-slot 負 my trick)。
296
+ - header:`<SurfaceHeader className={cn("justify-between", COMPACT_HEADER_SLOT)}>` + `<PopoverTitle>`(輕量浮層 header SSOT,slot 走 `COMPACT_HEADER_SLOT`=21 衍生自 text-body title;padding = px-loose py-tight + border-b + unbounded-slot 負 my trick)。
297
297
  - body:**`<SurfaceBody>`(body SSOT,含 px-loose py-tight + flex-1 scroll 鏈)**,FileItem-specific padding 用 className override(見下)。**這不是「List-as-region」**(那專指 edge-to-edge 選單清單:item hover-bg 貼容器、px-0;Cmd+K / menu / nav)—— upload-manager list 有 px-loose、item 無 hover-bg、是 chrome-padded body,故就用 SurfaceBody。**scroll(consumer 注意)**:SurfaceBody 的 `flex-1 / overflow-y-auto` 只在 shell 有 `max-h` + `overflow-hidden` 時生效;常駐面板若檔案數可超 viewport,shell 須加 `max-h`(對齊 overlay-surface.spec.md「viewport-aware scroll」),demo 短內容不需。
298
298
  - **禁手刻**:`<div px-loose py-2 border-b>`(header)/ 手刻 `<div px-loose py-tight>`(body)= drift(2026-06-03/04 user 抓:py-2≠py-tight / 殼 token 全偏 / body 重刻 SurfaceBody)。Hook `check_story_invariants.sh R9` 機械攔手刻 header。
299
299
 
@@ -6,7 +6,7 @@ import { FileItem } from './file-item'
6
6
  import { Button } from '@/design-system/components/Button/button'
7
7
  import { FileViewer, type FileInfo } from '@/design-system/components/FileViewer/file-viewer'
8
8
  // upload-manager 面板消費 overlay-surface header + body SSOT(非手刻)—— 跟 Popover/Dialog 同一組 primitive
9
- import { SurfaceHeader, SurfaceBody } from '@/design-system/patterns/overlay-surface/overlay-surface'
9
+ import { SurfaceHeader, SurfaceBody, COMPACT_HEADER_SLOT } from '@/design-system/patterns/overlay-surface/overlay-surface'
10
10
  import { PopoverTitle } from '@/design-system/components/Popover/popover'
11
11
 
12
12
  // 錯誤 description 範例(含 clickable "View log"):consumer 自由 ReactNode,通常用底線 link 表 clickable
@@ -217,7 +217,7 @@ export const UploadManagerSurface = {
217
217
  <div className="max-w-md flex flex-col rounded-lg border border-border bg-surface-raised shadow-[var(--elevation-200)]">
218
218
  {/* header 消費 overlay-surface SurfaceHeader + PopoverTitle(輕量浮層 chrome SSOT,非手刻):
219
219
  px-loose py-tight + border-b + chevron(variant=text → 自動 data-unbounded → 套 slot 負 my trick)*/}
220
- <SurfaceHeader className="justify-between [--chrome-slot-h:1.25rem]">
220
+ <SurfaceHeader className={`justify-between ${COMPACT_HEADER_SLOT}`}>
221
221
  <div className="flex-1 min-w-0"><PopoverTitle>正在上傳 3 個項目</PopoverTitle></div>
222
222
  <Button iconOnly variant="text" size="sm" startIcon={ChevronDown} aria-label="收合" onClick={noop} />
223
223
  </SurfaceHeader>
@@ -243,7 +243,7 @@ export const UploadManagerCompactSurface = {
243
243
  render: () => (
244
244
  <div className="max-w-md flex flex-col rounded-lg border border-border bg-surface-raised shadow-[var(--elevation-200)]">
245
245
  {/* header 消費同一個 overlay-surface SurfaceHeader + PopoverTitle SSOT(同 rich panel)*/}
246
- <SurfaceHeader className="justify-between [--chrome-slot-h:1.25rem]">
246
+ <SurfaceHeader className={`justify-between ${COMPACT_HEADER_SLOT}`}>
247
247
  <div className="flex-1 min-w-0"><PopoverTitle>同步 3 個檔案</PopoverTitle></div>
248
248
  <Button iconOnly variant="text" size="sm" startIcon={ChevronDown} aria-label="收合" onClick={noop} />
249
249
  </SurfaceHeader>
@@ -394,7 +394,7 @@ export const Inspector: Story = {
394
394
  </tr>
395
395
  <tr>
396
396
  <Td>Toolbar 高度</Td>
397
- <Td mono>--chrome-header-height(lg=56px,ChromeHeader lockDensity="lg",與 InfoPanel header 等高)</Td>
397
+ <Td mono>--chrome-header-height(隨 page:48 md / 56 lg,ChromeHeader 繼承 page,與 InfoPanel header 等高)</Td>
398
398
  </tr>
399
399
  <tr>
400
400
  <Td>Toolbar 水平 padding</Td>
@@ -632,9 +632,9 @@ export const SizeMatrix: Story = {
632
632
  </tr>
633
633
  <tr>
634
634
  <Td>Toolbar 高</Td>
635
- <Td mono>--chrome-header-height(lg=56px)</Td>
635
+ <Td mono>--chrome-header-height(48 md / 56 lg)</Td>
636
636
  <Td>Figma toolbar 48–56 / macOS Preview 56</Td>
637
- <Td>ChromeHeader lockDensity="lg",token 驅動;lg 下值為 56px</Td>
637
+ <Td>ChromeHeader 繼承 page density,token 驅動;md=48 / lg=56(2026-06-15 移除 lockDensity)</Td>
638
638
  </tr>
639
639
  <tr>
640
640
  <Td>InfoPanel 寬</Td>
@@ -698,7 +698,7 @@ export const SizeMatrix: Story = {
698
698
  <tr>
699
699
  <Td>Panel header 高</Td>
700
700
  <Td mono>--chrome-header-height</Td>
701
- <Td>消費 &lt;ChromeHeader lockDensity=&quot;lg&quot;&gt;,lg = 56px;與 Toolbar 等高,視覺水平對齊</Td>
701
+ <Td>消費 &lt;ChromeHeader&gt;(繼承 page density);md=48 / lg=56,與 Toolbar 等高,視覺水平對齊</Td>
702
702
  </tr>
703
703
  <tr>
704
704
  <Td>Body 水平 padding</Td>
@@ -100,7 +100,7 @@ Family 的 canonical 規定的是「同用途同 layout」;FileViewer 用途(ful
100
100
  ```
101
101
 
102
102
  **分區決策**:
103
- - **Toolbar 高度 = `--chrome-header-height`**(md=48 / lg=56)——與 InfoPanel header 同高,視覺對齊;**code 已消費 `<ChromeHeader>` primitive**(`file-viewer.tsx:328` Toolbar + `:459` InfoPanel header,皆 `<ChromeHeader lockDensity="lg">`),高度由 primitive 內部套 `h-[var(--chrome-header-height)]`(`chrome-header.tsx`),tsx 不硬寫 height className。**跨家族 SSOT pointer**:FileViewer Toolbar + InfoPanel header 屬 **Chrome header(Fixed-h)家族**,border / padding / dismiss size / withTabs 跨家族契約 SSOT 詳 `patterns/header-canonical/header-canonical.spec.md`。viewer lock lg density `lockDensity="lg"` prop
103
+ - **Toolbar 高度 = `--chrome-header-height`**(隨 page density:md=48 / lg=56)——與 InfoPanel header 同高,視覺對齊;**code 已消費 `<ChromeHeader>` primitive**(Toolbar + InfoPanel header,皆 `<ChromeHeader>` **繼承 page density**,2026-06-15 移除 lockDensity),高度由 primitive 內部套 `h-[var(--chrome-header-height)]`(`chrome-header.tsx`),tsx 不硬寫 height className。**跨家族 SSOT pointer**:FileViewer Toolbar + InfoPanel header 屬 **Chrome header(Fixed-h)家族**,border / padding / dismiss size / withTabs 跨家族契約 SSOT 詳 `patterns/header-canonical/header-canonical.spec.md`。FileViewer **不鎖 density**(全 surface 繼承 page;原 `lockDensity="lg"` 只鎖兩個 header、沒鎖 body → header(px-loose@lg=24)與 body(px-loose@page=16)左緣不對齊 = 圖二 bug,已移除,見 `density.spec.md` 消費者清單)
104
104
  - **Viewport `flex-1`**——填滿剩餘空間;InfoPanel 透過 `w-80 shrink-0` 從右側切出,不吃 viewport 自然寬
105
105
  - **Filmstrip 固定 h-24**——預留 thumb 64 + padding;只在 `showFilmstrip && files.length > 1` 時顯示
106
106
  - **Prev/Next arrows 絕對定位**——避免 layout shift,只在 `files.length > 1` 渲染
@@ -244,7 +244,7 @@ Shell 看到 `pageNumber` capability 時自動在 toolbar 顯示 page navigator(
244
244
 
245
245
  ### 同 flex 列幾何鐵律(CLAUDE.md 規則)
246
246
 
247
- `[−]` / `[%input]` / `[+]` 三個 slot **都是 h-field-sm**,統一高度確保 gap 不被 hover bg 吃掉。Toolbar 包在 `<ChromeHeader lockDensity="lg">`(`file-viewer.tsx:328`)內,subtree density = lg → h-field-sm 解析為 **32px**(對齊本 spec「Density」段 L313 + `uiSize.spec.md`:sm 在 md density = 28px / lg density = 32px)。Button iconOnly size="sm" aspect-square 32×32,Input size="sm" 32 高,視覺嚴格對齊。
247
+ `[−]` / `[%input]` / `[+]` 三個 slot **都是 h-field-sm**,統一高度確保 gap 不被 hover bg 吃掉。Toolbar 包在 `<ChromeHeader>`(繼承 page density,2026-06-15 移除 lockDensity)內 → h-field-sm page density 解析(`uiSize.spec.md`:sm 在 md = 28px / lg = 32px)。Button iconOnly size="sm" aspect-square Input size="sm" 同高,視覺嚴格對齊。
248
248
 
249
249
  ### Why inline(不抽獨立 primitive)
250
250
 
@@ -255,7 +255,7 @@ Shell 看到 `pageNumber` capability 時自動在 toolbar 顯示 page navigator(
255
255
  ## InfoPanel 規則
256
256
 
257
257
  - **寬度固定 w-80(320px)**——對齊 Figma right panel(320)的業界慣例;Google Photos 用 360 偏寬,FileViewer 走 Figma 偏窄以讓 viewport 多一些空間 <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
258
- - **Header 高度 = `--chrome-header-height`**(56px @ viewer lockDensity="lg")——與 Toolbar 等高,視覺對齊;消費 `<ChromeHeader lockDensity="lg">`(`file-viewer.tsx:459`)
258
+ - **Header 高度 = `--chrome-header-height`**( page density:48 md / 56 lg)——與 Toolbar 等高,視覺對齊;消費 `<ChromeHeader>`(繼承 page,2026-06-15 移除 lockDensity)
259
259
  - **內容分兩區**:
260
260
  - 「說明」Textarea(可編輯 / readOnly 依 `readOnly` prop)
261
261
  - 「檔案資訊」`<dl>`:檔名 / 類型 / 大小 / 自訂 metadata 條目
@@ -310,7 +310,7 @@ Shell 看到 `pageNumber` capability 時自動在 toolbar 顯示 page navigator(
310
310
 
311
311
  **Dark mode**:FileViewer chrome 鎖 dark(`data-theme="dark"` subtree);背景頁面的 theme 不影響 viewer chrome——viewer 是獨立沉浸式 context,類似 Tooltip / 全螢幕影片播放器的 convention。
312
312
 
313
- **Density**:FileViewer chrome `<ChromeHeader lockDensity="lg">`(`file-viewer.tsx:328,459` Toolbar + InfoPanel header),強制 lg-equivalent chrome-header-height = 56px;Filmstrip `h-24`(96px)+ thumb 64×64 屬媒體展示框尺寸,**刻意不隨 density 放大**(viewer 是展示殼不是工作區)。Toolbar 內的 `<Button size="sm">` 與 `<ZoomInput h-field-sm>` 會隨 density 微調(sm 在 md density = 28px,lg density = 32px),在 viewer 這個尺度可忽略。
313
+ **Density**:FileViewer **全 surface 繼承 page density**(2026-06-15 移除 lockDensity 原本只鎖兩個 ChromeHeader、沒鎖 body → header(px-loose@lg=24) body(px-loose@page=16)左緣不對齊,圖二 bug)。chrome-header-height page(48 md / 56 lg),header 與 body 同密度 → 左緣對齊;Filmstrip `h-24`(96px)+ thumb 64×64 屬媒體展示框尺寸,**刻意不隨 density 放大**(viewer 是展示殼不是工作區)。Toolbar `<Button size="sm">` 與 `<ZoomInput h-field-sm>` page density(sm 在 md = 28px / lg = 32px)
314
314
 
315
315
  ---
316
316
 
@@ -338,7 +338,9 @@ const Toolbar: React.FC<ToolbarProps> = ({
338
338
  }) => {
339
339
  return (
340
340
  <ChromeHeader
341
- lockDensity="lg"
341
+ // Density:繼承 page(2026-06-15 canonical)。原 lockDensity="lg" 只鎖在 header、沒鎖 body →
342
+ // InfoPanel header(px-loose@lg=24)與其 body(px-loose@page=16)左緣不對齊(圖二 bug)。移除 lock
343
+ // → header + body 全繼承 page density,左緣對齊;FileViewer 全 surface 同一密度(non-special)。
342
344
  className={cn(
343
345
  // Chrome layer — `bg-surface-raised` 對齊 token semantic「遮蓋型浮層必須不透明」。
344
346
  // FileViewer 整體是 overlay,chrome 屬其 raised surface(同 DropdownMenuContent line 244)。
@@ -467,8 +469,9 @@ const InfoPanel: React.FC<InfoPanelProps> = ({
467
469
  )}
468
470
  aria-label={labels.detailPanel}
469
471
  >
470
- {/* Panel header — 與 Toolbar 等高(consume ChromeHeader lockDensity="lg"),視覺一致 */}
471
- <ChromeHeader lockDensity="lg" className="justify-between">
472
+ {/* Panel header — 與 Toolbar ChromeHeader,繼承 page density(2026-06-15:移除 lockDensity,
473
+ header(原 px-loose@lg=24)與 body(px-loose@page=16)左緣不對齊 = 圖二 bug;全 surface 同密度)。 */}
474
+ <ChromeHeader className="justify-between">
472
475
  <h3 className="text-body-lg font-medium text-foreground">{labels.detailsHeading}</h3>
473
476
  {/* InfoPanel close 走 dismiss canonical `<Button iconOnly dismiss />`,對齊 button.spec.md
474
477
  「Dismiss 視覺類」+ inline-action.spec.md「Dismiss canonical — X close only」。 */}
@@ -53,10 +53,10 @@ const VARIANT_ICON: Record<NoticeVariant, LucideIcon | null> = {
53
53
 
54
54
  const SUBTLE_CONTAINER: Record<NoticeVariant, string> = {
55
55
  neutral: 'bg-muted border border-border',
56
- info: 'bg-info-subtle border border-[var(--info-hover)]',
57
- success: 'bg-success-subtle border border-[var(--success-hover)]',
58
- warning: 'bg-warning-subtle border border-[var(--warning-hover)]',
59
- error: 'bg-error-subtle border border-[var(--error-hover)]',
56
+ info: 'bg-info-subtle border border-[var(--info-text)]',
57
+ success: 'bg-success-subtle border border-[var(--success-text)]',
58
+ warning: 'bg-warning-subtle border border-[var(--warning-text)]',
59
+ error: 'bg-error-subtle border border-[var(--error-text)]',
60
60
  }
61
61
 
62
62
  function SubtleShell({ variant, children }: { variant: NoticeVariant; children: React.ReactNode }) {
@@ -154,4 +154,5 @@ Notice **不**自帶 Esc-to-dismiss 行為(`notice.tsx` 無 keydown handler);dis
154
154
  - `alert.spec.md`
155
155
  - `bulk-action-bar.spec.md`
156
156
  - `coachmark.spec.md`
157
+ - `item-anatomy.spec.md`
157
158
  - `toast.spec.md`
@@ -26,10 +26,10 @@ type Story = StoryObj<typeof Notice>
26
26
 
27
27
  const SUBTLE_CONTAINER: Record<NoticeVariant, string> = {
28
28
  neutral: 'bg-muted border border-border',
29
- info: 'bg-info-subtle border border-[var(--info-hover)]',
30
- success: 'bg-success-subtle border border-[var(--success-hover)]',
31
- warning: 'bg-warning-subtle border border-[var(--warning-hover)]',
32
- error: 'bg-error-subtle border border-[var(--error-hover)]',
29
+ info: 'bg-info-subtle border border-[var(--info-text)]',
30
+ success: 'bg-success-subtle border border-[var(--success-text)]',
31
+ warning: 'bg-warning-subtle border border-[var(--warning-text)]',
32
+ error: 'bg-error-subtle border border-[var(--error-text)]',
33
33
  }
34
34
 
35
35
  const SOLID_BG: Record<Exclude<NoticeVariant, 'neutral'>, string> = {
@@ -2,6 +2,8 @@
2
2
  import type { Meta, StoryObj } from '@storybook/react'
3
3
  import { Popover, PopoverTrigger, PopoverContent, PopoverHeader, PopoverBody, PopoverFooter, PopoverTitle } from './popover'
4
4
  import { Button } from '@/design-system/components/Button/button'
5
+ import { Checkbox } from '@/design-system/components/Checkbox/checkbox'
6
+ import { CheckboxGroup } from '@/design-system/components/Checkbox/checkbox-group'
5
7
  import { H3, Desc, Td, Th, TokenCell } from '@/design-system/stories-helpers/anatomy/anatomy-utils'
6
8
 
7
9
  const meta: Meta = {
@@ -26,11 +28,11 @@ export const Overview: Story = {
26
28
  <PopoverContent>
27
29
  <PopoverHeader><PopoverTitle>依類型篩選</PopoverTitle></PopoverHeader>
28
30
  <PopoverBody>
29
- <div className="flex flex-col gap-1.5 text-caption text-fg-secondary">
30
- <label className="flex items-center gap-2"><input type="checkbox" defaultChecked /> 待處理</label>
31
- <label className="flex items-center gap-2"><input type="checkbox" defaultChecked /> 進行中</label>
32
- <label className="flex items-center gap-2"><input type="checkbox" /> 已完成</label>
33
- </div>
31
+ <CheckboxGroup>
32
+ <Checkbox defaultChecked label="待處理" />
33
+ <Checkbox defaultChecked label="進行中" />
34
+ <Checkbox label="已完成" />
35
+ </CheckboxGroup>
34
36
  </PopoverBody>
35
37
  <PopoverFooter>
36
38
  <Button variant="tertiary" size="sm" className="flex-1">清除</Button>
@@ -129,11 +131,11 @@ export const Inspector: Story = {
129
131
  <PopoverHeader><PopoverTitle>依類型篩選</PopoverTitle></PopoverHeader>
130
132
  )}
131
133
  <PopoverBody>
132
- <div className="flex flex-col gap-1.5 text-caption text-fg-secondary">
133
- <label className="flex items-center gap-2"><input type="checkbox" defaultChecked /> 待處理</label>
134
- <label className="flex items-center gap-2"><input type="checkbox" defaultChecked /> 進行中</label>
135
- <label className="flex items-center gap-2"><input type="checkbox" /> 已完成</label>
136
- </div>
134
+ <CheckboxGroup>
135
+ <Checkbox defaultChecked label="待處理" />
136
+ <Checkbox defaultChecked label="進行中" />
137
+ <Checkbox label="已完成" />
138
+ </CheckboxGroup>
137
139
  </PopoverBody>
138
140
  {showFooter && (
139
141
  <PopoverFooter>
@@ -194,7 +196,7 @@ export const ColorMatrix: Story = {
194
196
  <tr><Td>次要文字</Td><Td><TokenCell token="--fg-secondary" display="text-fg-secondary" /></Td><Td>說明 / 次要資訊</Td></tr>
195
197
  <tr><Td>圓角</Td><Td mono>rounded-lg(8px)</Td><Td>對齊 Dialog radius(浮層視覺語言一致)</Td></tr>
196
198
  <tr><Td>陰影</Td><Td mono>--elevation-200</Td><Td>浮層級 elevation(見 `elevation.spec.md`)</Td></tr>
197
- <tr><Td>Density</Td><Td mono>data-density="md"</Td><Td>Popover 永遠鎖 md,不隨頁面 density 放大(lightweight)</Td></tr>
199
+ <tr><Td>Layout-space</Td><Td mono>data-layout-space="md"</Td><Td>只鎖版面間距(header py-tight 精簡);ui-size 跟 page → 內部控件對齊觸發點(2026-06-15 改,原 data-density master switch)</Td></tr>
198
200
  <tr><Td>Header/Body/Footer padding</Td><Td mono>px-[loose] py-[tight]</Td><Td>結構化 sub-components 採 Dialog 同一套 padding token</Td></tr>
199
201
  <tr><Td>Portal z-index</Td><Td mono>z-50</Td><Td>Radix Portal 統一層級,避開一般頁面內容</Td></tr>
200
202
  </tbody>
@@ -34,6 +34,7 @@ import {
34
34
  } from '@/design-system/components/Tooltip/tooltip'
35
35
  import { Button } from '@/design-system/components/Button/button'
36
36
  import { Checkbox } from '@/design-system/components/Checkbox/checkbox'
37
+ import { CheckboxGroup } from '@/design-system/components/Checkbox/checkbox-group'
37
38
 
38
39
  const meta: Meta = {
39
40
  title: 'Design System/Components/Popover/設計原則',
@@ -95,10 +96,11 @@ export const UsageGuidance: Story = {
95
96
  <PopoverContent align="start">
96
97
  <PopoverHeader><PopoverTitle>依狀態篩選</PopoverTitle></PopoverHeader>
97
98
  <PopoverBody>
98
- <div className="grid">
99
+ {/* CheckboxGroup zero-gap canonical(checkbox.spec.md L225)— 非手刻 grid div */}
100
+ <CheckboxGroup>
99
101
  <Checkbox defaultChecked label="進行中" />
100
102
  <Checkbox label="已完成" />
101
- </div>
103
+ </CheckboxGroup>
102
104
  </PopoverBody>
103
105
  </PopoverContent>
104
106
  </Popover>
@@ -168,12 +170,12 @@ export const UsageGuidance: Story = {
168
170
  <PopoverContent align="start" className="w-80">
169
171
  <PopoverHeader><PopoverTitle>進階篩選</PopoverTitle></PopoverHeader>
170
172
  <PopoverBody>
171
- <div className="grid">
173
+ <CheckboxGroup>
172
174
  <Checkbox defaultChecked label="我指派的" />
173
175
  <Checkbox label="我建立的" />
174
176
  <Checkbox label="我追蹤的" />
175
- <div className="border-t border-divider pt-3 mt-1 text-caption text-fg-muted">最多 3 個條件</div>
176
- </div>
177
+ </CheckboxGroup>
178
+ <div className="border-t border-divider pt-3 mt-1 text-caption text-fg-muted">最多 3 個條件</div>
177
179
  </PopoverBody>
178
180
  <PopoverFooter>
179
181
  <Button variant="tertiary" size="sm" className="flex-1">清除</Button>
@@ -217,10 +219,10 @@ export const UsageGuidance: Story = {
217
219
  </PopoverTrigger>
218
220
  <PopoverContent align="start">
219
221
  <PopoverBody>
220
- <div className="grid">
222
+ <CheckboxGroup>
221
223
  <Checkbox defaultChecked label="我的任務" />
222
224
  <Checkbox label="全部" />
223
- </div>
225
+ </CheckboxGroup>
224
226
  </PopoverBody>
225
227
  </PopoverContent>
226
228
  </Popover>
@@ -251,7 +253,7 @@ export const VisualAlignmentRule: Story = {
251
253
  <div>
252
254
  <Rule
253
255
  title="Popover 與 Dialog 共用 overlay-surface 視覺語言"
254
- note="bg-surface-raised / border-border / rounded-lg / elevation-200 完全一致。Header / Body / Footer 內 padding 來自 overlay-surface pattern 主檔(px-loose py-tight)。差異只有兩點:(1) Popover 是 non-modal 無 overlay 遮罩,(2) density 永遠鎖 md(不隨頁面 density 放大)"
256
+ note="bg-surface-raised / border-border / rounded-lg / elevation-200 完全一致。Header / Body / Footer 內 padding 來自 overlay-surface pattern 主檔(px-loose py-tight)。差異:(1) Popover 是 non-modal 無 overlay 遮罩,(2) Popover layout-space=md(header 精簡)、Dialog 鎖 layout-space=lg(寬鬆呼吸),兩者 ui-size 都繼承 page"
255
257
  >
256
258
  <Popover>
257
259
  <PopoverTrigger asChild>
@@ -62,9 +62,9 @@ Popover 是**點擊觸發的浮層容器**——提供定位、動畫、焦點
62
62
 
63
63
  > **跨家族 SSOT pointer**:PopoverHeader 屬 **Padding-based overlay header 家族**;border / padding / dismiss size / withTabs(tabs 進 PopoverHeader 時 border auto-suppress + tabs size sm)的跨家族視覺契約 SSOT 詳 `patterns/header-canonical/header-canonical.spec.md`。本節僅 codify Popover 特有 close X v5 unbounded canonical。
64
64
 
65
- **Close X 按鈕(2026-04-22 v5 unbounded canonical)**:所有 `PopoverHeader` 內建右上 X 按鈕,`<Button data-dismiss iconOnly dismiss size="sm" />` native sm(28 md / 32 lg)。透過 SurfaceHeader 的 v5 CSS rule 自動套負 margin 讓 layout 佔位;PopoverHeader 覆寫 `--chrome-slot-h:1.25rem` → 佔位 20(輕量 chrome,見 `popover.tsx` PopoverHeader docblock),非 Dialog 的 24。`hideClose` prop 可讓 composition 元件(如 Coachmark)選擇隱藏。詳 `patterns/overlay-surface/overlay-surface.spec.md`「Chrome dismiss size canonical」。
65
+ **Close X 按鈕(2026-04-22 v5 unbounded canonical)**:所有 `PopoverHeader` 內建右上 X 按鈕,`<Button data-dismiss iconOnly dismiss size="sm" />` native sm(28 md / 32 lg)。透過 SurfaceHeader 的 v5 CSS rule 自動套負 margin 讓 layout 佔位;PopoverHeader `COMPACT_HEADER_SLOT`(= calc(--font-body-size×1.5) = 21 = title line-box,衍生)→ 佔位 21(輕量 chrome,見 `popover.tsx` docblock),非 Dialog 的 24。`hideClose` prop 可讓 composition 元件(如 Coachmark)選擇隱藏。詳 `patterns/overlay-surface/overlay-surface.spec.md`「Chrome dismiss size canonical」。
66
66
 
67
- **Header / Footer 高度 canonical**:padding-based(繼承 SurfaceHeader / SurfaceFooter),高度 = max(child layout) + 2×tight。header:slot 佔位 20(`--chrome-slot-h` override)→ max(21 title, 20 slot) + 2×12 = **45**,自然比 Dialog / Sheet 48 輕一級(`popover.tsx` PopoverHeader docblock + overlay-surface.tsx 對照);footer:無 override,unbounded 佔位 24 → 48(若只有 unbounded)或自然長(若有 bounded)。詳 `tokens/uiSize/uiSize.spec.md`「Chrome header 選型 canonical」。
67
+ **Header / Footer 高度 canonical**:padding-based(繼承 SurfaceHeader / SurfaceFooter),高度 = max(child layout) + 2×tight。header:slot 佔位 21(`COMPACT_HEADER_SLOT` = title line-box,衍生)→ max(21 title, 21 slot) + 2×12 = **45**,自然比 Dialog / Sheet 48 輕一級(`popover.tsx` PopoverHeader docblock + overlay-surface.tsx 對照);footer:無 override,unbounded 佔位 24 → 48(若只有 unbounded)或自然長(若有 bounded)。詳 `tokens/uiSize/uiSize.spec.md`「Chrome header 選型 canonical」。
68
68
 
69
69
  ---
70
70
 
@@ -4,7 +4,7 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
4
4
  import { X as XIcon } from "lucide-react"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
- import { SurfaceHeader, SurfaceBody, SurfaceFooter } from "@/design-system/patterns/overlay-surface/overlay-surface"
7
+ import { SurfaceHeader, SurfaceBody, SurfaceFooter, COMPACT_HEADER_SLOT } from "@/design-system/patterns/overlay-surface/overlay-surface"
8
8
  import { Button } from "@/design-system/components/Button/button"
9
9
  import { OVERLAY_SIDE_OFFSET, OVERLAY_COLLISION_PADDING } from "@/design-system/tokens/elevation/overlay-geometry"
10
10
 
@@ -13,7 +13,7 @@ import { OVERLAY_SIDE_OFFSET, OVERLAY_COLLISION_PADDING } from "@/design-system/
13
13
  *
14
14
  * ── 視覺 ──
15
15
  * 與 Dialog 對齊:bg-surface-raised / rounded-lg / border-border / elevation-200。
16
- * density 永遠鎖 md(non-modal 輕量浮層不隨頁面 density 放大)。
16
+ * layout-space md(輕量浮層 header py-tight 保持精簡);ui-size 繼承 page(內部控件對齊觸發點,2026-06-15 改,原 data-density master switch)。
17
17
  *
18
18
  * ── 結構 ──
19
19
  * PopoverContent:外殼(bg / border / radius / shadow / density),無內距。
@@ -56,7 +56,11 @@ const PopoverContent = React.forwardRef<
56
56
  align={align}
57
57
  sideOffset={sideOffset}
58
58
  collisionPadding={collisionPadding}
59
- data-density="md"
59
+ // Layout-space lock(2026-06-15 canonical,原 data-density="md" 改為只鎖 layout-space):Popover 是
60
+ // 輕量浮層,header/footer 用 py-tight(layout-space)→ 鎖 layout-space=md 保持精簡 padding;但
61
+ // **ui-size 不鎖、繼承 page** → 內部 field 控件 / dismiss 按鈕隨 page 放大,跟觸發點一致(decouple)。
62
+ // [data-layout-space="md"] 有 reset selector(layoutSpace.css L31)→ lg page 上正確 reset 回 md。
63
+ data-layout-space="md"
60
64
  onOpenAutoFocus={onOpenAutoFocus ?? handlePopoverOpenAutoFocus}
61
65
  className={cn(
62
66
  "z-50 w-72 rounded-lg border border-border bg-surface-raised text-foreground shadow-[var(--elevation-200)] outline-none",
@@ -86,20 +90,19 @@ interface PopoverHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
86
90
 
87
91
  const PopoverHeader = React.forwardRef<HTMLDivElement, PopoverHeaderProps>(
88
92
  ({ className, children, hideClose = false, ...props }, ref) => (
89
- // Popover lightweight chrome canonical(2026-05-04 重思 v2):
90
- // 覆寫 `--chrome-slot-h: 1.25rem` (20px) unbounded button 佔位縮成 20,**匹配 PopoverTitle
91
- // text-body line-height (14×1.5≈21,floor 20)**。Header 維持 padding-based 自然撐開:
92
- // max(21 title, 20 slot) + py-tight(12*2) = 45 → 自然比 Dialog/Sheet 48 輕一級。
93
- // Q10 穩定:title-only / title+close 都 = title + py 主導,slot dominate
94
- // 無 min-h / 無 py override — 修正前一版過度設計。
93
+ // Popover lightweight chrome canonical(2026-05-04 重思 v2;2026-06-16 slot 改衍生 + 收斂 SSOT):
94
+ // `COMPACT_HEADER_SLOT`(= calc(--font-body-size×1.5) = 14×1.5 = 21 = PopoverTitle text-body line-box,
95
+ // 衍生非寫死,取代舊各自寫死的 1.25rem / 20 巧合值) slot = title 行高,title 字級改 slot 自動跟。
96
+ // Header 維持 padding-based:max(21 title, 21 slot) + py-tight(12*2) = 45 → 自然比 Dialog/Sheet 48 輕一級。
97
+ // Q10 穩定:title-only / title+close 都 = title + py 主導(slot=title)。無 min-h / 無 py override
95
98
  <SurfaceHeader
96
99
  ref={ref}
97
- className={cn("justify-between [--chrome-slot-h:1.25rem]", className)}
100
+ className={cn("justify-between", COMPACT_HEADER_SLOT, className)}
98
101
  {...props}
99
102
  >
100
103
  <div className="flex-1 min-w-0">{children}</div>
101
104
  {!hideClose && (
102
- // Dismiss X = native sm,SurfaceHeader 負 my trick 讓 layout 佔位 24 匹配 inner 24
105
+ // Dismiss X = native sm,SurfaceHeader 負 my trick 讓 layout 佔位縮到 slot(21 = title line-box)
103
106
  <PopoverPrimitive.Close asChild>
104
107
  <Button data-dismiss iconOnly dismiss size="sm" startIcon={XIcon} aria-label="關閉" />
105
108
  </PopoverPrimitive.Close>
@@ -609,8 +609,9 @@ export const SizeMatrix: Story = {
609
609
  <H3>Size token 對照</H3>
610
610
  <Desc>
611
611
  size 傳遞到浮層內的 CommandInput / MenuItem,三者統一尺寸。RowSizeProvider 確保所有 slot 自動讀取正確
612
- size,不需在每個 item 重設。注意:PopoverContent data-density="md"(popover.tsx),故 lg size 下
613
- --field-height-lg 仍解析為 md-default 36px(非 lg-density 40px),下表數值已反映此 overlay 鎖密度行為。
612
+ size,不需在每個 item 重設。size prop 決定 tier(sm/md/lg);實際像素隨 page density(2026-06-15:
613
+ PopoverContent 改鎖 `layout-space=md`、ui-size 繼承 page 下拉項目跟觸發點 / 頁面 density 一致,
614
+ 不再被鎖死 md-scale)。下表為 md page 的 field-height 值。
614
615
  </Desc>
615
616
  <div className="overflow-x-auto">
616
617
  <table className="text-caption border-collapse">
@@ -33,6 +33,7 @@ import { Button } from '@/design-system/components/Button/button'
33
33
  import { Field, FieldLabel } from '@/design-system/components/Field/field'
34
34
  import { Input } from '@/design-system/components/Input/input'
35
35
  import { Checkbox } from '@/design-system/components/Checkbox/checkbox'
36
+ import { CheckboxGroup } from '@/design-system/components/Checkbox/checkbox-group'
36
37
 
37
38
  const meta: Meta = {
38
39
  title: 'Design System/Components/Sheet/設計原則',
@@ -112,10 +113,10 @@ export const UsageGuidance: Story = {
112
113
  </Field>
113
114
  <Field>
114
115
  <FieldLabel>通知</FieldLabel>
115
- <div className="grid">
116
+ <CheckboxGroup>
116
117
  <Checkbox defaultChecked label="新任務" />
117
118
  <Checkbox label="每日摘要" />
118
- </div>
119
+ </CheckboxGroup>
119
120
  </Field>
120
121
  </SheetBody>
121
122
  <SheetFooter>
@@ -180,10 +181,10 @@ export const UsageGuidance: Story = {
180
181
  </PopoverTrigger>
181
182
  <PopoverContent align="start">
182
183
  <PopoverBody>
183
- <div className="grid">
184
+ <CheckboxGroup>
184
185
  <Checkbox defaultChecked label="進行中" />
185
186
  <Checkbox label="已完成" />
186
- </div>
187
+ </CheckboxGroup>
187
188
  </PopoverBody>
188
189
  </PopoverContent>
189
190
  </Popover>
@@ -715,6 +715,8 @@ Item-level default / hover / selected / disabled **色彩**完全共用 item-ana
715
715
  > 本節由 `scripts/add-reciprocal-pointers.mjs` 自動維護,列出在 SSOT 語境下指向本 spec 的其他 spec。若要手動補充,寫在本節之前。
716
716
 
717
717
  - `app-shell.spec.md`
718
+ - `density.spec.md`
719
+ - `header-canonical.spec.md`
718
720
  - `scroll-area.spec.md`
719
721
  - `sheet.spec.md`
720
722
  - `tree-view.spec.md`
@@ -455,7 +455,11 @@ function StepItemHeader({ children, className, style }: { children: React.ReactN
455
455
  onKeyDown={item.clickable ? onKeyDown : undefined}
456
456
  aria-disabled={item.disabled || undefined}
457
457
  className={cn(
458
- 'outline-none',
458
+ // leading-compact:scanning-family header 行高 = 1.3(item-anatomy.spec.md:776 掃描模式 label 行高)。
459
+ // 設在 header 而非 li 根 → prefix h-[1lh] + 水平 connector h-[1lh] + label(StepLabel 亦 leading-compact)
460
+ // 全用 1.3 對齊;li 根 text-body(1.5,steps.tsx:329-331)留給展開 content 的 reading 行高,不被
461
+ // scanning 波及(避免 改A壞B)。對齊 MenuItem 把 leading-compact 放 row 容器之原則(item-anatomy.tsx:144-146)。
462
+ 'outline-none leading-compact',
459
463
  item.clickable
460
464
  ? 'cursor-pointer rounded-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
461
465
  : 'cursor-not-allowed',
@@ -539,7 +543,9 @@ function VerticalConnectorLine() {
539
543
  <div
540
544
  aria-hidden
541
545
  className={cn(
542
- 'absolute w-px',
546
+ // leading-compact:connector 的 0.5lh 須跟 scanning label 同行高(1.3)→ 起點對齊 circle 中心
547
+ // (circle 對齊 label 第一行;label 已 leading-compact)。否則 connector 繼承 li 根 1.5 → 0.5lh 偏大 → 起點偏低。
548
+ 'absolute w-px leading-compact',
543
549
  isBlue ? 'bg-info' : 'bg-border',
544
550
  )}
545
551
  style={{
@@ -773,7 +779,9 @@ const StepLabel = React.forwardRef<HTMLSpanElement, StepLabelProps>(
773
779
  <span
774
780
  ref={ref}
775
781
  className={cn(
776
- 'font-medium break-words',
782
+ // leading-compact:scanning-family label 行高 = 1.3(item-anatomy.spec.md:776);text-body utility
783
+ // 自帶 lh:1.5,須顯式 leading-compact 蓋回 1.3,跟 MenuItem label(實測 14px/18px=1.3)一致。
784
+ 'font-medium break-words leading-compact',
777
785
  size === 'lg' ? 'text-body-lg' : 'text-body',
778
786
  disabled
779
787
  ? 'text-fg-disabled'
@@ -30,7 +30,9 @@ const TooltipContent = React.forwardRef<
30
30
  ref={ref}
31
31
  sideOffset={sideOffset}
32
32
  collisionPadding={collisionPadding}
33
- data-density="md"
33
+ // Density:繼承 page density(2026-06-15 canonical)。Tooltip padding 寫死 px-3 py-2、內容 text-body,
34
+ // 不消費任何 density / layout-space token → 鎖 density 對它是 inert(原 data-density="md" 是 409b91da
35
+ // a11y 批次「對齊 Popover」順手加,非設計決策)→ 移除,讓全浮層行為一致(全繼承 page)。
34
36
  className={cn(
35
37
  "z-50 overflow-hidden rounded-md px-3 py-2 text-body font-normal text-on-emphasis bg-tooltip max-w-[280px]",
36
38
  "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 motion-reduce:animate-none motion-reduce:zoom-in-100",