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

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 (60) 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/Tooltip/tooltip.d.ts.map +1 -1
  17. package/dist/components/Tooltip/tooltip.js +0 -1
  18. package/dist/components/Tooltip/tooltip.js.map +1 -1
  19. package/dist/patterns/overlay-surface/index.js +2 -1
  20. package/dist/patterns/overlay-surface/overlay-surface.d.ts +8 -0
  21. package/dist/patterns/overlay-surface/overlay-surface.d.ts.map +1 -1
  22. package/dist/patterns/overlay-surface/overlay-surface.js +3 -1
  23. package/dist/patterns/overlay-surface/overlay-surface.js.map +1 -1
  24. package/ds-canonical/hooks/tests/test_check_story_invariants.sh +1 -1
  25. package/ds-canonical/templates/dashboard-app.tsx +9 -4
  26. package/llms-full.txt +1 -1
  27. package/llms.txt +1 -1
  28. package/package.json +1 -1
  29. package/src/components/Alert/alert.spec.md +1 -1
  30. package/src/components/Alert/alert.tsx +7 -4
  31. package/src/components/Button/button.spec.md +2 -2
  32. package/src/components/Checkbox/checkbox.principles.stories.tsx +18 -15
  33. package/src/components/Coachmark/coachmark.principles.stories.tsx +3 -2
  34. package/src/components/DataTable/data-table-filter-panel.tsx +3 -3
  35. package/src/components/DataTable/data-table-sort-manager.tsx +3 -3
  36. package/src/components/Dialog/dialog.anatomy.stories.tsx +1 -1
  37. package/src/components/Dialog/dialog.spec.md +11 -11
  38. package/src/components/Dialog/dialog.tsx +7 -8
  39. package/src/components/DropdownMenu/dropdown-menu.tsx +4 -1
  40. package/src/components/FileItem/file-item.spec.md +1 -1
  41. package/src/components/FileItem/file-item.stories.tsx +3 -3
  42. package/src/components/FileViewer/file-viewer.anatomy.stories.tsx +4 -4
  43. package/src/components/FileViewer/file-viewer.spec.md +4 -4
  44. package/src/components/FileViewer/file-viewer.tsx +6 -3
  45. package/src/components/Notice/notice.anatomy.stories.tsx +4 -4
  46. package/src/components/Notice/notice.spec.md +1 -0
  47. package/src/components/Notice/notice.stories.tsx +4 -4
  48. package/src/components/Popover/popover.anatomy.stories.tsx +13 -11
  49. package/src/components/Popover/popover.principles.stories.tsx +10 -8
  50. package/src/components/Popover/popover.spec.md +2 -2
  51. package/src/components/Popover/popover.tsx +14 -11
  52. package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +3 -2
  53. package/src/components/Sheet/sheet.principles.stories.tsx +5 -4
  54. package/src/components/Sidebar/sidebar.spec.md +2 -0
  55. package/src/components/Tooltip/tooltip.tsx +3 -1
  56. package/src/patterns/header-canonical/header-canonical.spec.md +3 -2
  57. package/src/patterns/overlay-surface/overlay-surface.spec.md +12 -10
  58. package/src/patterns/overlay-surface/overlay-surface.tsx +20 -8
  59. package/src/tokens/density/density.spec.md +33 -22
  60. package/src/tokens/layoutSpace/layoutSpace.stories.tsx +4 -4
@@ -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`
@@ -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",
@@ -259,14 +259,14 @@ ChromeHeader / SurfaceHeader 新增 `tabsSlot?: ReactNode` prop。提供時自
259
259
  <ChromeHeader
260
260
  withTabs?: boolean // false (default) | true → auto-suppress border + delegate to TabsList
261
261
  tabsSlot?: ReactNode // Tabs row(column mode + W1-W6 lockstep,2026-05-18 ship)
262
- lockDensity?: 'inherit' | 'lg' // 'inherit' (default) | 'lg' 強制 chrome-header-height lg=56(viewer fullscreen chrome)
262
+ lockDensity?: 'inherit' | 'lg' // 'inherit' (default)'lg' = header 上設 data-density=lg;⚠️ 2026-06-15:density lock 應設在 surface 根而非 header(header 鎖、body 沒鎖 → 左緣不對齊,FileViewer 圖二教訓)→ 現無 consumer 使用,保留僅為 API 相容。見 density.spec.md 消費者清單 + anti-pattern
263
263
  leadingRail?: ReactNode // 2026-05-21 ship per AppShell primary-header globalHeader:固定 width=sidebar-width-icon 的左 rail,內 justify-center 排列(toggle/logo 跟 sidebar 收合 icon center x 對齊)。對齊 GitHub global nav 左 logo 區 / Slack thin workspace rail 慣例。
264
264
  >
265
265
  {children}
266
266
  </ChromeHeader>
267
267
  ```
268
268
 
269
- **不開**:`density?: 'md'|'lg'` 自由 prop(M21 違反 — 任意 density 等於 cva-on-pattern,該由 density context 決定)。`lockDensity="lg"` escape hatch fullscreen viewer chrome(FileViewer 永遠 lg-equivalent design intent),其餘 consumer 走 `inherit`(跟 page density)。**Grep evidence**(historical 錨,migration 已完成):migration 前 `Sidebar` + `FileViewer Toolbar/InfoPanel` 各自手刻同一 signature `flex items-center gap-2 shrink-0 h-[var(--chrome-header-height)] border-b border-divider px-[var(--layout-space-loose)]`(production 重複 contract,非 premature abstraction)→ 過 M21 prop variant test 3 條(`<Sidebar>` 等近親 grep / 3 家世界級 page-header primitive / value 結構不同)→ 抽成本 primitive。**現況**:全部已消費 `<ChromeHeader>`(`sidebar.tsx:479` / `file-viewer.tsx:328` Toolbar + `:459` InfoPanel),grep 全 repo 已無此手刻 signature(除 `chrome-header.tsx` primitive 本身)。
269
+ **不開**:`density?: 'md'|'lg'` 自由 prop(M21 違反 — 任意 density 等於 cva-on-pattern,該由 density context 決定)。`lockDensity="lg"` 原為 fullscreen viewer escape hatch,但 **2026-06-15 起無 consumer 使用** — FileViewer 改為全 surface 繼承 page density(原本只鎖兩個 header、沒鎖 body → 左緣不對齊,圖二 bug)。density lock 一律設在 **surface 根**而非 header primitive(見 `density.spec.md` anti-pattern)。其餘 consumer 走 `inherit`(跟 page density)。**Grep evidence**(historical 錨,migration 已完成):migration 前 `Sidebar` + `FileViewer Toolbar/InfoPanel` 各自手刻同一 signature `flex items-center gap-2 shrink-0 h-[var(--chrome-header-height)] border-b border-divider px-[var(--layout-space-loose)]`(production 重複 contract,非 premature abstraction)→ 過 M21 prop variant test 3 條(`<Sidebar>` 等近親 grep / 3 家世界級 page-header primitive / value 結構不同)→ 抽成本 primitive。**現況**:全部已消費 `<ChromeHeader>`(`sidebar.tsx:479` / `file-viewer.tsx:328` Toolbar + `:459` InfoPanel),grep 全 repo 已無此手刻 signature(除 `chrome-header.tsx` primitive 本身)。
270
270
 
271
271
  ### Layer 4 — Hook mechanical
272
272
 
@@ -313,6 +313,7 @@ ChromeHeader / SurfaceHeader 新增 `tabsSlot?: ReactNode` prop。提供時自
313
313
  > 本節由 `scripts/add-reciprocal-pointers.mjs` 自動維護,列出在 SSOT 語境下指向本 spec 的其他 spec。若要手動補充,寫在本節之前。
314
314
 
315
315
  - `app-shell.spec.md`
316
+ - `density.spec.md`
316
317
  - `dialog.spec.md`
317
318
  - `file-viewer.spec.md`
318
319
  - `item-anatomy.spec.md`
@@ -236,13 +236,13 @@ Overlay family 的 header title typography 依「modal vs non-modal」分級,**
236
236
 
237
237
  ## Size canonical(per-overlay default size SSOT,2026-05-04)
238
238
 
239
- > **背景**:Popover / HoverCard 是「輕量浮層」(chrome 45 / slot 20 / title 小一級 / density 鎖 md);Dialog / Sheet 是「heavy commitment chrome」(chrome 48 / slot 24)。Per-surface body+footer button/field default size 跟「輕量 vs heavy」分化對應。
239
+ > **背景**:Popover / HoverCard 是「輕量浮層」(chrome 45 / slot 21 / title 小一級 / density 鎖 md);Dialog / Sheet 是「heavy commitment chrome」(chrome 48 / slot 24)。Per-surface body+footer button/field default size 跟「輕量 vs heavy」分化對應。
240
240
  >
241
241
  > **依據**:Linear / Notion / Airtable / Carbon / Material Menu / Atlassian Popup popover bodies 共識用 sm-density;Material AlertDialog / Carbon Modal / shadcn Dialog 共識用 md。world-class 雙派分明依「浮層密度」分。 <!-- @benchmark-unverified -->
242
242
 
243
243
  | Overlay | Header chrome | Body field controls | Body inline action(drag handle / trash / +CTA)| Footer action Buttons |
244
244
  |--|--|--|--|--|
245
- | **Popover** | unbounded sm → 20 slot(輕)| **sm**(輕量 dense info)| inline-action sm/md = **16+18** | **sm** |
245
+ | **Popover** | unbounded sm → 21 slot(輕)| **sm**(輕量 dense info)| inline-action sm/md = **16+18** | **sm** |
246
246
  | **HoverCard** | (同 Popover)| **sm** | 同 | **sm** |
247
247
  | **Dialog** | unbounded sm → 24 slot | **md** | inline-action sm/md = 16+18 | **md** |
248
248
  | **Sheet** | (同 Dialog)| **md** | 同 | **md** |
@@ -264,13 +264,13 @@ Overlay family 的 header title typography 依「modal vs non-modal」分級,**
264
264
 
265
265
  ## Chrome dismiss size canonical
266
266
 
267
- **User 設計 insight**:header 的 padding-based sizing 在 **unbounded button**(text variant / dismiss,無 bg/border)場景視覺 padding 過大;在 **bounded button** 則剛好。解法 = **保持 button native size 不變(touch target / 視覺 render 都是 sm 原尺寸),但 layout 佔位縮回 xs(24)** via 負 margin。
267
+ **User 設計 insight**:header 的 padding-based sizing 在 **unbounded button**(text variant / dismiss,無 bg/border)場景視覺 padding 過大;在 **bounded button** 則剛好。解法 = **保持 button native size 不變(touch target / 視覺 render 都是 sm 原尺寸),但 layout 佔位縮到 title line-box(24,衍生自 --font-body-lg-size×1.5)** via 負 margin。
268
268
 
269
269
  **Canonical**:button native size **保留 sm**(touch target / 視覺 render 不動);**unbounded 的靠 CSS 負 my 把 layout 佔位縮到 `--chrome-slot-h`** ≤ title line-height,讓 title 主導 chrome 高度。
270
270
 
271
- | Button 類型 | 判定(button.tsx L429-430)| Trick 套用 | Layout 佔位 | Dialog/Sheet header (slot 24)| Popover header (slot 20)|
271
+ | Button 類型 | 判定(button.tsx L429-430)| Trick 套用 | Layout 佔位 | Dialog/Sheet header (slot 24)| Popover header (slot 21)|
272
272
  |--|--|--|--|--|--|
273
- | **Unbounded** | `variant === 'text'` OR `dismiss` → `data-unbounded="true"` | ✓ 套負 my | = `var(--chrome-slot-h)` | max(24, 24) + py-tight = **48** | max(21, 20) + py-tight = **45** |
273
+ | **Unbounded** | `variant === 'text'` OR `dismiss` → `data-unbounded="true"` | ✓ 套負 my | = `var(--chrome-slot-h)` | max(24, 24) + py-tight = **48** | max(21, 21) + py-tight = **45** |
274
274
  | **Bounded** | `primary` / `tertiary` / `outline` 等(有 bg/border) | ✗ 不套 | = native sm (28 md / 32 lg) | max(24, 28) + py-tight = **52**(自然長)| max(21, 28) + py-tight = **52**(自然長)|
275
275
 
276
276
  **為什麼差別**:Unbounded 沒視覺邊界 → native 28 是純 hit-target padding,縮 layout 不損視覺;Bounded 的 bg/border 就是內容,縮會切掉 → 必須讓 chrome 自然長高,**這是設計宣告「此 chrome 有重要 action,視覺重量該配得上」**。
@@ -281,13 +281,15 @@ Overlay family 的 header title typography 依「modal vs non-modal」分級,**
281
281
  // button.tsx ── 自動標記 data-unbounded
282
282
  const unboundedAttr = resolvedVariant === 'text' || dismiss ? { 'data-unbounded': 'true' } : {}
283
283
 
284
- // overlay-surface.tsx ── slot 透過 CSS var 參數化(2026-05-04 v3 Q10)
284
+ // overlay-surface.tsx ── slot 透過 CSS var 參數化(2026-05-04 v3;2026-06-16 default 改衍生)
285
285
  const CHROME_UNBOUNDED_SLOT =
286
- '[&_[data-unbounded]]:my-[calc((var(--chrome-slot-h,var(--field-height-xs))-var(--field-height-sm))/2)]'
286
+ '[&_[data-unbounded]]:my-[calc((var(--chrome-slot-h,calc(var(--font-body-lg-size)*1.5))-var(--field-height-sm))/2)]'
287
287
 
288
- // SurfaceHeader default → slot = field-height-xs (24)
289
- // PopoverHeader overrideclassName "[--chrome-slot-h:1.25rem]" (20)
290
- // 公式 = (slot - native) / 2 ── md unbounded sm: (24-28)/2 = -2px / Popover (20-28)/2 = -4px
288
+ // SurfaceHeader default → slot 衍生自 title typography = calc(--font-body-lg-size × 1.5) = 24
289
+ // (2026-06-16:從寫死 field-height-xs 改衍生title 字級改了 slot 自動跟、不靠巧合;M17 SSOT;
290
+ // 1.5 = typography.spec body 行高常數,非 token inline,body 行高改須同步)
291
+ // Popover-tier override → COMPACT_HEADER_SLOT(= calc(--font-body-size × 1.5) = 21,衍生自 text-body title)
292
+ // 公式 = (slot - native) / 2 ── Dialog md: (24-28)/2 = -2px / Popover md: (21-28)/2 = -3.5px
291
293
  ```
292
294
 
293
295
  **覆蓋範圍**:
@@ -51,18 +51,30 @@ import { HEADER_TABS_SLOT_WRAPPER_CLASS } from '@/design-system/patterns/header-
51
51
  // 時標記)的 **native size 不變**(sm: 28 md / 32 lg),但 **layout 佔位** via 負 `my` 縮成 `--chrome-slot-h`。
52
52
  //
53
53
  // **Slot height = header title 的 line-height**(讓 button 不 dominate,title 自然撐 header)
54
- // - Default `var(--field-height-xs)` = 24,匹配 Dialog/Sheet text-body-lg (16 × 1.5 = 24)
55
- // - Popover/Coachmark override `--chrome-slot-h: 1.25rem` (20),匹配 text-body (14 × 1.5 ≈ 21,floor 20)
54
+ // - Default 衍生自 title typography:`calc(var(--font-body-lg-size)*1.5)` = 16×1.5 = 24(Dialog/Sheet title = text-body-lg)
55
+ // 2026-06-16:從寫死 `var(--field-height-xs)`(巧合也=24)改為衍生 title 字級若改,slot 自動跟、不靠巧合(M17 SSOT;
56
+ // 1.5 = typography.spec body 行高常數,非 token 故 inline,若 body 行高改 1.3 須同步此處)。
57
+ // - Popover-tier(title = text-body)走 `COMPACT_HEADER_SLOT`(見下)= calc(--font-body-size×1.5) = 14×1.5 = 21
58
+ // (= PopoverTitle text-body line-box,衍生非寫死;2026-06-16 從各自寫死 1.25rem/20 巧合值收斂進 SSOT)
56
59
  //
57
- // Header 永遠 padding-based(無 min-h),但因 slot title line-height,header 高度由 title 主導:
60
+ // Header 永遠 padding-based(無 min-h),但因 slot = title line-box,header 高度由 title 主導:
58
61
  // - Dialog: max(24 title, 24 slot) + py-tight(12*2)= 48 ✓ chrome-header-height
59
- // - Popover: max(21 title, 20 slot) + py-tight(12*2)= 45 ✓ 自然輕量
60
- // Q10 穩定性:title-only / title+button / refresh in/out 全 case header 高度 = title + py(slot 不 dominate)
62
+ // - Popover: max(21 title, 21 slot) + py-tight(12*2)= 45 ✓ 自然輕量
63
+ // Q10 穩定性:title-only / title+button / refresh in/out 全 case header 高度 = title + py(slot=title,不超過)
61
64
  //
62
65
  // 負 my 公式:(slot - native) / 2,density-aware:
63
66
  // Dialog md: (24 - 28) / 2 = -2px; lg: (24 - 32) / 2 = -4px
64
- // Popover md: (20 - 28) / 2 = -4px
65
- const CHROME_UNBOUNDED_SLOT = '[&_[data-unbounded]]:my-[calc((var(--chrome-slot-h,var(--field-height-xs))-var(--field-height-sm))/2)]'
67
+ // Popover md: (21 - 28) / 2 = -3.5px(sub-pixel,可接受)
68
+ const CHROME_UNBOUNDED_SLOT = '[&_[data-unbounded]]:my-[calc((var(--chrome-slot-h,calc(var(--font-body-lg-size)*1.5))-var(--field-height-sm))/2)]'
69
+
70
+ /**
71
+ * 輕量浮層 header 的 slot override —— **SSOT,禁各自 inline**(2026-06-16 收斂)。
72
+ * Popover-tier header(title = PopoverTitle `text-body`)的 slot = body line-box = `calc(--font-body-size×1.5)` = 21,
73
+ * 衍生自 title typography → title 字級改,slot 自動跟(取代舊各自寫死的 1.25rem / 20 巧合值)。
74
+ * 消費者(同設計邏輯,皆 raw SurfaceHeader + PopoverTitle):PopoverHeader / DataTable sort·filter panel /
75
+ * FileItem upload-manager。Default slot(Dialog/Sheet,title = text-body-lg)= 24,在 CHROME_UNBOUNDED_SLOT fallback。
76
+ */
77
+ export const COMPACT_HEADER_SLOT = '[--chrome-slot-h:calc(var(--font-body-size)*1.5)]'
66
78
 
67
79
  export interface SurfaceHeaderProps
68
80
  extends React.HTMLAttributes<HTMLDivElement> {
@@ -136,7 +148,7 @@ export const SurfaceHeader = React.forwardRef<
136
148
 
137
149
  // Padding-based(預設) — Dialog/Sheet 用 body-lg title (16/24),自然撐 max(24 title, 24 button slot) = 24
138
150
  // → header = 24 + py-tight 12×2 = 48 chrome-header-height ✓ 穩定無需 min-h
139
- // Popover 等輕量 chrome 走 PopoverHeader override(`[--chrome-slot-h:1.25rem]` = 20 slot,無 min-h / 無 py override):max(21 title, 20 slot) + py-tight 12×2 = 45(見上方 L57)
151
+ // Popover 等輕量 chrome 走 COMPACT_HEADER_SLOT(slot = calc(--font-body-size×1.5)=21 = title line-box,無 min-h / 無 py override):max(21 title, 21 slot) + py-tight 12×2 = 45(見上方 L57)
140
152
  //
141
153
  // withTabs=true(無 tabsSlot,backward compat):移除 border-b,consumer 自畫
142
154
  return (
@@ -37,22 +37,21 @@ Density 由**兩個獨立維度**構成,並配合一個 convenience attribute
37
37
 
38
38
  當需要「版面間距寬鬆(layout-space)+ 控件高度標準(ui-size)」時,**顯式設兩個 attribute**:
39
39
 
40
- ### Canonical 情境 1 — 歷史備忘(Dialog 已撤回)
40
+ ### Canonical 情境 1 — Overlay 的 layout-space tier(2026-06-15 重訂)
41
41
 
42
- **歷史:** Dialog 先前設 `data-layout-space="lg"` header/body 寬鬆呼吸,但跟 `--chrome-header-height` canonical(md=48)衝突(強設 lg 會變 56)。**已於 2026-04-22 v5 撤回**,Dialog 全盤繼承 page density(跟 Sheet 對齊)。
42
+ **核心原則**:浮層(Portal overlay)**預設全繼承 page density**;只鎖「**有設計理由 + 該元件真的消費到**」的那個 dial,**禁用 `data-density` master switch 鎖浮層**(它連 ui-size 一起鎖 內部控件 / 選單 item 對不上觸發點)。**ui-size 永遠跟 page** —— 浮層只決定「版面間距 tier」(layout-space),控件大小交給 page
43
43
 
44
- **本情境仍有效的使用時機**:若未來有新元件需要「page density = md 但此元件 chrome 要 lg padding」,可用 `data-layout-space="lg"`(不撐高 header 的 rationale 仍成立,前提是 chrome-header-height 對齊不是需求)。
44
+ | 浮層 | | 理由 |
45
+ |------|----|------|
46
+ | Dialog | **不鎖(繼承 page)** | layout-space 本 spec 第 10 行定義即管「dialog body padding」,鎖死 = override 自家 dial 自相矛盾;modal 寬鬆需求在 lg 階自然滿足(md 16 = Polaris 下限合格);button 不撐高由 ui-size 繼承解決。有同類 dial 的 SAP/Cloudscape modal 跟 dial、不鎖 |
47
+ | Popover / Coachmark | `data-layout-space="md"` | 輕量浮層 → header py-tight 保持精簡;ui-size 跟 page 讓內部 field 控件對齊觸發點。Coachmark 建在 Popover 上自動繼承 |
48
+ | Tooltip / DropdownMenu | 不鎖(繼承 page) | 不消費任何 layout-space token(Tooltip padding 寫死 px-3 py-2;Dropdown 是 RowSize + py-2);鎖 ui-size 會把選單 item(field-height-{size})釘在 md-scale → 對不上 lg 觸發點 |
49
+ | Sheet | 不鎖(繼承 page) | 側板,螢幕變小時常駐側板會 responsive 變 Sheet;若 Sheet 鎖 tier 而側板繼承 page → 切換瞬間跳 mode,不一致 |
50
+ | FileViewer | 不鎖(繼承 page) | 全螢幕 chrome 殼。原 `lockDensity="lg"` 只鎖在兩個 ChromeHeader、沒鎖 body → header(px-loose@lg=24)與 body(px-loose@page=16)左緣不對齊;移除 → 全 surface 同密度 |
45
51
 
46
- ```tsx
47
- // 假設未來某元件需要 lg 呼吸 + page-default ui-size
48
- <SomeContent data-layout-space="lg">
49
- <Button size="sm" /> {/* 仍是 28px md */}
50
- </SomeContent>
51
- ```
52
+ **Tier alignment**:每個 overlay 宣告自己的 layout-space tier → header 高度隨之(Dialog lg → title 24 + py-tight 16×2 = 56;Popover md → 45)。「對齊 `--chrome-header-height`」是**同 tier 內**成立(Popover 45 ≠ Dialog 56 ≠ chrome 48 本就允許,各宣告各 tier),非跨 page 強制。詳 `patterns/header-canonical/header-canonical.spec.md` A 家族。
52
53
 
53
- **實際 v5 canonical(Dialog / Sheet / Popover)**:
54
- - Dialog / Sheet:繼承 page density(無 override)
55
- - Popover / DropdownMenu / Tooltip:`data-density="md"` 鎖定(Portal 逃逸 + compact 語意)
54
+ **為何不鎖 ui-size**:field-height token 隨 density 變(md 28/32/36 lg 32/36/40);鎖 ui-size=md 會把浮層內控件 / 選單 item 釘死 md-scale,lg page 上對不上 lg 觸發點。decouple = 只鎖版面間距、控件跟 page。對標 Carbon spacing scale 獨立於 control size / GitHub Primer 8px scale 獨立。
56
55
 
57
56
  ### Canonical 情境 2 — 單獨切 ui-size
58
57
 
@@ -94,24 +93,36 @@ document.documentElement.setAttribute('data-layout-space', 'md')
94
93
  - 是 → 用 `data-density="X"`(convenience)
95
94
  - 否(想解耦) → 明示 `data-layout-space="X"` + / 或 `data-ui-size="Y"`
96
95
 
97
- 3. **Portal 逃逸 subtree?**(Dialog / Popover / Sheet / DropdownMenu)
98
- - Portal body 的元件**不繼承 trigger density** 必自設(對齊 Meta-Pattern M3)
96
+ 3. **Portal 逃逸 subtree?**(Dialog / Popover / Sheet / DropdownMenu / Tooltip)
97
+ - density 設在 `html` root 時,CSS cascade 會傳到 portal 出去的內容(portal body,仍在 html 底下)→ **浮層繼承 page density 正常,不需自設**。
98
+ - 只有 density 設在「中層 div」時 portal 才逃逸該 subtree(M3 邊界)。
99
+ - **鎖浮層 density/layout-space = 刻意覆蓋成某 tier,需設計理由(見情境 1 表)**,非 Portal 必要;且只鎖該元件真正消費的 dial。
99
100
 
100
101
  ## 消費者清單
101
102
 
102
103
  | 元件 | attribute 設置 | 用法理由 |
103
104
  |------|---------------|---------|
104
- | Dialog | (繼承 page,v5 校準)| Sheet 對齊;header 高度 = `--chrome-header-height` 自動對齊(md=48 / lg=56);原 `data-layout-space="lg"` 已於 2026-04-22 撤回(衝突 chrome-header canonical) |
105
- | Sheet | 無(繼承 page) | Sheet 繼承 page density,不 lock |
106
- | Popover | `data-density="md"` | Portal 逃逸,且 popover 語意「compact」兩維度同鎖 md |
107
- | DropdownMenu | `data-density="md"` | Popover |
108
- | Tooltip | `data-density="md"` | Popover |
109
- | Sidebar | 無(繼承 page) | Sidebar 是 inline chrome(非 Portal 逃逸),跟隨 page density;size="md" density="lg" 下自動變 36px row(見 sidebar.spec.md「Row size 跟 density 的差別」)|
105
+ | Dialog | **無(繼承 page,2026-06-16)** | layout-space 定義即管 dialog body padding → 鎖 = override 自家 dial 自相矛盾;header 隨 page md=48 / lg=56;有同類 dial SAP/Cloudscape modal dial |
106
+ | Sheet | 無(繼承 page) | 側板;responsive 會跟常駐側板互換,不能跳 mode |
107
+ | Popover / Coachmark | `data-layout-space="md"`(2026-06-15) | 輕量;header py-tight 精簡;ui-size 繼承 page → 內部控件對齊觸發點。Coachmark 建在 Popover 上自動繼承 |
108
+ | DropdownMenu | 無(繼承 page,2026-06-15) | 不消費 layout-space token;鎖 ui-size 會讓 menu item(field-height-{size})對不上觸發點 |
109
+ | Tooltip | 無(繼承 page,2026-06-15) | 不消費任何 density/layout-space token,鎖 = inert |
110
+ | FileViewer | 無(繼承 page,2026-06-15) | 全螢幕 chrome 殼;原 lockDensity 只鎖 header 造成左緣不對齊 移除 |
111
+ | Sidebar | 無(繼承 page) | inline chrome(非 Portal 逃逸),跟隨 page density;size="md" 在 density="lg" 下自動變 36px row(見 sidebar.spec.md)|
110
112
 
111
- **邊界:page density=lg 下的 Portal overlay**——Popover / DropdownMenu / Tooltip `data-density="lg"` page 上仍鎖 `md`(code:各 Content 自設 `data-density="md"`)。這不是漏接:Portal body 本就不繼承 subtree density(M3),鎖 md overlay「compact」語意的刻意設計(2026-04-22 v5 canonical)
113
+ **邊界:page density=lg 下的 Portal overlay**——Tooltip / DropdownMenu 繼承 page lg page 上隨 page 放大,選單 item 對齊 lg 觸發點。Popover / Coachmark 鎖 layout-space=md(header 精簡) ui-size 仍繼承 lg(內部控件放大)。Dialog 繼承 page(不鎖) md 16 / lg 24 padding 隨 page。皆透過 `[data-layout-space="md|lg"]` reset selector(layoutSpace.css L21/L31)在任意 page density 上正確解析。
112
114
 
113
115
  ## Anti-patterns(禁止)
114
116
 
115
117
  - ❌ 元件同時設 `data-density` + `data-ui-size`(重複,以後者為準但混亂)
116
- - ❌ Overlay Portal 元件不自設 density(Portal body 不繼承 trigger Meta-Pattern M3)
118
+ - ❌ **用 `data-density` master switch 鎖浮層**( ui-size 一起鎖 內部控件 / 選單 item 對不上觸發點)。只鎖該元件真正消費的 dial(見情境 1 表)
119
+ - ❌ **把 density/layout-space lock 設在 header primitive(ChromeHeader / SurfaceHeader)而非 surface 根**——header 鎖、body 沒鎖 → 左緣不對齊(FileViewer 圖二 bug)。lock 一律放 surface 根容器
117
120
  - ❌ 為了追求表面一致性硬把 Dialog button 綁 lg ui-size(犧牲 header 高度 / strapline 彈性)
121
+ - ❌ **鎖 Dialog 的 layout-space**(2026-06-16 定論)—— 本 spec 第 10 行定義 layout-space 就管「dialog body padding」,鎖 Dialog = 令自家 dial 對它點名要管的對象失效 = 自相矛盾;有同類 dial 的 SAP/Cloudscape 都讓 modal 跟 page dial。modal 寬鬆需求在 lg 階自然滿足(md 16 = Polaris 下限),不靠鎖。Popover/Coachmark 鎖 md 是另一回事(空間效率、消費 layout-space token、且為非阻斷錨定浮層)
122
+
123
+ ## 被引用(auto-maintained,Dim 3 reciprocal audit)
124
+
125
+ > 本節由 `scripts/add-reciprocal-pointers.mjs` 自動維護,列出在 SSOT 語境下指向本 spec 的其他 spec。若要手動補充,寫在本節之前。
126
+
127
+ - `file-viewer.spec.md`
128
+ - `header-canonical.spec.md`
@@ -56,15 +56,15 @@ export const Overview: Story = {
56
56
  Pattern 層級間距 token,跟 density 連動(md / lg 兩個值)。下表展示 md density 預覽。
57
57
  </p>
58
58
 
59
- <SpaceRow utility="--layout-space-tight" mdValue="8px" lgValue="12px" usage="header / footer 上下 padding;緊密 inline gap" />
59
+ <SpaceRow utility="--layout-space-tight" mdValue="12px" lgValue="16px" usage="header / footer 上下 padding;緊密 inline gap" />
60
60
  <SpaceRow utility="--layout-space-loose" mdValue="16px" lgValue="24px" usage="surface 左右 padding;tabs trigger gap;form field 間距" />
61
- <SpaceRow utility="--layout-space-bottom" mdValue="24px" lgValue="32px" usage="Dialog / Sheet body 底部多一拍" />
61
+ <SpaceRow utility="--layout-space-bottom" mdValue="48px" lgValue="48px" usage="Dialog / Sheet body 底部多一拍 + modal viewport inset(固定,不隨 density)" />
62
62
 
63
63
  <h3 className="text-h5 mt-8 mb-2">使用規則</h3>
64
64
  <ul className="list-disc pl-6 text-body text-fg-secondary space-y-1">
65
- <li>跟 block 元素相鄰 → <code>tight</code>(8/12)</li>
65
+ <li>跟 block 元素相鄰 → <code>tight</code>(12/16)</li>
66
66
  <li>inline ↔ inline parallel → <code>loose</code>(16/24)</li>
67
- <li>容器最底部 → <code>bottom</code>(24/32)</li>
67
+ <li>容器最底部 → <code>bottom</code>(48 固定)</li>
68
68
  </ul>
69
69
 
70
70
  <p className="text-caption text-fg-muted mt-6">