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

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 (100) hide show
  1. package/dist/components/AppShell/_demo-helpers.d.ts.map +1 -1
  2. package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
  3. package/dist/components/BulkActionBar/bulk-action-bar.js +1 -1
  4. package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
  5. package/dist/components/DataTable/data-table.d.ts +27 -6
  6. package/dist/components/DataTable/data-table.d.ts.map +1 -1
  7. package/dist/components/DataTable/data-table.js +57 -34
  8. package/dist/components/DataTable/data-table.js.map +1 -1
  9. package/ds-canonical/fork/governance.lock +7 -7
  10. package/ds-canonical/fork/skills/bug-fix-rhythm/SKILL.md +2 -0
  11. package/ds-canonical/fork/skills/code-quality-audit/SKILL.md +2 -0
  12. package/ds-canonical/fork/skills/product-ui-audit/SKILL.md +2 -0
  13. package/ds-canonical/fork/skills/prototype/references/audit-checks.md +1 -0
  14. package/ds-canonical/fork/skills/scan-similar-bugs/SKILL.md +2 -0
  15. package/ds-canonical/fork/skills/visual-audit/SKILL.md +2 -0
  16. package/ds-canonical/fork/skills/visual-audit/references/audit-architecture.md +1 -0
  17. package/ds-canonical/hooks/check_plugin_fork_health.sh +2 -2
  18. package/ds-canonical/hooks/check_story_invariants.sh +26 -0
  19. package/ds-canonical/hooks/lib/_app_shell_primary_header_consistency.sh +31 -4
  20. package/ds-canonical/hooks/session_start_governance_check.sh +1 -1
  21. package/ds-canonical/hooks/tests/test_check_app_shell_primary_header_consistency.sh +46 -2
  22. package/ds-canonical/hooks/tests/test_check_story_invariants.sh +30 -0
  23. package/ds-canonical/skills/design-system-audit/SKILL.md +2 -2
  24. package/llms-full.txt +1 -1
  25. package/llms.txt +1 -1
  26. package/package.json +1 -1
  27. package/src/components/Accordion/accordion.principles.stories.tsx +3 -3
  28. package/src/components/Alert/alert.principles.stories.tsx +5 -5
  29. package/src/components/AppShell/_demo-helpers.tsx +23 -6
  30. package/src/components/AppShell/app-shell.principles.stories.tsx +9 -8
  31. package/src/components/AppShell/app-shell.spec.md +9 -8
  32. package/src/components/AppShell/app-shell.stories.tsx +5 -3
  33. package/src/components/AspectRatio/aspect-ratio.principles.stories.tsx +1 -1
  34. package/src/components/Avatar/avatar.principles.stories.tsx +3 -3
  35. package/src/components/Badge/badge.principles.stories.tsx +3 -3
  36. package/src/components/Breadcrumb/breadcrumb.principles.stories.tsx +3 -3
  37. package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
  38. package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +3 -3
  39. package/src/components/BulkActionBar/bulk-action-bar.spec.md +4 -2
  40. package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +2 -2
  41. package/src/components/BulkActionBar/bulk-action-bar.tsx +3 -2
  42. package/src/components/Button/button.principles.stories.tsx +3 -3
  43. package/src/components/Calendar/calendar.principles.stories.tsx +3 -3
  44. package/src/components/Carousel/carousel.principles.stories.tsx +2 -2
  45. package/src/components/Chart/chart.principles.stories.tsx +4 -4
  46. package/src/components/Checkbox/checkbox.principles.stories.tsx +2 -2
  47. package/src/components/Chip/chip.principles.stories.tsx +3 -3
  48. package/src/components/Coachmark/coachmark.principles.stories.tsx +3 -3
  49. package/src/components/Combobox/combobox.anatomy.stories.tsx +2 -2
  50. package/src/components/Combobox/combobox.principles.stories.tsx +5 -5
  51. package/src/components/Command/command.principles.stories.tsx +7 -7
  52. package/src/components/DataTable/data-table.principles.stories.tsx +3 -3
  53. package/src/components/DataTable/data-table.spec.md +23 -15
  54. package/src/components/DataTable/data-table.stories.tsx +29 -25
  55. package/src/components/DataTable/data-table.tsx +92 -44
  56. package/src/components/DateGrid/date-grid.principles.stories.tsx +4 -4
  57. package/src/components/DatePicker/date-picker.principles.stories.tsx +5 -5
  58. package/src/components/DescriptionList/description-list.principles.stories.tsx +5 -5
  59. package/src/components/Dialog/dialog.principles.stories.tsx +4 -4
  60. package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +1 -1
  61. package/src/components/DropdownMenu/dropdown-menu.principles.stories.tsx +5 -5
  62. package/src/components/Empty/empty.principles.stories.tsx +2 -2
  63. package/src/components/Field/field.principles.stories.tsx +4 -4
  64. package/src/components/FileItem/file-item.principles.stories.tsx +5 -5
  65. package/src/components/FileUpload/file-upload.principles.stories.tsx +4 -4
  66. package/src/components/FileViewer/file-viewer.principles.stories.tsx +5 -5
  67. package/src/components/HoverCard/hover-card.principles.stories.tsx +5 -5
  68. package/src/components/Input/input.principles.stories.tsx +4 -4
  69. package/src/components/LinkInput/link-input.principles.stories.tsx +5 -5
  70. package/src/components/Menu/menu-item.principles.stories.tsx +7 -7
  71. package/src/components/Notice/notice.principles.stories.tsx +7 -7
  72. package/src/components/NumberInput/number-input.principles.stories.tsx +4 -4
  73. package/src/components/OverflowIndicator/overflow-indicator.principles.stories.tsx +5 -5
  74. package/src/components/PeoplePicker/people-picker.principles.stories.tsx +3 -3
  75. package/src/components/Popover/popover.principles.stories.tsx +1 -1
  76. package/src/components/ProfileCard/profile-card.principles.stories.tsx +1 -1
  77. package/src/components/ProgressBar/progress-bar.principles.stories.tsx +2 -2
  78. package/src/components/RadioGroup/radio-group.principles.stories.tsx +2 -2
  79. package/src/components/Rating/rating.principles.stories.tsx +3 -3
  80. package/src/components/ScrollArea/scroll-area.principles.stories.tsx +4 -4
  81. package/src/components/Select/select.principles.stories.tsx +5 -5
  82. package/src/components/SelectMenu/select-menu.principles.stories.tsx +8 -8
  83. package/src/components/SelectionControl/selection-item.principles.stories.tsx +7 -7
  84. package/src/components/Separator/separator.principles.stories.tsx +4 -4
  85. package/src/components/Sheet/sheet.principles.stories.tsx +2 -2
  86. package/src/components/Sidebar/sidebar.principles.stories.tsx +4 -4
  87. package/src/components/Sidebar/sidebar.spec.md +1 -1
  88. package/src/components/Skeleton/skeleton.principles.stories.tsx +5 -5
  89. package/src/components/Slider/slider.principles.stories.tsx +3 -3
  90. package/src/components/Steps/steps.principles.stories.tsx +4 -4
  91. package/src/components/Switch/switch.principles.stories.tsx +1 -1
  92. package/src/components/Tabs/tabs.principles.stories.tsx +3 -3
  93. package/src/components/Tag/tag.principles.stories.tsx +3 -3
  94. package/src/components/Textarea/textarea.principles.stories.tsx +2 -2
  95. package/src/components/TimePicker/time-picker.principles.stories.tsx +5 -5
  96. package/src/components/Toast/toast.principles.stories.tsx +2 -2
  97. package/src/components/Tooltip/tooltip.principles.stories.tsx +3 -3
  98. package/src/components/TreeView/tree-view.principles.stories.tsx +5 -5
  99. package/src/components/TreeView/tree-view.stories.tsx +1 -1
  100. package/src/tokens/color/color.spec.md +2 -0
@@ -4,7 +4,7 @@ import React from 'react'
4
4
  import type { Meta, StoryObj } from '@storybook/react'
5
5
  import { createColumnHelper, type ColumnDef } from '@tanstack/react-table'
6
6
  import { Pencil, Trash2, MoreVertical, Search, Filter, Eye, Download, Plus, ArrowUpDown } from 'lucide-react'
7
- import { DataTable } from './data-table'
7
+ import { DataTable, type DataTableSelection } from './data-table'
8
8
  import { DataTableSortManager } from './data-table-sort-manager'
9
9
  import { DataTableColumnVisibilityPanel } from './data-table-column-visibility-panel'
10
10
  import { DataTableFilterPanel, evaluateTree, createEmptyFilterTree, isFilterTreeActive, type FilterTree } from './data-table-filter-panel'
@@ -948,8 +948,7 @@ export const WithBulkActions: Story = {
948
948
  name: '選取 + 批次操作',
949
949
  parameters: { layout: 'fullscreen' },
950
950
  render: () => {
951
- const [selection, setSelection] = React.useState<string[]>([])
952
- const [allSelected, setAllSelected] = React.useState(false)
951
+ const [selection, setSelection] = React.useState<DataTableSelection>({ mode: 'include', ids: [] })
953
952
  const [search, setSearch] = React.useState('')
954
953
  const [columnVisibility, setColumnVisibility] = React.useState<Record<string, boolean>>({})
955
954
  // Issue 3(2026-05-10):columnSearch 移到 `<DataTableColumnVisibilityPanel>` 內 own,
@@ -968,13 +967,16 @@ export const WithBulkActions: Story = {
968
967
  [search]
969
968
  )
970
969
  const VISIBLE = filteredData.length
971
- // **NEW fix(2026-05-04)**:showHint 必含 selection.length > 0 前提,否則「清除選取」後 allSelected
972
- // 還是 true 邏輯走「: true」branch Alert render「已選取全部 N 個」 state
973
- const showHint = selection.length > 0 && (
974
- !allSelected
975
- ? selection.length === VISIBLE && VISIBLE > 0 && TOTAL > VISIBLE
976
- : true
977
- )
970
+ // 反向選取(inverted)showcase:include 全可見已選且 dataset 更大 offer「選取全部 M」;
971
+ // all 模式顯示「已選取全部 M(排除 K)」。count = M excluded(consumer 端計算)。
972
+ const isAll = selection.mode === 'all'
973
+ const visibleIds = filteredData.map((p) => p.sku)
974
+ const selectedCount = isAll ? TOTAL - selection.excluded.length : selection.ids.length
975
+ const visibleSelectedIds = isAll
976
+ ? visibleIds.filter((id) => !selection.excluded.includes(id))
977
+ : selection.ids.filter((id) => visibleIds.includes(id))
978
+ const allVisibleSelected = VISIBLE > 0 && visibleSelectedIds.length === VISIBLE
979
+ const showHint = isAll || (allVisibleSelected && TOTAL > VISIBLE)
978
980
 
979
981
  return (
980
982
  // 撐滿 parent(layout=fullscreen);
@@ -1068,6 +1070,7 @@ export const WithBulkActions: Story = {
1068
1070
  selectable
1069
1071
  selection={selection}
1070
1072
  onSelectionChange={setSelection}
1073
+ totalCount={TOTAL}
1071
1074
  columnVisibility={columnVisibility}
1072
1075
  onColumnVisibilityChange={setColumnVisibility}
1073
1076
  getRowId={(row) => row.sku}
@@ -1092,7 +1095,7 @@ export const WithBulkActions: Story = {
1092
1095
 
1093
1096
  {/* 底部 chrome group(撤回前一版 absolute overlay,2026-05-04 user 抓 BulkActionBar 沒底色 + 蓋表底列 regression):
1094
1097
  回 flex flow 自然推 table。Q7 mount-time growth 真因 = virtualizer estimateRowHeight ≠ token,已在 DataTable 內修(estimate size-aware) */}
1095
- {(showHint || selection.length > 0) && (
1098
+ {(showHint || selectedCount > 0) && (
1096
1099
  <div className="flex flex-col">
1097
1100
  {showHint && (
1098
1101
  <Alert
@@ -1100,24 +1103,24 @@ export const WithBulkActions: Story = {
1100
1103
  placement="fixed"
1101
1104
  dismissible={false}
1102
1105
  title={
1103
- allSelected ? (
1106
+ isAll ? (
1104
1107
  <>
1105
- 已選取全部 {TOTAL} 個項目。{' '}
1108
+ 已選取全部 {TOTAL} 個項目{selection.excluded.length > 0 ? `(排除 ${selection.excluded.length} 個)` : ''}。{' '}
1106
1109
  <button
1107
1110
  type="button"
1108
- onClick={() => { setSelection([]); setAllSelected(false) }}
1109
- className="text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
1111
+ onClick={() => setSelection({ mode: 'include', ids: [] })}
1112
+ className="text-primary hover:text-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
1110
1113
  >
1111
1114
  清除選取項目
1112
1115
  </button>
1113
1116
  </>
1114
1117
  ) : (
1115
1118
  <>
1116
- 已選取本頁全部 {selection.length} 個。{' '}
1119
+ 已選取本頁全部 {selectedCount} 個。{' '}
1117
1120
  <button
1118
1121
  type="button"
1119
- onClick={() => setAllSelected(true)}
1120
- className="text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
1122
+ onClick={() => setSelection({ mode: 'all', excluded: [] })}
1123
+ className="text-primary hover:text-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
1121
1124
  >
1122
1125
  點此選取全部 {TOTAL} 個項目
1123
1126
  </button>
@@ -1126,10 +1129,11 @@ export const WithBulkActions: Story = {
1126
1129
  }
1127
1130
  />
1128
1131
  )}
1129
- {selection.length > 0 && (
1132
+ {selectedCount > 0 && (
1130
1133
  <BulkActionBar
1131
- selection={selection}
1132
- onClear={() => { setSelection([]); setAllSelected(false) }}
1134
+ selection={visibleSelectedIds}
1135
+ totalSelected={selectedCount}
1136
+ onClear={() => setSelection({ mode: 'include', ids: [] })}
1133
1137
  actions={
1134
1138
  <>
1135
1139
  <Button variant="tertiary" size="md" startIcon={Download}>下載</Button>
@@ -1165,7 +1169,7 @@ export const SelectionKeyboardAndShift: Story = {
1165
1169
  height="400px"
1166
1170
  selectable
1167
1171
  selection={selection}
1168
- onSelectionChange={setSelection}
1172
+ onSelectionChange={(s) => setSelection(s.mode === 'include' ? s.ids : [])}
1169
1173
  getRowId={(row) => row.sku}
1170
1174
  />
1171
1175
  </div>
@@ -1192,7 +1196,7 @@ export const SelectionSingleMode: Story = {
1192
1196
  height="auto"
1193
1197
  selectable="single"
1194
1198
  selection={selection}
1195
- onSelectionChange={setSelection}
1199
+ onSelectionChange={(s) => setSelection(s.mode === 'include' ? s.ids : [])}
1196
1200
  getRowId={(row) => row.sku}
1197
1201
  />
1198
1202
  </div>
@@ -1216,7 +1220,7 @@ export const SelectionDisabledRows: Story = {
1216
1220
  height="auto"
1217
1221
  selectable
1218
1222
  selection={selection}
1219
- onSelectionChange={setSelection}
1223
+ onSelectionChange={(s) => setSelection(s.mode === 'include' ? s.ids : [])}
1220
1224
  getRowId={(row) => row.sku}
1221
1225
  isRowSelectable={(row) => row.stock !== 'Out of stock'}
1222
1226
  />
@@ -1819,7 +1823,7 @@ export const RoadmapAllInOne: Story = {
1819
1823
  inlineEdit
1820
1824
  selectable
1821
1825
  selection={selection}
1822
- onSelectionChange={setSelection}
1826
+ onSelectionChange={(s) => setSelection(s.mode === 'include' ? s.ids : [])}
1823
1827
  columnVisibility={columnVisibility}
1824
1828
  onColumnVisibilityChange={setColumnVisibility}
1825
1829
  pinnedLeftColumns={['id']}
@@ -134,12 +134,14 @@ export interface DataTableProps<TData>
134
134
  spreadsheetMode?: boolean
135
135
 
136
136
  // ── L2 Selection(see data-table.spec.md「L2 選取」)──
137
- /** 已選 row IDs(controlled) */
138
- selection?: string[]
139
- /** 預設選取(uncontrolled) */
140
- defaultSelection?: string[]
141
- /** Selection 變更 callback */
142
- onSelectionChange?: (next: string[]) => void
137
+ /** 已選列(controlled)。傳 string[] = include shorthand;傳 DataTableSelection 支援反向選取(all + excluded) */
138
+ selection?: string[] | DataTableSelection
139
+ /** 預設選取(uncontrolled);同上接受 string[] 或 DataTableSelection */
140
+ defaultSelection?: string[] | DataTableSelection
141
+ /** Selection 變更 callback(emit DataTableSelection union;include / all 兩模型) */
142
+ onSelectionChange?: (next: DataTableSelection) => void
143
+ /** 全資料集筆數 M(server-side / filter 後);all 模式 count = totalCount − excluded.length(consumer 計算) */
144
+ totalCount?: number
143
145
  /** 是否啟用 selection / 模式;true 等同 'multi' */
144
146
  selectable?: boolean | 'single' | 'multi'
145
147
  /** Row 是否可選(disabled rows 只 disable checkbox,row 內容正常 render) */
@@ -264,6 +266,48 @@ const cellEditId = (rowId: string, colId: string) => `${rowId}__${colId}`
264
266
  // (詳 ./data-table.css)— consumer 可走 CSS override 改值,不再 hard-code in TS。
265
267
  // L2 selection 內部 column id(避免 magic string 重複)
266
268
  const SELECT_COL_ID = '__select__'
269
+
270
+ /**
271
+ * L2 選取模型(discriminated union,2026-06-22 對齊 MUI X DataGrid v8
272
+ * rowSelectionModel { type: include | exclude, ids } + AG Grid selectAll + toggledNodes)。
273
+ *
274
+ * - mode==='include' (ids):只選 ids 列(預設;= 載入/可見列逐一選取)。
275
+ * - mode==='all' (excluded):全資料集(filter 後)選取,扣掉 excluded —— 反向選取(inverted)。
276
+ * 解決「全選 10k 筆只載 50 筆 → 無法列舉其餘 ID」:all 模式「選取 = 全集 − excluded」,
277
+ * 任何 toggle 都只是 excluded 的 add/remove,對任意順序封閉、O(1)、不需列舉未載入 ID。
278
+ *
279
+ * 計數(consumer):mode==='all' ? totalCount − excluded.length : ids.length(需傳 totalCount)。
280
+ * 進入 all 模式:consumer 在「選取全部 M」hint 點擊時 setSelection({ mode: 'all', excluded: [] })。
281
+ */
282
+ export type DataTableSelection =
283
+ | { mode: 'include'; ids: string[] }
284
+ | { mode: 'all'; excluded: string[] }
285
+
286
+ // 正規化:string[] shorthand → { mode:'include' }(向後相容既有 consumer 傳 ID 陣列)
287
+ function normalizeSelection(
288
+ input: string[] | DataTableSelection | undefined,
289
+ ): DataTableSelection | undefined {
290
+ if (input === undefined) return undefined
291
+ if (Array.isArray(input)) return { mode: 'include', ids: input }
292
+ return input
293
+ }
294
+
295
+ // union-aware「把 ids 設成 willSelect 狀態」純函式(對任意 toggle 順序封閉)
296
+ function applySelectIds(
297
+ sel: DataTableSelection,
298
+ ids: string[],
299
+ willSelect: boolean,
300
+ ): DataTableSelection {
301
+ if (sel.mode === 'include') {
302
+ const set = new Set(sel.ids)
303
+ ids.forEach((id) => { if (willSelect) set.add(id); else set.delete(id) })
304
+ return { mode: 'include', ids: Array.from(set) }
305
+ }
306
+ // all 模式:選取 = 全集 − excluded → willSelect 移出 excluded;取消選取 加進 excluded
307
+ const set = new Set(sel.excluded)
308
+ ids.forEach((id) => { if (willSelect) set.delete(id); else set.add(id) })
309
+ return { mode: 'all', excluded: Array.from(set) }
310
+ }
267
311
  const cellPadding: React.CSSProperties = { paddingBlock: 'var(--table-cell-py)', paddingInline: 'var(--table-cell-px)' }
268
312
  const HEADER_BG = 'bg-muted'
269
313
 
@@ -774,6 +818,7 @@ function DataTableInner<TData>(
774
818
  estimateRowHeight, tableOptions, rowActions, cellErrors,
775
819
  pinnedLeftColumns, pinnedRightColumns, inlineEdit = false,
776
820
  selection: selectionProp, defaultSelection, onSelectionChange,
821
+ totalCount,
777
822
  selectable = false, isRowSelectable, getRowId, getRowAriaLabel,
778
823
  preserveSelectionOnFilter = false,
779
824
  columnVisibility: columnVisibilityProp, defaultColumnVisibility, onColumnVisibilityChange,
@@ -887,9 +932,9 @@ function DataTableInner<TData>(
887
932
  // ── L2 Selection state ──
888
933
  const enabled = selectable !== false
889
934
  const mode = selectable === 'single' ? 'single' : 'multi'
890
- const [selection, setSelection] = useControllable<string[]>({
891
- value: selectionProp,
892
- defaultValue: defaultSelection ?? [],
935
+ const [selection, setSelection] = useControllable<DataTableSelection>({
936
+ value: normalizeSelection(selectionProp),
937
+ defaultValue: normalizeSelection(defaultSelection) ?? { mode: 'include', ids: [] },
893
938
  onChange: onSelectionChange,
894
939
  })
895
940
  // Shift-click anchor:存最後一次「單擊」的 row id,shift-click 時做區間選
@@ -1381,7 +1426,7 @@ function DataTableInner<TData>(
1381
1426
  // 內部 checkbox/radio 用 stopPropagation 避免 double-fire
1382
1427
  const onCellClick = isDisabled ? undefined : (e: React.MouseEvent) => {
1383
1428
  e.stopPropagation()
1384
- if (mode === 'single') setSelection([rowId])
1429
+ if (mode === 'single') setSelection({ mode: 'include', ids: [rowId] })
1385
1430
  else toggleRow(rowId, rowOriginal, { shiftKey: e.shiftKey })
1386
1431
  }
1387
1432
  return (
@@ -1408,7 +1453,7 @@ function DataTableInner<TData>(
1408
1453
  ) : (
1409
1454
  <Checkbox
1410
1455
  size={checkboxSize}
1411
- checked={selectionSet.has(rowId)}
1456
+ checked={isSelectedId(rowId)}
1412
1457
  disabled={isDisabled}
1413
1458
  aria-label={ariaLabel}
1414
1459
  onClick={(e) => {
@@ -1679,8 +1724,10 @@ function DataTableInner<TData>(
1679
1724
  React.useEffect(() => {
1680
1725
  if (!enabled || preserveSelectionOnFilter) return
1681
1726
  setSelection(prev => {
1682
- const filtered = prev.filter(id => visibleRowIdsSet.has(id))
1683
- return filtered.length === prev.length ? prev : filtered
1727
+ // all 模式 = 「全部符合當前 filter」→ 不清(excluded 留著,被 filter 掉的 excluded 列無害)
1728
+ if (prev.mode === 'all') return prev
1729
+ const filtered = prev.ids.filter(id => visibleRowIdsSet.has(id))
1730
+ return filtered.length === prev.ids.length ? prev : { mode: 'include', ids: filtered }
1684
1731
  })
1685
1732
  }, [visibleRowIdsKey, enabled, preserveSelectionOnFilter, visibleRowIdsSet, setSelection])
1686
1733
 
@@ -1692,9 +1739,22 @@ function DataTableInner<TData>(
1692
1739
  .map(r => r.id)
1693
1740
  }, [rows, enabled, isRowSelectable])
1694
1741
 
1742
+ // Union-aware「某列是否選取」+ 計數(include = ids 內;all = 不在 excluded 內)
1743
+ const includeSet = React.useMemo(
1744
+ () => (selection.mode === 'include' ? new Set(selection.ids) : new Set<string>()),
1745
+ [selection],
1746
+ )
1747
+ const excludeSet = React.useMemo(
1748
+ () => (selection.mode === 'all' ? new Set(selection.excluded) : new Set<string>()),
1749
+ [selection],
1750
+ )
1751
+ const isSelectedId = React.useCallback(
1752
+ (id: string) => (selection.mode === 'include' ? includeSet.has(id) : !excludeSet.has(id)),
1753
+ [selection.mode, includeSet, excludeSet],
1754
+ )
1755
+ const hasAnySelection = selection.mode === 'all' || includeSet.size > 0
1695
1756
  // Header tri-state checkbox value
1696
- const selectionSet = React.useMemo(() => new Set(selection), [selection])
1697
- const visibleSelectedCount = selectableVisibleIds.filter(id => selectionSet.has(id)).length
1757
+ const visibleSelectedCount = selectableVisibleIds.filter(id => isSelectedId(id)).length
1698
1758
  const headerCheckedState: boolean | 'indeterminate' =
1699
1759
  selectableVisibleIds.length === 0 ? false
1700
1760
  : visibleSelectedCount === 0 ? false
@@ -1708,20 +1768,16 @@ function DataTableInner<TData>(
1708
1768
  )
1709
1769
 
1710
1770
  const toggleHeaderCheckbox = React.useCallback(() => {
1711
- if (headerCheckedState === true) {
1712
- // 清掉本頁可見已選(保留可見外的 selection)
1713
- const visibleSet = new Set(selectableVisibleIds)
1714
- setSelection(prev => prev.filter(id => !visibleSet.has(id)))
1715
- } else {
1716
- // 選全可見(扣除 disabled);保留可見外的既有 selection
1717
- setSelection(prev => Array.from(new Set([...prev, ...selectableVisibleIds])))
1718
- }
1771
+ // header tri-state visible-scoped:全可見已選 → 取消可見;否則 → 選全可見。
1772
+ // include / all 兩模型由 applySelectIds 處理(all 模式 toggle 改寫 excluded)
1773
+ const willSelect = headerCheckedState !== true
1774
+ setSelection(prev => applySelectIds(prev, selectableVisibleIds, willSelect))
1719
1775
  }, [headerCheckedState, selectableVisibleIds, setSelection])
1720
1776
 
1721
1777
  const toggleRow = React.useCallback((rowId: string, rowOriginal: TData, opts?: { shiftKey?: boolean }) => {
1722
1778
  if (isRowSelectable && !isRowSelectable(rowOriginal)) return
1723
1779
  if (mode === 'single') {
1724
- setSelection(selectionSet.has(rowId) ? [] : [rowId])
1780
+ setSelection(isSelectedId(rowId) ? { mode: 'include', ids: [] } : { mode: 'include', ids: [rowId] })
1725
1781
  anchorRowIdRef.current = rowId
1726
1782
  return
1727
1783
  }
@@ -1739,24 +1795,16 @@ function DataTableInner<TData>(
1739
1795
  return row && (!isRowSelectable || isRowSelectable(row.original))
1740
1796
  })
1741
1797
  // Mail / GitHub 慣例:shift-click 把 range 全變「rowId 點擊後該變的狀態」
1742
- const willCheck = !selectionSet.has(rowId)
1743
- setSelection(prev => {
1744
- const set = new Set(prev)
1745
- rangeIds.forEach(id => willCheck ? set.add(id) : set.delete(id))
1746
- return Array.from(set)
1747
- })
1798
+ const willCheck = !isSelectedId(rowId)
1799
+ setSelection(prev => applySelectIds(prev, rangeIds, willCheck))
1748
1800
  return
1749
1801
  }
1750
1802
  }
1751
- // 一般 toggle + 更新 anchor
1752
- setSelection(prev => {
1753
- const set = new Set(prev)
1754
- if (set.has(rowId)) set.delete(rowId)
1755
- else set.add(rowId)
1756
- return Array.from(set)
1757
- })
1803
+ // 一般 toggle + 更新 anchor(include / all 由 applySelectIds 處理)
1804
+ const willCheck = !isSelectedId(rowId)
1805
+ setSelection(prev => applySelectIds(prev, [rowId], willCheck))
1758
1806
  anchorRowIdRef.current = rowId
1759
- }, [isRowSelectable, mode, selectionSet, rows, visibleIdToRow, setSelection])
1807
+ }, [isRowSelectable, mode, isSelectedId, rows, visibleIdToRow, setSelection])
1760
1808
 
1761
1809
  // ── Cmd+A / Esc / Arrow keys 鍵盤 handler(table-level)──
1762
1810
  // code-quality-allow: long-function — single keyboard dispatch covering Cmd+A / Esc / Arrow / Space + selection state mutations,拆 sub-handler 會切散 keyboard mode coherence
@@ -1823,18 +1871,18 @@ function DataTableInner<TData>(
1823
1871
  // Cmd/Ctrl+A:選全可見(扣 disabled)— 對齊 Mail / GitHub / Linear 慣例
1824
1872
  if ((e.metaKey || e.ctrlKey) && e.key === 'a' && mode === 'multi') {
1825
1873
  e.preventDefault()
1826
- setSelection(prev => Array.from(new Set([...prev, ...selectableVisibleIds])))
1874
+ setSelection(prev => applySelectIds(prev, selectableVisibleIds, true))
1827
1875
  return
1828
1876
  }
1829
1877
  // Esc:clear selection
1830
- if (e.key === 'Escape' && selection.length > 0) {
1878
+ if (e.key === 'Escape' && hasAnySelection) {
1831
1879
  e.preventDefault()
1832
- setSelection([])
1880
+ setSelection({ mode: 'include', ids: [] })
1833
1881
  anchorRowIdRef.current = null
1834
1882
  return
1835
1883
  }
1836
1884
  },
1837
- [enabled, mode, selection.length, selectableVisibleIds, setSelection,
1885
+ [enabled, mode, hasAnySelection, selectableVisibleIds, setSelection,
1838
1886
  spreadsheetMode, selectedCellId, editingCellId, table, isCellEditable]
1839
1887
  )
1840
1888
 
@@ -2906,8 +2954,8 @@ function DataTableInner<TData>(
2906
2954
  if (enabled && mode === 'single') {
2907
2955
  return (
2908
2956
  <RadioGroupPrimitive.Root
2909
- value={selection[0] ?? ''}
2910
- onValueChange={(v) => v && setSelection([v])}
2957
+ value={selection.mode === 'include' ? (selection.ids[0] ?? '') : ''}
2958
+ onValueChange={(v) => v && setSelection({ mode: 'include', ids: [v] })}
2911
2959
  >
2912
2960
  {wrapWithDnd(tableContent)}
2913
2961
  </RadioGroupPrimitive.Root>
@@ -71,16 +71,16 @@ export const UsageGuidance: Story = {
71
71
  <p>適合 DateGrid 的真實業務場景(點擊跳轉「展示」頁範例):</p>
72
72
  <ul className="space-y-1">
73
73
  <li>
74
- <LinkTo kind="Design System/Internal/DateGrid/展示" name="單日 — 生日 / 到期日"><span className="text-primary hover:underline font-medium cursor-pointer">Single — 生日 / 到期日</span></LinkTo>
74
+ <LinkTo kind="Design System/Internal/DateGrid/展示" name="單日 — 生日 / 到期日"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">Single — 生日 / 到期日</span></LinkTo>
75
75
  </li>
76
76
  <li>
77
- <LinkTo kind="Design System/Internal/DateGrid/展示" name="多日 — 活動可參加日期"><span className="text-primary hover:underline font-medium cursor-pointer">Multiple — 活動可參加日期</span></LinkTo>
77
+ <LinkTo kind="Design System/Internal/DateGrid/展示" name="多日 — 活動可參加日期"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">Multiple — 活動可參加日期</span></LinkTo>
78
78
  </li>
79
79
  <li>
80
- <LinkTo kind="Design System/Internal/DateGrid/展示" name="範圍 — 分析時段 / 訂單範圍"><span className="text-primary hover:underline font-medium cursor-pointer">Range — 分析時段 / 訂單範圍</span></LinkTo>
80
+ <LinkTo kind="Design System/Internal/DateGrid/展示" name="範圍 — 分析時段 / 訂單範圍"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">Range — 分析時段 / 訂單範圍</span></LinkTo>
81
81
  </li>
82
82
  <li>
83
- <LinkTo kind="Design System/Internal/DateGrid/展示" name="行內 — 儀表板小卡"><span className="text-primary hover:underline font-medium cursor-pointer">行內 — Linear 專案截止日小卡</span></LinkTo>
83
+ <LinkTo kind="Design System/Internal/DateGrid/展示" name="行內 — 儀表板小卡"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">行內 — Linear 專案截止日小卡</span></LinkTo>
84
84
  </li>
85
85
  </ul>
86
86
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見下方「vs 近親」)。</p>
@@ -47,11 +47,11 @@ export const UsageGuidance: Story = {
47
47
  <div className="prose prose-sm max-w-prose mb-8">
48
48
  <p>適合 DatePicker 的真實業務場景(點擊跳轉「展示」頁範例):</p>
49
49
  <ul className="space-y-1">
50
- <li><LinkTo kind="Design System/Components/DatePicker/展示" name="四模式"><span className="text-primary hover:underline font-medium cursor-pointer">請假單送審後日期欄位從可編輯轉唯讀/純展示(四模式)</span></LinkTo></li>
51
- <li><LinkTo kind="Design System/Components/DatePicker/展示" name="可清除"><span className="text-primary hover:underline font-medium cursor-pointer">篩選器的選填截止日,填錯一鍵清空(可清除)</span></LinkTo></li>
52
- <li><LinkTo kind="Design System/Components/DatePicker/展示" name="尺寸"><span className="text-primary hover:underline font-medium cursor-pointer">緊湊工具列與標準表單的尺寸對應(尺寸)</span></LinkTo></li>
53
- <li><LinkTo kind="Design System/Components/DatePicker/展示" name="範圍模式:訂房 / 訂機票情境"><span className="text-primary hover:underline font-medium cursor-pointer">Range:訂房 / 訂機票情境</span></LinkTo></li>
54
- <li><LinkTo kind="Design System/Components/DatePicker/展示" name="展示樣式"><span className="text-primary hover:underline font-medium cursor-pointer">審批詳情頁唯讀展示申請日期(展示樣式)</span></LinkTo></li>
50
+ <li><LinkTo kind="Design System/Components/DatePicker/展示" name="四模式"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">請假單送審後日期欄位從可編輯轉唯讀/純展示(四模式)</span></LinkTo></li>
51
+ <li><LinkTo kind="Design System/Components/DatePicker/展示" name="可清除"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">篩選器的選填截止日,填錯一鍵清空(可清除)</span></LinkTo></li>
52
+ <li><LinkTo kind="Design System/Components/DatePicker/展示" name="尺寸"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">緊湊工具列與標準表單的尺寸對應(尺寸)</span></LinkTo></li>
53
+ <li><LinkTo kind="Design System/Components/DatePicker/展示" name="範圍模式:訂房 / 訂機票情境"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">Range:訂房 / 訂機票情境</span></LinkTo></li>
54
+ <li><LinkTo kind="Design System/Components/DatePicker/展示" name="展示樣式"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">審批詳情頁唯讀展示申請日期(展示樣式)</span></LinkTo></li>
55
55
  </ul>
56
56
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見下方 vs 近親 段)。</p>
57
57
  </div>
@@ -53,19 +53,19 @@ export const UsageGuidance: Story = {
53
53
  <p>適合 DescriptionList 的真實業務場景(點擊跳轉「展示」頁範例):</p>
54
54
  <ul className="space-y-1">
55
55
  <li>
56
- <LinkTo kind="Design System/Components/DescriptionList/展示" name="使用者個資"><span className="text-primary hover:underline font-medium cursor-pointer">使用者個資</span></LinkTo>
56
+ <LinkTo kind="Design System/Components/DescriptionList/展示" name="使用者個資"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">使用者個資</span></LinkTo>
57
57
  </li>
58
58
  <li>
59
- <LinkTo kind="Design System/Components/DescriptionList/展示" name="產品規格"><span className="text-primary hover:underline font-medium cursor-pointer">產品規格</span></LinkTo>
59
+ <LinkTo kind="Design System/Components/DescriptionList/展示" name="產品規格"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">產品規格</span></LinkTo>
60
60
  </li>
61
61
  <li>
62
- <LinkTo kind="Design System/Components/DescriptionList/展示" name="訂單明細"><span className="text-primary hover:underline font-medium cursor-pointer">訂單明細</span></LinkTo>
62
+ <LinkTo kind="Design System/Components/DescriptionList/展示" name="訂單明細"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">訂單明細</span></LinkTo>
63
63
  </li>
64
64
  <li>
65
- <LinkTo kind="Design System/Components/DescriptionList/展示" name="詳情面板"><span className="text-primary hover:underline font-medium cursor-pointer">詳情面板</span></LinkTo>
65
+ <LinkTo kind="Design System/Components/DescriptionList/展示" name="詳情面板"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">詳情面板</span></LinkTo>
66
66
  </li>
67
67
  <li>
68
- <LinkTo kind="Design System/Components/DescriptionList/展示" name="水平佈局"><span className="text-primary hover:underline font-medium cursor-pointer">水平佈局</span></LinkTo>
68
+ <LinkTo kind="Design System/Components/DescriptionList/展示" name="水平佈局"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">水平佈局</span></LinkTo>
69
69
  </li>
70
70
  </ul>
71
71
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見 <code>Vs*Rule</code> stories)。</p>
@@ -52,16 +52,16 @@ export const UsageGuidance: Story = {
52
52
  <p>適合 Dialog 的真實業務場景(點擊跳轉「展示」頁範例):</p>
53
53
  <ul className="space-y-1">
54
54
  <li>
55
- <LinkTo kind="Design System/Components/Dialog/展示" name="表單"><span className="text-primary hover:underline font-medium cursor-pointer">表單</span></LinkTo>
55
+ <LinkTo kind="Design System/Components/Dialog/展示" name="表單"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">表單</span></LinkTo>
56
56
  </li>
57
57
  <li>
58
- <LinkTo kind="Design System/Components/Dialog/展示" name="長內容"><span className="text-primary hover:underline font-medium cursor-pointer">長內容</span></LinkTo>
58
+ <LinkTo kind="Design System/Components/Dialog/展示" name="長內容"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">長內容</span></LinkTo>
59
59
  </li>
60
60
  <li>
61
- <LinkTo kind="Design System/Components/Dialog/展示" name="危險操作"><span className="text-primary hover:underline font-medium cursor-pointer">危險操作</span></LinkTo>
61
+ <LinkTo kind="Design System/Components/Dialog/展示" name="危險操作"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">危險操作</span></LinkTo>
62
62
  </li>
63
63
  <li>
64
- <LinkTo kind="Design System/Components/Dialog/展示" name="主體放清單"><span className="text-primary hover:underline font-medium cursor-pointer">主體放清單</span></LinkTo>
64
+ <LinkTo kind="Design System/Components/Dialog/展示" name="主體放清單"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">主體放清單</span></LinkTo>
65
65
  </li>
66
66
  </ul>
67
67
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見 <code>Vs*Rule</code> stories)。</p>
@@ -963,7 +963,7 @@ export const Accessibility = {
963
963
  <div className="max-w-3xl text-body text-fg-secondary">
964
964
  <h3 className="text-h5 text-foreground mb-2">無障礙設計</h3>
965
965
  <p className="whitespace-pre-line">{"無障礙能力沿用 Radix 選單元件的內建支援,涵蓋語意角色、選單導覽與焦點管理,設計師與工程師不需額外設定。\n\n鍵盤操作:\n\n- Tab — 將焦點移到觸發按鈕\n- Enter / Space / 向下鍵 — 開啟選單\n- 向上鍵 / 向下鍵 — 在選項之間移動\n- Enter — 選擇目前的選項\n- Esc — 關閉整個選單(含子選單;逐層收合用左方向鍵)\n\n焦點管理:選單開啟時焦點會進入選單,關閉後自動回到原本的觸發按鈕;目前導覽到的選項會以明顯的 highlight 底色標示,鍵盤使用者隨時看得到自己在哪一項。\n\n驗證:鍵盤可完整操作(不需滑鼠),文字對比達 WCAG AA;在 Storybook 無障礙檢查面板上應為零項嚴重問題。"}</p>
966
- <p className="mt-4 text-fg-muted">完整 ARIA 細節參考 <a className="text-primary hover:underline" href="https://www.radix-ui.com/primitives/docs/components/dropdown-menu#accessibility" target="_blank" rel="noreferrer">Radix Accessibility 文件</a>。</p>
966
+ <p className="mt-4 text-fg-muted">完整 ARIA 細節參考 <a className="text-primary hover:text-primary-hover" href="https://www.radix-ui.com/primitives/docs/components/dropdown-menu#accessibility" target="_blank" rel="noreferrer">Radix Accessibility 文件</a>。</p>
967
967
  </div>
968
968
  ),
969
969
  }
@@ -58,19 +58,19 @@ export const UsageGuidance: Story = {
58
58
  <p>適合 DropdownMenu 的真實業務場景(點擊跳轉「展示」頁範例):</p>
59
59
  <ul className="space-y-1">
60
60
  <li>
61
- <LinkTo kind="Design System/Components/DropdownMenu/展示" name="群組"><span className="text-primary hover:underline font-medium cursor-pointer">群組 — 帳號選單(個人資料 / 設定 / 登出分組)</span></LinkTo>
61
+ <LinkTo kind="Design System/Components/DropdownMenu/展示" name="群組"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">群組 — 帳號選單(個人資料 / 設定 / 登出分組)</span></LinkTo>
62
62
  </li>
63
63
  <li>
64
- <LinkTo kind="Design System/Components/DropdownMenu/展示" name="後綴"><span className="text-primary hover:underline font-medium cursor-pointer">後綴 — 收件匣未讀數 badge、外開連結提示</span></LinkTo>
64
+ <LinkTo kind="Design System/Components/DropdownMenu/展示" name="後綴"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">後綴 — 收件匣未讀數 badge、外開連結提示</span></LinkTo>
65
65
  </li>
66
66
  <li>
67
- <LinkTo kind="Design System/Components/DropdownMenu/展示" name="子選單"><span className="text-primary hover:underline font-medium cursor-pointer">子選單 — 主題切換(淺色 / 深色 / 跟隨系統)</span></LinkTo>
67
+ <LinkTo kind="Design System/Components/DropdownMenu/展示" name="子選單"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">子選單 — 主題切換(淺色 / 深色 / 跟隨系統)</span></LinkTo>
68
68
  </li>
69
69
  <li>
70
- <LinkTo kind="Design System/Components/DropdownMenu/展示" name="勾選項"><span className="text-primary hover:underline font-medium cursor-pointer">勾選項 — 表格顯示欄位切換</span></LinkTo>
70
+ <LinkTo kind="Design System/Components/DropdownMenu/展示" name="勾選項"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">勾選項 — 表格顯示欄位切換</span></LinkTo>
71
71
  </li>
72
72
  <li>
73
- <LinkTo kind="Design System/Components/DropdownMenu/展示" name="單選"><span className="text-primary hover:underline font-medium cursor-pointer">單選 — 檔案清單排序方式</span></LinkTo>
73
+ <LinkTo kind="Design System/Components/DropdownMenu/展示" name="單選"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">單選 — 檔案清單排序方式</span></LinkTo>
74
74
  </li>
75
75
  </ul>
76
76
  <p className="text-fg-muted mt-3">判斷不確定時:先看「選完之後畫面是否需要保留選中狀態」——需要就改用 Select / SelectMenu。下方的「DropdownMenu vs 選值元件」與「群組 vs 分隔線」範例提供更完整的對照。</p>
@@ -59,10 +59,10 @@ export const UsageGuidance: Story = {
59
59
  <p>適合 Empty 的真實業務場景(點擊跳轉「展示」頁範例):</p>
60
60
  <ul className="space-y-1">
61
61
  <li>
62
- <LinkTo kind="Design System/Components/Empty/展示" name="搜尋無結果"><span className="text-primary hover:underline font-medium cursor-pointer">搜尋無結果</span></LinkTo>
62
+ <LinkTo kind="Design System/Components/Empty/展示" name="搜尋無結果"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">搜尋無結果</span></LinkTo>
63
63
  </li>
64
64
  <li>
65
- <LinkTo kind="Design System/Components/Empty/展示" name="空清單"><span className="text-primary hover:underline font-medium cursor-pointer">空清單</span></LinkTo>
65
+ <LinkTo kind="Design System/Components/Empty/展示" name="空清單"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">空清單</span></LinkTo>
66
66
  </li>
67
67
  </ul>
68
68
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見下方「vs 近親」)。</p>
@@ -49,10 +49,10 @@ export const UsageGuidance: Story = {
49
49
  <div className="prose prose-sm max-w-prose mb-8">
50
50
  <p>適合 Field 的真實業務場景(點擊跳轉「展示」頁範例):</p>
51
51
  <ul className="space-y-1">
52
- <li><LinkTo kind="Design System/Components/Field/展示" name="垂直"><span className="text-primary hover:underline font-medium cursor-pointer">註冊 / 建立專案表單 — 引導式逐欄輸入(垂直佈局)</span></LinkTo></li>
53
- <li><LinkTo kind="Design System/Components/Field/展示" name="水平"><span className="text-primary hover:underline font-medium cursor-pointer">帳號設定 / 偏好設定頁 — 修改已知屬性(水平佈局)</span></LinkTo></li>
54
- <li><LinkTo kind="Design System/Components/Field/展示" name="混合控制元件的高度對齊"><span className="text-primary hover:underline font-medium cursor-pointer">同一表單混用 Input / Select / DatePicker 等控制元件</span></LinkTo></li>
55
- <li><LinkTo kind="Design System/Components/Field/展示" name="SegmentedControl 作為 Field 控制元件"><span className="text-primary hover:underline font-medium cursor-pointer">少量互斥選項(計費週期、版面密度)用 SegmentedControl 當控制元件</span></LinkTo></li>
52
+ <li><LinkTo kind="Design System/Components/Field/展示" name="垂直"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">註冊 / 建立專案表單 — 引導式逐欄輸入(垂直佈局)</span></LinkTo></li>
53
+ <li><LinkTo kind="Design System/Components/Field/展示" name="水平"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">帳號設定 / 偏好設定頁 — 修改已知屬性(水平佈局)</span></LinkTo></li>
54
+ <li><LinkTo kind="Design System/Components/Field/展示" name="混合控制元件的高度對齊"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">同一表單混用 Input / Select / DatePicker 等控制元件</span></LinkTo></li>
55
+ <li><LinkTo kind="Design System/Components/Field/展示" name="SegmentedControl 作為 Field 控制元件"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">少量互斥選項(計費週期、版面密度)用 SegmentedControl 當控制元件</span></LinkTo></li>
56
56
  </ul>
57
57
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見下方 vs 近親 段)。</p>
58
58
  </div>
@@ -43,19 +43,19 @@ export const UsageGuidance: Story = {
43
43
  <p>適合 FileItem 的真實業務場景(點擊跳轉「展示」頁範例):</p>
44
44
  <ul className="space-y-1">
45
45
  <li>
46
- <LinkTo kind="Design System/Components/FileItem/展示" name="豐富樣式"><span className="text-primary hover:underline font-medium cursor-pointer">圖片 / 設計稿上傳 — 需要縮圖預覽(豐富樣式)</span></LinkTo>
46
+ <LinkTo kind="Design System/Components/FileItem/展示" name="豐富樣式"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">圖片 / 設計稿上傳 — 需要縮圖預覽(豐富樣式)</span></LinkTo>
47
47
  </li>
48
48
  <li>
49
- <LinkTo kind="Design System/Components/FileItem/展示" name="緊湊樣式"><span className="text-primary hover:underline font-medium cursor-pointer">CSV / JSON 批次匯入的密集清單(緊湊樣式)</span></LinkTo>
49
+ <LinkTo kind="Design System/Components/FileItem/展示" name="緊湊樣式"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">CSV / JSON 批次匯入的密集清單(緊湊樣式)</span></LinkTo>
50
50
  </li>
51
51
  <li>
52
- <LinkTo kind="Design System/Components/FileItem/展示" name="懸停替換"><span className="text-primary hover:underline font-medium cursor-pointer">上傳完成後滑入整列、狀態 icon 換成下載鈕(懸停替換)</span></LinkTo>
52
+ <LinkTo kind="Design System/Components/FileItem/展示" name="懸停替換"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">上傳完成後滑入整列、狀態 icon 換成下載鈕(懸停替換)</span></LinkTo>
53
53
  </li>
54
54
  <li>
55
- <LinkTo kind="Design System/Components/FileItem/展示" name="已上傳"><span className="text-primary hover:underline font-medium cursor-pointer">工單 / 訊息附件的已上傳靜態檔案</span></LinkTo>
55
+ <LinkTo kind="Design System/Components/FileItem/展示" name="已上傳"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">工單 / 訊息附件的已上傳靜態檔案</span></LinkTo>
56
56
  </li>
57
57
  <li>
58
- <LinkTo kind="Design System/Components/FileItem/展示" name="緊湊 混合"><span className="text-primary hover:underline font-medium cursor-pointer">同一批次混合上傳中 / 完成 / 失敗狀態的清單</span></LinkTo>
58
+ <LinkTo kind="Design System/Components/FileItem/展示" name="緊湊 混合"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">同一批次混合上傳中 / 完成 / 失敗狀態的清單</span></LinkTo>
59
59
  </li>
60
60
  </ul>
61
61
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見 <code>Vs*Rule</code> stories)。</p>
@@ -64,16 +64,16 @@ export const UsageGuidance: Story = {
64
64
  <p>適合 FileUpload 的真實業務場景(點擊跳轉「展示」頁範例):</p>
65
65
  <ul className="space-y-1">
66
66
  <li>
67
- <LinkTo kind="Design System/Components/FileUpload/展示" name="單檔上傳"><span className="text-primary hover:underline font-medium cursor-pointer">履歷 / 大頭貼等單檔上傳</span></LinkTo>
67
+ <LinkTo kind="Design System/Components/FileUpload/展示" name="單檔上傳"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">履歷 / 大頭貼等單檔上傳</span></LinkTo>
68
68
  </li>
69
69
  <li>
70
- <LinkTo kind="Design System/Components/FileUpload/展示" name="批次上傳"><span className="text-primary hover:underline font-medium cursor-pointer">相簿照片 / 多檔附件批次上傳</span></LinkTo>
70
+ <LinkTo kind="Design System/Components/FileUpload/展示" name="批次上傳"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">相簿照片 / 多檔附件批次上傳</span></LinkTo>
71
71
  </li>
72
72
  <li>
73
- <LinkTo kind="Design System/Components/FileUpload/展示" name="內建 files 屬性"><span className="text-primary hover:underline font-medium cursor-pointer">上傳後直接顯示進度清單(內建 files 屬性)</span></LinkTo>
73
+ <LinkTo kind="Design System/Components/FileUpload/展示" name="內建 files 屬性"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">上傳後直接顯示進度清單(內建 files 屬性)</span></LinkTo>
74
74
  </li>
75
75
  <li>
76
- <LinkTo kind="Design System/Components/FileUpload/展示" name="自訂內容"><span className="text-primary hover:underline font-medium cursor-pointer">品牌化上傳區 — 自訂 dropzone 內容</span></LinkTo>
76
+ <LinkTo kind="Design System/Components/FileUpload/展示" name="自訂內容"><span className="text-primary hover:text-primary-hover font-medium cursor-pointer">品牌化上傳區 — 自訂 dropzone 內容</span></LinkTo>
77
77
  </li>
78
78
  </ul>
79
79
  <p className="text-fg-muted mt-3">判斷不確定時:對照 spec.md「何時用 / 何時不用」段;若仍不符,改用近親元件(見 <code>Vs*Rule</code> stories)。</p>