@qijenchen/design-system 0.1.0-beta.75 → 0.1.0-beta.76

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 (132) hide show
  1. package/CLAUDE.md +1 -1
  2. package/dist/components/AppShell/app-shell.d.ts +2 -2
  3. package/dist/components/AppShell/app-shell.js.map +1 -1
  4. package/dist/components/Avatar/avatar.js.map +1 -1
  5. package/dist/components/Button/button.d.ts.map +1 -1
  6. package/dist/components/Button/button.js.map +1 -1
  7. package/dist/components/Chart/chart.d.ts +1 -1
  8. package/dist/components/Chart/chart.js.map +1 -1
  9. package/dist/components/Checkbox/checkbox.d.ts +1 -1
  10. package/dist/components/Checkbox/checkbox.js +1 -1
  11. package/dist/components/Checkbox/checkbox.js.map +1 -1
  12. package/dist/components/Combobox/combobox.d.ts +1 -1
  13. package/dist/components/Combobox/combobox.d.ts.map +1 -1
  14. package/dist/components/Combobox/combobox.js +13 -10
  15. package/dist/components/Combobox/combobox.js.map +1 -1
  16. package/dist/components/Command/command.d.ts +1 -1
  17. package/dist/components/Command/command.js +3 -3
  18. package/dist/components/Command/command.js.map +1 -1
  19. package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
  20. package/dist/components/DataTable/cell-registry.js +2 -2
  21. package/dist/components/DataTable/cell-registry.js.map +1 -1
  22. package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
  23. package/dist/components/DatePicker/date-picker.js +2 -2
  24. package/dist/components/DatePicker/date-picker.js.map +1 -1
  25. package/dist/components/DescriptionList/description-list.d.ts +1 -1
  26. package/dist/components/DescriptionList/description-list.js +2 -2
  27. package/dist/components/DescriptionList/description-list.js.map +1 -1
  28. package/dist/components/Empty/empty.d.ts +2 -0
  29. package/dist/components/Empty/empty.d.ts.map +1 -1
  30. package/dist/components/Empty/empty.js.map +1 -1
  31. package/dist/components/Field/field-wrapper.js +4 -4
  32. package/dist/components/Field/field-wrapper.js.map +1 -1
  33. package/dist/components/OverflowIndicator/overflow-indicator.d.ts +1 -1
  34. package/dist/components/OverflowIndicator/overflow-indicator.js +2 -2
  35. package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
  36. package/dist/components/PeoplePicker/people-picker.js +2 -2
  37. package/dist/components/PeoplePicker/people-picker.js.map +1 -1
  38. package/dist/components/ProfileCard/profile-card.d.ts +1 -1
  39. package/dist/components/ProfileCard/profile-card.js +2 -1
  40. package/dist/components/ProfileCard/profile-card.js.map +1 -1
  41. package/dist/components/Rating/rating.js.map +1 -1
  42. package/dist/components/Select/select.js +4 -4
  43. package/dist/components/Select/select.js.map +1 -1
  44. package/dist/components/Textarea/textarea.d.ts +1 -1
  45. package/dist/components/Textarea/textarea.js +2 -2
  46. package/dist/components/Textarea/textarea.js.map +1 -1
  47. package/dist/components/TimePicker/time-picker.d.ts.map +1 -1
  48. package/dist/components/TimePicker/time-picker.js +14 -23
  49. package/dist/components/TimePicker/time-picker.js.map +1 -1
  50. package/dist/components/TreeView/tree-view.d.ts +1 -1
  51. package/dist/components/TreeView/tree-view.js +1 -1
  52. package/dist/components/TreeView/tree-view.js.map +1 -1
  53. package/ds-canonical/fork/governance.lock +1 -1
  54. package/ds-canonical/fork/preamble.md +2 -2
  55. package/ds-canonical/hooks/check_field_controls_contracts.sh +30 -3
  56. package/ds-canonical/references/props-naming.md +15 -1
  57. package/ds-canonical/rules/ui-development.md +2 -2
  58. package/llms-full.txt +7 -2
  59. package/llms.txt +3 -3
  60. package/package.json +1 -1
  61. package/src/components/Alert/alert.anatomy.stories.tsx +4 -4
  62. package/src/components/AppShell/app-shell.spec.md +4 -4
  63. package/src/components/AppShell/app-shell.tsx +2 -2
  64. package/src/components/Avatar/avatar.tsx +1 -1
  65. package/src/components/Breadcrumb/breadcrumb.spec.md +11 -1
  66. package/src/components/Button/button.tsx +0 -10
  67. package/src/components/Calendar/calendar.anatomy.stories.tsx +1 -1
  68. package/src/components/Chart/chart.tsx +1 -1
  69. package/src/components/Checkbox/checkbox.tsx +1 -1
  70. package/src/components/Coachmark/coachmark.anatomy.stories.tsx +1 -1
  71. package/src/components/Coachmark/coachmark.spec.md +2 -2
  72. package/src/components/Combobox/combobox.anatomy.stories.tsx +12 -12
  73. package/src/components/Combobox/combobox.principles.stories.tsx +1 -1
  74. package/src/components/Combobox/combobox.spec.md +1 -1
  75. package/src/components/Combobox/combobox.tsx +25 -14
  76. package/src/components/Command/command.anatomy.stories.tsx +2 -0
  77. package/src/components/Command/command.tsx +2 -2
  78. package/src/components/DataTable/cell-registry.tsx +6 -2
  79. package/src/components/DataTable/data-table-filter-panel.tsx +3 -3
  80. package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
  81. package/src/components/DataTable/data-table.css +1 -1
  82. package/src/components/DataTable/data-table.spec.md +2 -2
  83. package/src/components/DateGrid/date-grid.anatomy.stories.tsx +1 -1
  84. package/src/components/DateGrid/date-grid.spec.md +1 -1
  85. package/src/components/DatePicker/date-picker.anatomy.stories.tsx +15 -11
  86. package/src/components/DatePicker/date-picker.spec.md +1 -1
  87. package/src/components/DatePicker/date-picker.tsx +9 -6
  88. package/src/components/DescriptionList/description-list.tsx +1 -1
  89. package/src/components/Dialog/dialog.anatomy.stories.tsx +1 -1
  90. package/src/components/DropdownMenu/dropdown-menu.spec.md +1 -1
  91. package/src/components/Empty/empty.tsx +2 -0
  92. package/src/components/Field/field-controls.spec.md +9 -6
  93. package/src/components/Field/field-wrapper.tsx +4 -4
  94. package/src/components/FileItem/file-item.principles.stories.tsx +1 -0
  95. package/src/components/FileUpload/file-upload.principles.stories.tsx +2 -2
  96. package/src/components/FileUpload/file-upload.spec.md +1 -1
  97. package/src/components/HoverCard/hover-card.principles.stories.tsx +1 -1
  98. package/src/components/Input/input.anatomy.stories.tsx +3 -3
  99. package/src/components/LinkInput/link-input.anatomy.stories.tsx +3 -3
  100. package/src/components/Notice/notice.anatomy.stories.tsx +1 -1
  101. package/src/components/NumberInput/number-input.anatomy.stories.tsx +8 -7
  102. package/src/components/NumberInput/number-input.spec.md +1 -1
  103. package/src/components/OverflowIndicator/overflow-indicator.tsx +1 -1
  104. package/src/components/PeoplePicker/people-picker.spec.md +3 -3
  105. package/src/components/PeoplePicker/people-picker.tsx +6 -6
  106. package/src/components/Popover/popover.principles.stories.tsx +4 -4
  107. package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
  108. package/src/components/ProfileCard/profile-card.tsx +1 -1
  109. package/src/components/ProgressBar/progress-bar.spec.md +1 -1
  110. package/src/components/Rating/rating.anatomy.stories.tsx +2 -2
  111. package/src/components/Rating/rating.spec.md +1 -1
  112. package/src/components/Rating/rating.tsx +1 -1
  113. package/src/components/Select/select.anatomy.stories.tsx +18 -18
  114. package/src/components/Select/select.spec.md +1 -1
  115. package/src/components/Select/select.tsx +7 -7
  116. package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +1 -1
  117. package/src/components/Sidebar/sidebar.spec.md +2 -2
  118. package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
  119. package/src/components/Steps/steps.spec.md +2 -2
  120. package/src/components/Tabs/tabs.principles.stories.tsx +1 -1
  121. package/src/components/Tabs/tabs.spec.md +1 -1
  122. package/src/components/Textarea/textarea.tsx +3 -3
  123. package/src/components/TimePicker/time-picker.spec.md +1 -1
  124. package/src/components/TimePicker/time-picker.tsx +11 -12
  125. package/src/components/TreeView/tree-view.tsx +1 -1
  126. package/src/patterns/element-anatomy/item-anatomy.spec.md +1 -1
  127. package/src/patterns/element-anatomy/item-anatomy.stories.tsx +1 -1
  128. package/src/patterns/overlay-surface/overlay-surface.spec.md +1 -0
  129. package/src/patterns/resize-handle/resize-handle.spec.md +1 -1
  130. package/src/tokens/color/semantic.css +1 -1
  131. package/src/tokens/uiSize/uiSize.css +5 -0
  132. package/src/tokens/uiSize/uiSize.spec.md +17 -3
@@ -47,11 +47,11 @@ const TOKEN_MAP: Record<ModeKey, Record<StateKey, ColorSpec>> = {
47
47
  },
48
48
  // readonly wrapper 為靜態 bg-readonly(field-wrapper.tsx 無 hover/focus 樣式;mode prop 勝 disabled)
49
49
  readonly: {
50
- default: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '--fg-muted' },
51
- hover: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '--fg-muted' },
52
- focus: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '--fg-muted' },
53
- error: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '--fg-muted' },
54
- disabled: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '--fg-muted' },
50
+ default: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '' },
51
+ hover: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '' },
52
+ focus: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '' },
53
+ error: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '' },
54
+ disabled: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '' },
55
55
  },
56
56
  disabled: {
57
57
  default: { bg: '--bg-disabled', text: '--fg-disabled', border: 'transparent', icon: '--fg-disabled' },
@@ -86,7 +86,7 @@ interface SizeSpec {
86
86
  const SIZE_SPECS: Record<SizeKey, SizeSpec> = {
87
87
  sm: {
88
88
  heightToken: 'h-field-sm', height: '28px',
89
- pxToken: 'px-3', px: '12px',
89
+ pxToken: 'px-[var(--field-px)]', px: '12px',
90
90
  gapToken: 'gap-2', gap: '8px',
91
91
  fontToken: 'text-body', font: '14px',
92
92
  icon: 16,
@@ -95,7 +95,7 @@ const SIZE_SPECS: Record<SizeKey, SizeSpec> = {
95
95
  },
96
96
  md: {
97
97
  heightToken: 'h-field-md', height: '32px',
98
- pxToken: 'px-3', px: '12px',
98
+ pxToken: 'px-[var(--field-px)]', px: '12px',
99
99
  gapToken: 'gap-2', gap: '8px',
100
100
  fontToken: 'text-body', font: '14px',
101
101
  icon: 16,
@@ -104,7 +104,7 @@ const SIZE_SPECS: Record<SizeKey, SizeSpec> = {
104
104
  },
105
105
  lg: {
106
106
  heightToken: 'h-field-lg', height: '36px',
107
- pxToken: 'px-3', px: '12px',
107
+ pxToken: 'px-[var(--field-px)]', px: '12px',
108
108
  gapToken: 'gap-2', gap: '8px',
109
109
  fontToken: 'text-body-lg', font: '16px',
110
110
  icon: 20,
@@ -271,7 +271,7 @@ export const Overview = {
271
271
  <span className="rounded px-2 py-1 text-[11px] font-mono border border-dashed"
272
272
  style={{ borderColor: 'var(--color-turquoise-6)', backgroundColor: 'var(--color-turquoise-1)', color: 'var(--color-turquoise-6)' }}>Tag</span>
273
273
  </div>
274
- <span className="text-[10px] text-fg-muted font-mono">chevron 恆顯(類型身份 indicator)· 不可開選單 · tagPadding 置中</span>
274
+ <span className="text-[10px] text-fg-muted font-mono">chevron:readonly 不顯示 / disabled 保留(類型身份 indicator)· 不可開選單 · tagPadding 置中</span>
275
275
  </div>
276
276
  </div>
277
277
  </div>
@@ -437,7 +437,7 @@ const InspectorInner = () => {
437
437
  { c: Z.gap, l: '元素間距' },
438
438
  { c: Z.select, l: 'Select 文字' },
439
439
  ...(clearable && value ? [{ c: Z.action, l: 'Clear' }] : []),
440
- { c: Z.icon, l: 'Chevron' },
440
+ ...(mode !== 'readonly' ? [{ c: Z.icon, l: 'Chevron' }] : []),
441
441
  ].map(({ c, l }) => (
442
442
  <span key={l} className="inline-flex items-center gap-1">
443
443
  <span className="w-2.5 h-2.5 rounded-md" style={{ background: c.bg, border: `1px dashed ${c.border}` }} />
@@ -452,7 +452,7 @@ const InspectorInner = () => {
452
452
  {hasStartIcon && <BpZone w={32} color={Z.gap} label={s.gapToken} sub={s.gap} />}
453
453
  <BpZone w={80} color={Z.select} label="flex-1" sub="select 文字" />
454
454
  {clearable && value && <BpZone w={36} color={Z.action} label={`${s.icon}px`} sub="clear" />}
455
- <BpZone w={36} color={Z.icon} label={`${s.icon}px`} sub="chevron" />
455
+ {mode !== 'readonly' && <BpZone w={36} color={Z.icon} label={`${s.icon}px`} sub="chevron" />}
456
456
  <BpZone w={44} color={Z.pad} label={s.pxToken} sub={s.px} />
457
457
  </div>
458
458
  <div className="ml-3 flex items-center" style={{ height: 52 }}>
@@ -475,7 +475,7 @@ const InspectorInner = () => {
475
475
  { c: Z.select, l: 'Select (hidden)' },
476
476
  { c: Z.gap, l: 'Spacer' },
477
477
  ...(clearable && value ? [{ c: Z.action, l: 'Clear' }] : []),
478
- { c: Z.icon, l: 'Chevron' },
478
+ ...(mode !== 'readonly' ? [{ c: Z.icon, l: 'Chevron' }] : []),
479
479
  ].map(({ c, l }) => (
480
480
  <span key={l} className="inline-flex items-center gap-1">
481
481
  <span className="w-2.5 h-2.5 rounded-md" style={{ background: c.bg, border: `1px dashed ${c.border}` }} />
@@ -490,7 +490,7 @@ const InspectorInner = () => {
490
490
  <BpZone w={40} color={Z.select} label="select" sub="hidden" />
491
491
  <BpZone w={36} color={Z.gap} label="flex-1" sub="spacer" />
492
492
  {clearable && value && <BpZone w={36} color={Z.action} label={`${s.icon}px`} sub="clear" />}
493
- <BpZone w={36} color={Z.icon} label={`${s.icon}px`} sub="chevron" />
493
+ {mode !== 'readonly' && <BpZone w={36} color={Z.icon} label={`${s.icon}px`} sub="chevron" />}
494
494
  <BpZone w={44} color={Z.pad} label="12px" sub="pR fixed" />
495
495
  </div>
496
496
  <div className="ml-3 flex items-center" style={{ height: 52 }}>
@@ -520,7 +520,7 @@ const InspectorInner = () => {
520
520
  <PropRow label="Fill"><TokenValue value={colors.bg} /></PropRow>
521
521
  <PropRow label="Text"><TokenValue value={colors.text} /></PropRow>
522
522
  <PropRow label="Stroke"><TokenValue value={colors.border} /></PropRow>
523
- <PropRow label="Icon"><TokenValue value={colors.icon} /></PropRow>
523
+ {mode !== 'readonly' && <PropRow label="Icon"><TokenValue value={colors.icon} /></PropRow>}
524
524
  </div>
525
525
 
526
526
  {/* LAYOUT */}
@@ -530,14 +530,14 @@ const InspectorInner = () => {
530
530
  {isTag ? (
531
531
  <>
532
532
  <PropRow label="tagPadding" dot={Z.pad.text}><TkVal token="calc()" value={s.tagPaddingFormula} /></PropRow>
533
- <PropRow label="右側 pR"><TkVal token="0.75rem" value="12px" /></PropRow>
533
+ <PropRow label="右側 pR"><TkVal token="var(--field-px)" value="12px" /></PropRow>
534
534
  <PropRow label="Tag 高度" dot={Z.tag.text}>{s.tagHeight}</PropRow>
535
535
  </>
536
536
  ) : (
537
537
  <PropRow label="左右內距" dot={Z.pad.text}><TkVal token={s.pxToken} value={s.px} /></PropRow>
538
538
  )}
539
539
  <PropRow label="元素間距" dot={Z.gap.text}><TkVal token={s.gapToken} value={s.gap} /></PropRow>
540
- <PropRow label="Icon 尺寸" dot={Z.icon.text}>{s.icon}px</PropRow>
540
+ {mode !== 'readonly' && <PropRow label="Icon 尺寸" dot={Z.icon.text}>{s.icon}px</PropRow>}
541
541
  </div>
542
542
 
543
543
  {/* TYPOGRAPHY */}
@@ -722,7 +722,7 @@ export const SizeMatrix = {
722
722
  {/* ── tag mode additional tokens ── */}
723
723
  <div className="flex flex-col gap-2">
724
724
  <span className="text-caption font-medium text-fg-secondary">tag 模式額外 token</span>
725
- <Desc>tag 模式的 padding 用 calc() 公式置中 Tag:px = (field-height - tag-height) / 2。右側 paddingRight 固定 12px(0.75rem)。</Desc>
725
+ <Desc>tag 模式的 padding 用 calc() 公式置中 Tag:px = (field-height - tag-height) / 2。右側 paddingRight 固定 12px(--field-px token)。</Desc>
726
726
  <div className="overflow-x-auto">
727
727
  <table className="border-collapse text-caption">
728
728
  <thead><tr>
@@ -752,7 +752,7 @@ export const SizeMatrix = {
752
752
  <Td>paddingRight</Td>
753
753
  {SIZES.map((sz) => (
754
754
  <Td key={sz} mono>
755
- <div className="text-fg-secondary">0.75rem</div>
755
+ <div className="text-fg-secondary">var(--field-px)</div>
756
756
  <div className="text-fg-muted text-[10px]">12px(固定)</div>
757
757
  </Td>
758
758
  ))}
@@ -240,7 +240,7 @@ Select 的值套用時機是**由 onChange handler 的副作用決定**,不是
240
240
  - Tag 元件呈現選中值 + 隱藏的原生 select overlay
241
241
  - Tag 設為 `pointer-events-none`,點擊穿透到底層 select
242
242
  - edit 模式:Tag + ChevronDown + 可選 clear
243
- - readonly / disabled:Tag 顯示 + ChevronDown 恆顯(類型身份 indicator;readonly fg-muted / disabled fg-disabled),不可開選單
243
+ - readonly:Tag 顯示,**ChevronDown 不顯示**(純值、不可開選單);disabled:Tag 顯示 + **ChevronDown 保留**(類型身份 indicatorfg-disabled),不可開選單
244
244
 
245
245
  ---
246
246
 
@@ -327,12 +327,12 @@ function ReadonlyDisplay({
327
327
  )
328
328
  }
329
329
 
330
- // 2026-06-10 user 拍板「類型身份 indicator」規則:readonly/disabled 保留 ChevronDown(表單情境恆顯;
331
- // naked cell 情境依 showDisplayEndIcon = isEditable,維持 2026-05-10 cell canonical「非可編欄不顯」)。
332
- // disabled fg-disabled(對齊 spec L213 + Accordion M24 precedent + 原生 select/MUI/Carbon 慣例)。
330
+ // 2026-06-26 類型身份 indicator 規則:edit 顯示 / readonly 不顯示(純值、不可開下拉,箭頭會誤導) /
331
+ // disabled 保留(fg-disabled,對齊原生 <select disabled> 灰示箭頭 + Accordion M24 precedent)。
332
+ // naked cell 情境依 showDisplayEndIcon = isEditable,維持 2026-05-10 cell canonical「非可編欄不顯」。
333
333
  // aria-disabled:styled-disabled(非 native disabled 元素)需明告 AT「inactive」,同時讓 axe 正確
334
334
  // 豁免 disabled 文字的 color-contrast(WCAG 1.4.3 inactive UI 例外)。
335
- const showIndicator = variant === 'naked' ? !!showDisplayEndIcon : true
335
+ const showIndicator = variant === 'naked' ? !!showDisplayEndIcon : resolvedMode === 'disabled'
336
336
  const ariaDisabled = resolvedMode === 'disabled' ? true : undefined
337
337
 
338
338
  if (isTextDisplay) {
@@ -351,7 +351,7 @@ function ReadonlyDisplay({
351
351
  const tagVariant = selectedOpt?.tagVariant as 'blue' | 'green' | 'red' | 'yellow' | 'neutral' | undefined
352
352
 
353
353
  return (
354
- <div className={cn(fieldWrapperStyles({ mode: resolvedMode, variant, size: sz }), value && tagPadding[sz], className)} data-field-mode={resolvedMode} aria-disabled={ariaDisabled}>
354
+ <div className={cn(fieldWrapperStyles({ mode: resolvedMode, variant, size: sz }), value && tagPadding[sz], className)} style={{ paddingRight: 'var(--field-px)' }} data-field-mode={resolvedMode} aria-disabled={ariaDisabled}>
355
355
  {value ? <Tag size={sz} color={tagVariant}>{label}</Tag> : <span className={emptyColorCls}>{emptyText}</span>}
356
356
  {showIndicator && <ItemSuffix className="pointer-events-none"><ChevronDown size={iconSize} className={cn('shrink-0', iconColor)} aria-hidden /></ItemSuffix>}
357
357
  </div>
@@ -429,7 +429,7 @@ const NativeSelect = React.forwardRef<HTMLSelectElement, SelectProps>(
429
429
  return (
430
430
  <div className={cn(fieldWrapperStyles({ mode: 'edit', variant: variant, size }), value && tagPadding[size], 'relative',
431
431
  error && ['border-error hover:border-error-hover', 'focus-within:border-error focus-within:hover:border-error'], className)}
432
- style={{ paddingRight: '0.75rem' }} data-field-mode="edit" data-error={error ? '' : undefined}>
432
+ style={{ paddingRight: 'var(--field-px)' }} data-field-mode="edit" data-error={error ? '' : undefined}>
433
433
  {value ? <Tag size={size} color={nativeTagVariant} className="shrink-0 relative z-10 pointer-events-none">{label}</Tag> : <span className="text-fg-muted">{placeholder ?? '選擇...'}</span>}
434
434
  {selectEl}
435
435
  <span className="flex-1" />
@@ -599,7 +599,7 @@ const CustomSelect = React.forwardRef<HTMLDivElement, SelectProps>(
599
599
  'cursor-pointer',
600
600
  className,
601
601
  )}
602
- style={!isTextDisplay ? { paddingRight: '0.75rem' } : undefined}
602
+ style={!isTextDisplay ? { paddingRight: 'var(--field-px)' } : undefined}
603
603
  data-field-mode="edit"
604
604
  data-error={error ? '' : undefined}
605
605
  onKeyDown={(e) => {
@@ -806,7 +806,7 @@ export const Accessibility = {
806
806
  render: () => (
807
807
  <div className="max-w-3xl text-body text-fg-secondary">
808
808
  <h3 className="text-h5 text-foreground mb-2">無障礙設計</h3>
809
- <p className="whitespace-pre-line">{"詳 `select-menu.spec.md` 「A11y 預設」段。摘要:\n\n ARIA / Pattern :基於 cmdk library a11y(combobox / listbox / option role + aria-activedescendant)。詳 [cmdk a11y](https://cmdk.paco.me/#accessibility)。\n\n Keyboard 行為 :\n\n- Tab — focus trigger\n- Enter / Space — 開啟 menu(trigger 由 consumer 經 PopoverTrigger asChild 提供:Select / Combobox 的 trigger 是 role=\"combobox\" 容器自綁 Enter / Space handler;若 consumer 用 DS Button 則由 native click 觸發)\n- ↑/↓ — 導覽 options(menu 開啟後)\n- Enter — 選擇\n- 字母鍵 — type-ahead 過濾(search 模式)\n- Esc — 關閉\n\n Focus :menu 開啟時 active-descendant 虛擬焦點落在第一個 / 已選 option(aria-activedescendant 高亮,非 DOM focus);searchable 時 DOM focus 給搜尋 input;option 無 tabIndex,DOM focus 不落在 option 上。關閉時 focus 回 trigger。\n\n 驗證 :Storybook a11y addon panel 應 0 critical violation;鍵盤完整可操作(無需滑鼠)。WCAG AA contrast ≥ 4.5:1(text)/ 3:1(UI)。"}</p>
809
+ <p className="whitespace-pre-line">{"詳 `select-menu.spec.md` 「A11y 預設」段。摘要:\n\n ARIA / Pattern :基於 cmdk library a11y(combobox / listbox / option role + aria-activedescendant)。詳 [cmdk a11y](https://cmdk.paco.me/#accessibility)。\n\n Keyboard 行為 :\n\n- Tab — focus trigger\n- Enter / Space — 開啟 menu(trigger 由 consumer 經 PopoverTrigger asChild 提供:Select / Combobox 的 trigger 是 role=\"combobox\" 容器自綁 Enter / Space handler;若 consumer 用 DS Button 則由 native click 觸發)\n- ↑/↓ — 導覽 options(menu 開啟後)\n- Enter — 選擇\n- 字母鍵 — type-ahead 過濾(search 模式)\n- Esc — 關閉\n\n Focus :menu 開啟時 active-descendant 虛擬焦點落在第一個 / 已選 option(aria-activedescendant 高亮,非 DOM focus);searchable 時 DOM focus 給搜尋 input;非 searchable 時 DOM focus 移到 cmdk 的 [cmdk-root](Command 元素,見 handleNonSearchableAutoFocus),讓 cmdk 內建方向鍵 / Enter / Home / End 導覽生效;option 無 tabIndex,DOM focus 不落在 option 上。關閉時 focus 回 trigger。\n\n 驗證 :Storybook a11y addon panel 應 0 critical violation;鍵盤完整可操作(無需滑鼠)。WCAG AA contrast ≥ 4.5:1(text)/ 3:1(UI)。"}</p>
810
810
  </div>
811
811
  ),
812
812
  }
@@ -74,7 +74,7 @@ benchmark:
74
74
 
75
75
  ```
76
76
  SidebarProvider ← 全域 context(open 狀態、cookie、快捷鍵)
77
- Sidebar ← 主容器(variant + collapsible + side
77
+ Sidebar ← 主容器(collapsible + side + viewportInsetTop
78
78
  SidebarHeader ← 頂部:logo、workspace switcher
79
79
  SidebarContent ← 中間可捲動區
80
80
  SidebarGroup ← 分組容器(可多個,純視覺分段)
@@ -83,7 +83,7 @@ SidebarProvider ← 全域 context(open 狀態、cookie、快捷
83
83
 
84
84
  ├─ SidebarMenu 扁平選單(1 層、有限可列舉)
85
85
  │ SidebarMenuItem
86
- │ SidebarMenuButton ← 必須有 icon
86
+ │ SidebarMenuButton ← icon 模式下 consumer 應確保每個 item 有 icon(否則 collapsed rail 出現空槽;非元件強制)
87
87
  │ SidebarMenuBadge ← 右側 badge
88
88
  │ SidebarMenuAction ← hover 浮出的 action
89
89
 
@@ -43,7 +43,7 @@ export const Overview: Story = {
43
43
  ['min / max', 'number', '0 / 100', '值範圍'],
44
44
  ['step', 'number', '1', '步進'],
45
45
  ['size', "'sm' | 'md' | 'lg'", "'md'", '**只影響容器外高**,不影響 track/thumb 尺寸'],
46
- ['disabled', 'boolean', 'false', '灰階降級(range/thumb border→border,thumb bg 保留白)'],
46
+ ['disabled', 'boolean', 'false', '灰階降級(range/thumb border→border,thumb bg 沉回 bg-canvas(不透明背景色))'],
47
47
  ['minStepsBetweenThumbs', 'number', '—', 'range mode 兩 thumb 最小距離'],
48
48
  ].map(([p, t, d, desc]) => (
49
49
  <tr key={p}><Td mono>{p}</Td><Td mono>{t}</Td><Td mono>{d}</Td><Td>{desc}</Td></tr>
@@ -80,7 +80,7 @@ Steps 是 `patterns/element-anatomy/item-anatomy.spec.md` 的 row primitive **co
80
80
  - **字體 tier**:label `text-body` (sm/md) / `text-body-lg` (lg);description `text-caption` (sm/md) / `text-body` (lg)。跟 MenuItem / TreeItem 同一套。
81
81
  - **字重**:`font-medium`(含 label,不隨 focus 變)
82
82
  - **預設文字色**:`text-fg-secondary`;`value` 指向的 step(focused)為 `text-foreground`
83
- - **Icon tier**:`ICON_SIZE = { sm: 16, md: 16, lg: 20 }`
83
+ - **Icon tier**:`INDICATOR_ICON_SIZE = { sm: 0, md: 16, lg: 20 }`(sm 為純圓點無內部 icon,見 size table)
84
84
  - **Description 永遠可選**,任何 size 都不強制
85
85
  - **Hit area 地板**:可點擊的 indicator 至少 `field-height-xs`(24px),不足者用透明 padding 撐開
86
86
 
@@ -172,7 +172,7 @@ indicator 圓形 flex items-center 居中
172
172
 
173
173
  | State | 視覺 | 觸發 |
174
174
  |---|---|---|
175
- | `upcoming` | 灰底(`bg-muted`)+ 灰字(`text-fg-disabled`)| 未走到 |
175
+ | `upcoming` | 灰底(inline `background: var(--muted)`)+ 灰字(`text-fg-disabled`)| 未走到 |
176
176
  | `current` | 藍底(`bg-info`)+ 白字數字 | step === `value` 且不在 completedValues / errorValues |
177
177
  | `completed` | 藍底(`bg-info`)+ 白色 ✓ | step 在 `completedValues` 內 |
178
178
  | `reachable` | 藍底(`bg-info`)+ 白字數字(僅 linear;sm 為空心環)| linear 下「下一個未完成」step(非 current / completed / error),可點 |
@@ -57,7 +57,7 @@ export const UsageGuidance: Story = {
57
57
  <LinkTo kind="Design System/Components/Tabs/展示" name="溢出處理 — 水平捲動"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">溢出處理 — 水平捲動</span></LinkTo>
58
58
  </li>
59
59
  <li>
60
- <LinkTo kind="Design System/Components/Tabs/展示" name="溢出處理 — ⌄ 導覽選單"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">溢出處理 — 收進選單</span></LinkTo>
60
+ <LinkTo kind="Design System/Components/Tabs/展示" name="溢出處理 — ⌄ 導覽選單"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">溢出處理 — ⌄ 導覽選單</span></LinkTo>
61
61
  </li>
62
62
  </ul>
63
63
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見下方「vs 近親」)。</p>
@@ -207,7 +207,7 @@ TabsList 底部有 1px gray border(`border-divider`,neutral-4),selected
207
207
 
208
208
  Tabs 常與容器 header 的底邊 border 合併——**視覺上只有一條線**,不是 header border + tabs border 疊兩條。
209
209
 
210
- **做法**:Tabs 的 `TabsList` 底部 border 與 header 的 `border-b` 實際上是**同一條線**(設計上重疊、實作上不重複渲染)。**Header 退讓**(移除自己 `border-b`),**Tabs 接管**(自身 `border-b border-divider` per `TABS_LIST_BASE`,2026-05-18 fd843c25 統一 chrome separator 色)。三種 overflow mode(none / scroll / menu)的 TabsList 一律 `inline-flex w-fit`(`TABS_LIST_BASE` + `w-fit`;2026-05-19 c359c711 border owner 升 list 內部 + `overflow-y-hidden` 阻 y auto-promote);chrome header 內的全寬 paint 由 `ChromeHeader` tabsSlot wrapper 以 `[&>[role=tablist]]:w-full` 強制(見 `header-canonical.spec.md`)。
210
+ **做法**:Tabs 的 `TabsList` 底部 border 與 header 的 `border-b` 實際上是**同一條線**(設計上重疊、實作上不重複渲染)。**Header 退讓**(移除自己 `border-b`),**Tabs 接管**(自身 `border-b border-divider` per `TABS_LIST_BASE`,2026-05-18 fd843c25 統一 chrome separator 色)。三種 overflow mode 的 TabsList 一律跨滿父容器(none 模式 `w-full`;scroll / menu 模式 inner list `min-w-full` 保留溢出成長,見 L184;`TABS_LIST_BASE` + `overflow-y-hidden` 阻 y auto-promote,2026-05-19 c359c711 border owner 升 list 內部);chrome header 內的全寬 paint 由 `ChromeHeader` tabsSlot wrapper 以 `[&>[role=tablist]]:w-full` 強制(見 `header-canonical.spec.md`)。
211
211
 
212
212
  **世界級對照(verbatim cite)**:
213
213
  - **GitHub Primer PageHeader**:「`hasBorder` defaults true,**but border NOT rendered if Navigation child contains UnderlineNav**;UnderlineNav itself provides bottom border」(`primer.style/components/page-header/react`)
@@ -19,7 +19,7 @@ import { EMPTY_DISPLAY } from '@/design-system/components/Field/field-wrapper'
19
19
  *
20
20
  * ── Padding 規則 ───────────────────────────────────────────────────────
21
21
  * 多行內容必須有上下內距才能閱讀舒適。不沿用 Input 的 items-center,
22
- * 改用 py-2(8px)固定上下內距 + px-3 左右內距(與 Input 一致)。
22
+ * 改用 py-2(8px)固定上下內距 + px-[var(--field-px)](12px token,左右內距 SSOT,與 Input/Field family 一致)。
23
23
  *
24
24
  * ── Size ────────────────────────────────────────────────────────────────
25
25
  * sm / md → text-body(14px)
@@ -40,7 +40,7 @@ const textareaVariants = cva(
40
40
  // K10 fix(2026-05-04):disabled 時 placeholder + text 切 fg-disabled(parallel 到 bareInputStyles)
41
41
  // Textarea 自身 `<textarea disabled>` 帶 disabled HTML attribute,用 `disabled:` variant 直接命中
42
42
  'disabled:placeholder:text-fg-disabled disabled:text-fg-disabled',
43
- 'px-3 py-2',
43
+ 'px-[var(--field-px)] py-2',
44
44
  'transition-colors duration-150',
45
45
  ],
46
46
  {
@@ -77,7 +77,7 @@ const textareaVariants = cva(
77
77
  {
78
78
  mode: 'display',
79
79
  variant: 'default',
80
- // 2026-05-13 Q3 Path Ⅰ:Textarea default display zero chrome,!px-0 !py-0 override base `px-3 py-2`
80
+ // 2026-05-13 Q3 Path Ⅰ:Textarea default display zero chrome,!px-0 !py-0 override base `px-[var(--field-px)] py-2`
81
81
  // (跟 Input 同 SSOT,per field-controls.spec.md (d))
82
82
  className: 'bg-transparent border border-transparent !px-0 !py-0',
83
83
  },
@@ -152,7 +152,7 @@ Panel 展開後的 column picker 結構:
152
152
 
153
153
  ### Spacing + 結構(2026-04-21 canonical,2026-04-21 window width 修正)
154
154
 
155
- - Footer 內 padding = `--layout-space-tight`(12px @ md density)
155
+ - Footer 內 padding = `--layout-space-tight`(12px @ md density);**消費 `<SurfaceFooter>` SSOT**(border-t + py + gap + justify + shrink-0),px 經 `className` override 回 tight——因滿欄 column 面板無 chrome-padded body 內縮邊可對齊(見 `overlay-surface.spec.md` SurfaceFooter「footer px = body 內縮原則」例外);此刻按鈕 `mr-auto` 推右,視覺等同原 justify-between(零視覺變化)
156
156
  - **Panel 容器固定寬:2 欄(時 / 分)`w-40`(160px)/ 3 欄(時 / 分 / 秒)`w-60`(240px)**;**每欄 `flex-1 h-full` 等分**容器寬(非固定 `w-12`)。**分隔「:」移除**(AR8 canonical — Ant TimePicker / Google Calendar 同樣不加 `:`,靠 column 間距自明 `@benchmark-unverified` visual)
157
157
  - Scrollable list 用 **`<ScrollArea>`**(對齊 DS 跨 OS 一致 overlay 捲軸 canonical);不 raw `overflow-y-auto`
158
158
  - **Scroll-into-view**:mount = `behavior:'auto'`(避閃爍),後續 `value` 變更 = `behavior:'smooth'`(對齊 iOS / Material / Ant timepicker idiom)。SSOT 在 `time-columns.tsx` `TimeColumn` useEffect
@@ -14,6 +14,7 @@ import { ItemInlineAction, ItemSuffix } from '@/design-system/patterns/element-a
14
14
  import { Popover, PopoverTrigger, PopoverContent } from '@/design-system/components/Popover/popover'
15
15
  import { useFieldContext, useResolvedFieldSize, useResolvedFieldDisabled, useResolvedFieldMode, useResolvedFieldVariant, useResolvedFieldInvalid } from '@/design-system/components/Field/field-context'
16
16
  import { Button } from '@/design-system/components/Button/button'
17
+ import { SurfaceFooter } from '@/design-system/patterns/overlay-surface/overlay-surface'
17
18
  import {
18
19
  TimeColumns,
19
20
  isoToTimeParts,
@@ -279,8 +280,8 @@ const TimePicker = React.forwardRef<HTMLDivElement, TimePickerProps>(
279
280
  : <span className="text-fg-muted">{EMPTY_DISPLAY}</span>
280
281
  }
281
282
  </span>
282
- {/* 2026-06-10 類型身份 indicator gate:naked cell 依 showDisplayEndIcon=isEditable(修 disabled cell 漏顯 bug)*/}
283
- {EndIconCmp && (variant === 'naked' ? showDisplayEndIcon : true) && (
283
+ {/* 2026-06-26 類型身份 indicator:edit 顯示 / readonly 不顯示 / disabled 保留(fg-disabled);naked cell 依 showDisplayEndIcon=isEditable(修 disabled cell 漏顯 bug)*/}
284
+ {EndIconCmp && (variant === 'naked' ? showDisplayEndIcon : resolvedMode === 'disabled') && (
284
285
  <ItemSuffix className="pointer-events-none">
285
286
  <EndIconCmp
286
287
  size={iconSize}
@@ -389,15 +390,13 @@ const TimePicker = React.forwardRef<HTMLDivElement, TimePickerProps>(
389
390
  // 不會走到 document body 把 popover 內容推出畫面(user 報「hours 欄空白」根因)。
390
391
  className="flex-1 min-h-0"
391
392
  />
392
- {/* Footer:Now + OK */}
393
- <div
394
- className={cn(
395
- 'flex items-center justify-between gap-2',
396
- 'border-t border-divider',
397
- 'px-[var(--layout-space-tight)] py-[var(--layout-space-tight)]',
398
- )}
399
- >
400
- <Button variant="text" size="sm" onClick={handleNow}>
393
+ {/* Footer:消費 SurfaceFooter SSOT(border-t + py-tight + gap-2 + shrink-0 + justify-end)。
394
+ px override 回 layout-space-tight,因 TimePicker 滿欄 column 面板無 chrome-padded body
395
+ 內縮邊可對齊(footer px = body 內縮 原則;column-selector 數字置中、零內距,
396
+ time-picker.spec.md)。此刻 mr-auto 把確定推右,視覺等同原 justify-between
397
+ —— 純結構消費 overlay-surface canonical(overlay-surface.spec.md:17「不自寫 padding token」),零視覺變化。 */}
398
+ <SurfaceFooter className="px-[var(--layout-space-tight)]">
399
+ <Button variant="text" size="sm" onClick={handleNow} className="mr-auto">
401
400
  此刻
402
401
  </Button>
403
402
  <Button
@@ -407,7 +406,7 @@ const TimePicker = React.forwardRef<HTMLDivElement, TimePickerProps>(
407
406
  >
408
407
  確定
409
408
  </Button>
410
- </div>
409
+ </SurfaceFooter>
411
410
  </div>
412
411
  </PopoverContent>
413
412
  </Popover>
@@ -1057,7 +1057,7 @@ export const treeViewMeta = {
1057
1057
  },
1058
1058
  states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
1059
1059
  tokens: {
1060
- bg: ['bg-neutral-hover', 'bg-primary', 'bg-primary-subtle', 'bg-surface'],
1060
+ bg: ['bg-neutral-hover', 'bg-neutral-selected', 'bg-surface'],
1061
1061
  fg: ['text-fg-disabled', 'text-fg-muted', 'text-fg-secondary', 'text-foreground'],
1062
1062
  ring: ['ring-ring'],
1063
1063
  },
@@ -474,7 +474,7 @@ MenuItem 承載 Switch 時,**整列 row 的 click = toggle 這個 control**(不
474
474
  | 內容物高度 | 對齊容器 | 對齊目標 |
475
475
  |---|---|---|
476
476
  | ≤ 24px | `h-[1lh]` | 第一行 label 的垂直中心 |
477
- | > 24px(+ 有 description) | `h-[calc(1lh + 2px + desc_1lh)]` | label + gap + description 文字塊的垂直中心 |
477
+ | > 24px(+ 有 description) | `h-[calc(1lh + var(--item-gap-label-desc-<mode>[-lg]) + desc-font-size*1.3)]` | label + gap + description 文字塊的垂直中心 |
478
478
  | > 24px(無 description) | `h-[1lh]` | 強制 inline(沒有文字塊可對齊) |
479
479
 
480
480
  ### 為什麼 prefix 和 suffix 各自獨立(不互相同步)
@@ -161,7 +161,7 @@ const SCANNING_SPECS: Record<SizeKey, TypoSpec> = {
161
161
  const READING_SPECS: Record<SizeKey, TypoSpec> = {
162
162
  sm: { labelFont: 'text-body', labelSize: '14px', labelLh: '1.5', descFont: 'text-body', descSize: '14px', descLh: '1.5', iconPx: 16 },
163
163
  md: { labelFont: 'text-body', labelSize: '14px', labelLh: '1.5', descFont: 'text-body', descSize: '14px', descLh: '1.5', iconPx: 16 },
164
- lg: { labelFont: 'text-body-lg', labelSize: '16px', labelLh: '1.5', descFont: 'text-body-lg', descSize: '16px', descLh: '1.5', iconPx: 20 },
164
+ lg: { labelFont: 'text-body-lg', labelSize: '16px', labelLh: '1.5', descFont: 'text-body', descSize: '14px', descLh: '1.5', iconPx: 20 },
165
165
  }
166
166
 
167
167
  /* ═══════════════════════════════════════════════════════════════════════════
@@ -177,6 +177,7 @@ list 不再是 body 唯一 region → **不該撤 body chrome padding**(撤了
177
177
  - `border-t border-divider`
178
178
  - `px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]`
179
179
  - `flex items-center justify-end gap-2 shrink-0`(右對齊按鈕列,不被壓縮)
180
+ - **Footer px = body 內容內縮原則**:預設 `layout-space-loose` 讓 footer 按鈕左緣對齊 SurfaceHeader title / SurfaceBody 內容左緣(整面板內容垂直對齊一條線,同 Body alignment rationale)。**例外**:body 為 full-bleed(滿欄 column selector / 無 chrome padding 的 unbounded list)、無內容左緣可對齊時,可經 `className` override 較緊 px(eg. TimePicker footer override `px-[var(--layout-space-tight)]` 對齊滿欄置中 columns、保窄面板平衡)。**注**:`--field-px` 是 form-field gutter 概念(uiSize),**不**用於 overlay footer——footer 屬 overlay chrome,padding 走 layoutSpace 家族。
180
181
 
181
182
  ---
182
183
 
@@ -59,7 +59,7 @@ benchmark:
59
59
  取值依據:7px 命中區 / 1px line 非自創——來自下方「世界級對照細節」5 家共識(hit zone 7-8px fingertip-friendly / 1px line non-intrusive)+ DataTable v11 已 ship 的既有 canonical(本 primitive 抽取自它,M17)。
60
60
 
61
61
  - **命中區**:7px 寬(horizontal)/ 高(vertical),`-3px` outward offset 跨 boundary 抓得到
62
- - **Visual line**:1px,positioned `right-[3px]` / `bottom-[3px]`,default full-extent
62
+ - **Visual line**:1px,line offset 3px from the position edge(inline style `right: 3` / `bottom: 3`,非 Tailwind class — 見 tsx JIT-quirk note),default full-extent
63
63
  - **idle**:`bg-divider`
64
64
  - **disabled**:`bg-divider`(無 hover affordance)
65
65
  - **hover**:`bg-[var(--border-hover)]`(via `group/resize` selector)
@@ -197,7 +197,7 @@
197
197
 
198
198
  /* Brand / Action — 品牌主操作色
199
199
  --primary-text 為 SOP 規定的 5 件套之一,目前無消費者
200
- (Button 直接用 text-white, Tag 用 primitive --color-blue-7) */
200
+ (Button 直接用 text-on-emphasis, Tag 用 primitive --color-blue-7) */
201
201
  --primary: var(--color-blue-6);
202
202
  --primary-hover: var(--color-blue-5);
203
203
  --primary-active: var(--color-blue-7);
@@ -24,6 +24,11 @@
24
24
  --field-height-md: 2rem; /* 32px */
25
25
  --field-height-lg: 2.25rem; /* 36px */
26
26
 
27
+ /* Field horizontal padding — 固定 12px(不隨 size/density);form-context field 左右內距 SSOT。
28
+ 跟 --table-cell-px(cell-context)同 -px 慣例;Input/Select/Combobox/DatePicker/TimePicker/Textarea
29
+ + tag 模式右緣 re-assert 全消費此 token(取代散落的 px-3 / inline 0.75rem,M17 SSOT 可傳播)。 */
30
+ --field-px: 0.75rem; /* 12px */
31
+
27
32
  /* Table Row Height — DataTable */
28
33
  --table-row-sm: 2rem; /* 32px */
29
34
  --table-row-md: 2.5rem; /* 40px */
@@ -94,6 +94,20 @@ Consumer 寫 Form 或 Toolbar 時並排多個 field-height 元件:
94
94
 
95
95
  本專案曾發生 SegmentedControl 的 code defaults 是 `md`、spec + docblock 寫 `sm ★default` 的三方不一致(2026-04-18 修正)。避免方式:改 cva `defaultVariants` 前先讀本表,確認新值仍符合 family 約束。
96
96
 
97
+ ## Field Horizontal Padding
98
+
99
+ Form-context field 控件的左右水平內距。**固定 12px,不隨 size / density 變化**——縱向才是 density 節奏;橫向是內容溝槽常數,對齊 field-height family 全控件。
100
+
101
+ | Token | 值 | 說明 |
102
+ |-------|-----|------|
103
+ | `--field-px` | 0.75rem (12px) | form-context field 左右內距 SSOT;固定不隨 size / density |
104
+
105
+ **消費者**:`Input` / `NumberInput` / `Select` / `Combobox` / `DatePicker` / `TimePicker` / `LinkInput` / `Textarea`(經 `fieldWrapperStyles` cva `px-[var(--field-px)]`)+ `PeoplePicker`(form-context inject `!px-[var(--field-px)]`)+ tag 模式右緣 re-assert(`paddingRight: var(--field-px)`,Select / Combobox readonly + edit)。
106
+
107
+ **與 `--table-cell-px` 的關係**:同 `-px` 命名慣例。`--table-cell-px`(DataTable-scoped)預設 `var(--field-px)`(form / cell 同 12px content gutter SSOT),但仍是獨立 named token、可被 DataTable 單獨 override(per `components/Field/field-controls.spec.md` contract (c) scoped 決策)。
108
+
109
+ **Why 固定不隨 density**:density 本質是縱向密度(一屏幾列);橫向管可讀性(字離格線距離),目的不同不綁。對齊 Ant Table(橫向 padding 不隨 density 變、縱橫分離)+ 全 DS field 控件 12px 常數。M17「SSOT 必可傳播」:散落的 `px-3` / inline `0.75rem` 全收斂進此 token。
110
+
97
111
  ## Table Row
98
112
 
99
113
  DataTable 行高。density 切換統一 +0.5rem (+8px)。
@@ -152,9 +166,9 @@ DataTable 行高。density 切換統一 +0.5rem (+8px)。
152
166
  | Avatar 內 icon | `components/Avatar/avatar.spec.md:165` | `round_even(size × 0.6)` formula | Material / Apple HIG |
153
167
  | Empty illustration | `components/Empty/empty.tsx:48` | Avatar 48 wrap → icon 28(Avatar formula derived)| Empty-state canonical |
154
168
  | FileViewer thumb | `components/FileViewer/file-viewer.tsx:621` | thumb 64 → icon 20(file-type indicator hardcode 無公式)| Thumbnail UI 慣例 |
155
- | CircularProgress | `components/CircularProgress/circular-progress.tsx:87` | `strokeWidth = max(2, size/10)` stroke ring 厚度非 icon | Geometric scaling |
156
- | Steps indicator icon | `components/Steps/steps.tsx:24` | `INDICATOR_ICON_SIZE {sm:0, md:16, lg:20}`(sm 因圓圈 8px 太小)| Indicator-internal |
157
- | Checkbox/Switch check | `components/Checkbox/checkbox.tsx:49` + `components/Switch/switch.tsx:73` | `{sm:12, md:12, lg:16}` form-control internal + stroke 下限 12 | iOS HIG / Material 3 / Polaris | <!-- @benchmark-unverified -->
169
+ | CircularProgress | `components/CircularProgress/circular-progress.tsx:86` | `strokeWidth = max(2, round(size/10))` stroke ring 厚度非 icon | Geometric scaling |
170
+ | Steps indicator icon | `components/Steps/steps.tsx:25` | `INDICATOR_ICON_SIZE {sm:0, md:16, lg:20}`(sm 因圓圈 8px 太小)| Indicator-internal |
171
+ | Checkbox/Switch check | `components/Checkbox/checkbox.tsx:52` + `components/Switch/switch.tsx:83` | `{sm:12, md:12, lg:16}` form-control internal + stroke 下限 12 | iOS HIG / Material 3 / Polaris | <!-- @benchmark-unverified -->
158
172
 
159
173
  **程式化 SSOT**:`patterns/element-anatomy/item-anatomy.tsx:66` `ICON_SIZE = {sm:16, md:16, lg:20}` 是本 tier 的 type-safe const。**Form control 透過 `tokens/uiSize/icon-size.ts` re-export entry import**(避 components→patterns 反向 dependency)。
160
174