@qijenchen/design-system 0.1.0-beta.74 → 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 (203) 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/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
  6. package/dist/components/BulkActionBar/bulk-action-bar.js +1 -1
  7. package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
  8. package/dist/components/Button/button.d.ts.map +1 -1
  9. package/dist/components/Button/button.js.map +1 -1
  10. package/dist/components/Chart/chart.d.ts +1 -1
  11. package/dist/components/Chart/chart.js.map +1 -1
  12. package/dist/components/Checkbox/checkbox.d.ts +1 -1
  13. package/dist/components/Checkbox/checkbox.js +1 -1
  14. package/dist/components/Checkbox/checkbox.js.map +1 -1
  15. package/dist/components/Combobox/combobox.d.ts +1 -1
  16. package/dist/components/Combobox/combobox.d.ts.map +1 -1
  17. package/dist/components/Combobox/combobox.js +13 -10
  18. package/dist/components/Combobox/combobox.js.map +1 -1
  19. package/dist/components/Command/command.d.ts +1 -1
  20. package/dist/components/Command/command.js +3 -3
  21. package/dist/components/Command/command.js.map +1 -1
  22. package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
  23. package/dist/components/DataTable/cell-registry.js +2 -2
  24. package/dist/components/DataTable/cell-registry.js.map +1 -1
  25. package/dist/components/DataTable/data-table.d.ts +27 -6
  26. package/dist/components/DataTable/data-table.d.ts.map +1 -1
  27. package/dist/components/DataTable/data-table.js +57 -34
  28. package/dist/components/DataTable/data-table.js.map +1 -1
  29. package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
  30. package/dist/components/DatePicker/date-picker.js +2 -2
  31. package/dist/components/DatePicker/date-picker.js.map +1 -1
  32. package/dist/components/DescriptionList/description-list.d.ts +1 -1
  33. package/dist/components/DescriptionList/description-list.js +2 -2
  34. package/dist/components/DescriptionList/description-list.js.map +1 -1
  35. package/dist/components/Empty/empty.d.ts +2 -0
  36. package/dist/components/Empty/empty.d.ts.map +1 -1
  37. package/dist/components/Empty/empty.js.map +1 -1
  38. package/dist/components/Field/field-wrapper.js +4 -4
  39. package/dist/components/Field/field-wrapper.js.map +1 -1
  40. package/dist/components/OverflowIndicator/overflow-indicator.d.ts +1 -1
  41. package/dist/components/OverflowIndicator/overflow-indicator.js +2 -2
  42. package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
  43. package/dist/components/PeoplePicker/people-picker.js +2 -2
  44. package/dist/components/PeoplePicker/people-picker.js.map +1 -1
  45. package/dist/components/ProfileCard/profile-card.d.ts +1 -1
  46. package/dist/components/ProfileCard/profile-card.js +2 -1
  47. package/dist/components/ProfileCard/profile-card.js.map +1 -1
  48. package/dist/components/Rating/rating.js.map +1 -1
  49. package/dist/components/Select/select.js +4 -4
  50. package/dist/components/Select/select.js.map +1 -1
  51. package/dist/components/Textarea/textarea.d.ts +1 -1
  52. package/dist/components/Textarea/textarea.js +2 -2
  53. package/dist/components/Textarea/textarea.js.map +1 -1
  54. package/dist/components/TimePicker/time-picker.d.ts.map +1 -1
  55. package/dist/components/TimePicker/time-picker.js +14 -23
  56. package/dist/components/TimePicker/time-picker.js.map +1 -1
  57. package/dist/components/TreeView/tree-view.d.ts +1 -1
  58. package/dist/components/TreeView/tree-view.js +1 -1
  59. package/dist/components/TreeView/tree-view.js.map +1 -1
  60. package/ds-canonical/fork/governance.lock +1 -1
  61. package/ds-canonical/fork/preamble.md +2 -2
  62. package/ds-canonical/hooks/check_field_controls_contracts.sh +30 -3
  63. package/ds-canonical/hooks/check_story_invariants.sh +26 -0
  64. package/ds-canonical/hooks/tests/test_check_story_invariants.sh +30 -0
  65. package/ds-canonical/references/props-naming.md +15 -1
  66. package/ds-canonical/rules/ui-development.md +2 -2
  67. package/llms-full.txt +7 -2
  68. package/llms.txt +3 -3
  69. package/package.json +1 -1
  70. package/src/components/Accordion/accordion.principles.stories.tsx +3 -3
  71. package/src/components/Alert/alert.anatomy.stories.tsx +4 -4
  72. package/src/components/Alert/alert.principles.stories.tsx +5 -5
  73. package/src/components/AppShell/app-shell.principles.stories.tsx +6 -6
  74. package/src/components/AppShell/app-shell.spec.md +4 -4
  75. package/src/components/AppShell/app-shell.tsx +2 -2
  76. package/src/components/AspectRatio/aspect-ratio.principles.stories.tsx +1 -1
  77. package/src/components/Avatar/avatar.principles.stories.tsx +3 -3
  78. package/src/components/Avatar/avatar.tsx +1 -1
  79. package/src/components/Badge/badge.principles.stories.tsx +3 -3
  80. package/src/components/Breadcrumb/breadcrumb.principles.stories.tsx +3 -3
  81. package/src/components/Breadcrumb/breadcrumb.spec.md +11 -1
  82. package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
  83. package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +3 -3
  84. package/src/components/BulkActionBar/bulk-action-bar.spec.md +4 -2
  85. package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +2 -2
  86. package/src/components/BulkActionBar/bulk-action-bar.tsx +3 -2
  87. package/src/components/Button/button.principles.stories.tsx +3 -3
  88. package/src/components/Button/button.tsx +0 -10
  89. package/src/components/Calendar/calendar.anatomy.stories.tsx +1 -1
  90. package/src/components/Calendar/calendar.principles.stories.tsx +3 -3
  91. package/src/components/Carousel/carousel.principles.stories.tsx +2 -2
  92. package/src/components/Chart/chart.principles.stories.tsx +4 -4
  93. package/src/components/Chart/chart.tsx +1 -1
  94. package/src/components/Checkbox/checkbox.principles.stories.tsx +2 -2
  95. package/src/components/Checkbox/checkbox.tsx +1 -1
  96. package/src/components/Chip/chip.principles.stories.tsx +3 -3
  97. package/src/components/Coachmark/coachmark.anatomy.stories.tsx +1 -1
  98. package/src/components/Coachmark/coachmark.principles.stories.tsx +3 -3
  99. package/src/components/Coachmark/coachmark.spec.md +2 -2
  100. package/src/components/Combobox/combobox.anatomy.stories.tsx +14 -14
  101. package/src/components/Combobox/combobox.principles.stories.tsx +6 -6
  102. package/src/components/Combobox/combobox.spec.md +1 -1
  103. package/src/components/Combobox/combobox.tsx +25 -14
  104. package/src/components/Command/command.anatomy.stories.tsx +2 -0
  105. package/src/components/Command/command.principles.stories.tsx +7 -7
  106. package/src/components/Command/command.tsx +2 -2
  107. package/src/components/DataTable/cell-registry.tsx +6 -2
  108. package/src/components/DataTable/data-table-filter-panel.tsx +3 -3
  109. package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
  110. package/src/components/DataTable/data-table.css +1 -1
  111. package/src/components/DataTable/data-table.principles.stories.tsx +3 -3
  112. package/src/components/DataTable/data-table.spec.md +25 -17
  113. package/src/components/DataTable/data-table.stories.tsx +29 -25
  114. package/src/components/DataTable/data-table.tsx +92 -44
  115. package/src/components/DateGrid/date-grid.anatomy.stories.tsx +1 -1
  116. package/src/components/DateGrid/date-grid.principles.stories.tsx +4 -4
  117. package/src/components/DateGrid/date-grid.spec.md +1 -1
  118. package/src/components/DatePicker/date-picker.anatomy.stories.tsx +15 -11
  119. package/src/components/DatePicker/date-picker.principles.stories.tsx +5 -5
  120. package/src/components/DatePicker/date-picker.spec.md +1 -1
  121. package/src/components/DatePicker/date-picker.tsx +9 -6
  122. package/src/components/DescriptionList/description-list.principles.stories.tsx +5 -5
  123. package/src/components/DescriptionList/description-list.tsx +1 -1
  124. package/src/components/Dialog/dialog.anatomy.stories.tsx +1 -1
  125. package/src/components/Dialog/dialog.principles.stories.tsx +4 -4
  126. package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +1 -1
  127. package/src/components/DropdownMenu/dropdown-menu.principles.stories.tsx +5 -5
  128. package/src/components/DropdownMenu/dropdown-menu.spec.md +1 -1
  129. package/src/components/Empty/empty.principles.stories.tsx +2 -2
  130. package/src/components/Empty/empty.tsx +2 -0
  131. package/src/components/Field/field-controls.spec.md +9 -6
  132. package/src/components/Field/field-wrapper.tsx +4 -4
  133. package/src/components/Field/field.principles.stories.tsx +4 -4
  134. package/src/components/FileItem/file-item.principles.stories.tsx +6 -5
  135. package/src/components/FileUpload/file-upload.principles.stories.tsx +6 -6
  136. package/src/components/FileUpload/file-upload.spec.md +1 -1
  137. package/src/components/FileViewer/file-viewer.principles.stories.tsx +5 -5
  138. package/src/components/HoverCard/hover-card.principles.stories.tsx +6 -6
  139. package/src/components/Input/input.anatomy.stories.tsx +3 -3
  140. package/src/components/Input/input.principles.stories.tsx +4 -4
  141. package/src/components/LinkInput/link-input.anatomy.stories.tsx +3 -3
  142. package/src/components/LinkInput/link-input.principles.stories.tsx +5 -5
  143. package/src/components/Menu/menu-item.principles.stories.tsx +7 -7
  144. package/src/components/Notice/notice.anatomy.stories.tsx +1 -1
  145. package/src/components/Notice/notice.principles.stories.tsx +7 -7
  146. package/src/components/NumberInput/number-input.anatomy.stories.tsx +8 -7
  147. package/src/components/NumberInput/number-input.principles.stories.tsx +4 -4
  148. package/src/components/NumberInput/number-input.spec.md +1 -1
  149. package/src/components/OverflowIndicator/overflow-indicator.principles.stories.tsx +5 -5
  150. package/src/components/OverflowIndicator/overflow-indicator.tsx +1 -1
  151. package/src/components/PeoplePicker/people-picker.principles.stories.tsx +3 -3
  152. package/src/components/PeoplePicker/people-picker.spec.md +3 -3
  153. package/src/components/PeoplePicker/people-picker.tsx +6 -6
  154. package/src/components/Popover/popover.principles.stories.tsx +5 -5
  155. package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
  156. package/src/components/ProfileCard/profile-card.principles.stories.tsx +1 -1
  157. package/src/components/ProfileCard/profile-card.tsx +1 -1
  158. package/src/components/ProgressBar/progress-bar.principles.stories.tsx +2 -2
  159. package/src/components/ProgressBar/progress-bar.spec.md +1 -1
  160. package/src/components/RadioGroup/radio-group.principles.stories.tsx +2 -2
  161. package/src/components/Rating/rating.anatomy.stories.tsx +2 -2
  162. package/src/components/Rating/rating.principles.stories.tsx +3 -3
  163. package/src/components/Rating/rating.spec.md +1 -1
  164. package/src/components/Rating/rating.tsx +1 -1
  165. package/src/components/ScrollArea/scroll-area.principles.stories.tsx +4 -4
  166. package/src/components/Select/select.anatomy.stories.tsx +18 -18
  167. package/src/components/Select/select.principles.stories.tsx +5 -5
  168. package/src/components/Select/select.spec.md +1 -1
  169. package/src/components/Select/select.tsx +7 -7
  170. package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +1 -1
  171. package/src/components/SelectMenu/select-menu.principles.stories.tsx +8 -8
  172. package/src/components/SelectionControl/selection-item.principles.stories.tsx +7 -7
  173. package/src/components/Separator/separator.principles.stories.tsx +4 -4
  174. package/src/components/Sheet/sheet.principles.stories.tsx +2 -2
  175. package/src/components/Sidebar/sidebar.principles.stories.tsx +4 -4
  176. package/src/components/Sidebar/sidebar.spec.md +2 -2
  177. package/src/components/Skeleton/skeleton.principles.stories.tsx +5 -5
  178. package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
  179. package/src/components/Slider/slider.principles.stories.tsx +3 -3
  180. package/src/components/Steps/steps.principles.stories.tsx +4 -4
  181. package/src/components/Steps/steps.spec.md +2 -2
  182. package/src/components/Switch/switch.principles.stories.tsx +1 -1
  183. package/src/components/Tabs/tabs.principles.stories.tsx +3 -3
  184. package/src/components/Tabs/tabs.spec.md +1 -1
  185. package/src/components/Tag/tag.principles.stories.tsx +3 -3
  186. package/src/components/Textarea/textarea.principles.stories.tsx +2 -2
  187. package/src/components/Textarea/textarea.tsx +3 -3
  188. package/src/components/TimePicker/time-picker.principles.stories.tsx +5 -5
  189. package/src/components/TimePicker/time-picker.spec.md +1 -1
  190. package/src/components/TimePicker/time-picker.tsx +11 -12
  191. package/src/components/Toast/toast.principles.stories.tsx +2 -2
  192. package/src/components/Tooltip/tooltip.principles.stories.tsx +3 -3
  193. package/src/components/TreeView/tree-view.principles.stories.tsx +5 -5
  194. package/src/components/TreeView/tree-view.stories.tsx +1 -1
  195. package/src/components/TreeView/tree-view.tsx +1 -1
  196. package/src/patterns/element-anatomy/item-anatomy.spec.md +1 -1
  197. package/src/patterns/element-anatomy/item-anatomy.stories.tsx +1 -1
  198. package/src/patterns/overlay-surface/overlay-surface.spec.md +1 -0
  199. package/src/patterns/resize-handle/resize-handle.spec.md +1 -1
  200. package/src/tokens/color/color.spec.md +2 -0
  201. package/src/tokens/color/semantic.css +1 -1
  202. package/src/tokens/uiSize/uiSize.css +5 -0
  203. package/src/tokens/uiSize/uiSize.spec.md +17 -3
@@ -263,18 +263,24 @@ DataTable 的 row selection layer。提供 controlled/uncontrolled state + 視
263
263
 
264
264
  **世界級對照**:Material DataGrid `rowSelectionModel` / Polaris IndexTable `selectedResources` / Linear / Notion 全 controlled-first + uncontrolled fallback。AG Grid 的 imperative `gridRef.api` 不採(違背 React idiom + 既有 Field/Switch/Checkbox controllable 慣例)。 <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
265
265
 
266
- ### 一、State 模式
266
+ ### 一、State 模式(discriminated union,2026-06-22 支援反向選取 inverted)
267
267
 
268
268
  ```ts
269
- selection?: string[] // controlled
270
- defaultSelection?: string[] // uncontrolled
271
- onSelectionChange?: (ids: string[]) => void
272
- selectable?: boolean | 'single' | 'multi' // default 'multi'
269
+ // 選取模型:include(列舉)/ all(反向,全集 − excluded)
270
+ type DataTableSelection =
271
+ | { mode: 'include'; ids: string[] } // 只選 ids 列(預設)
272
+ | { mode: 'all'; excluded: string[] } // 全資料集(filter 後)選取,扣掉 excluded
273
+
274
+ selection?: string[] | DataTableSelection // controlled;傳 string[] = include shorthand(向後相容)
275
+ defaultSelection?: string[] | DataTableSelection // uncontrolled
276
+ onSelectionChange?: (next: DataTableSelection) => void // 一律 emit union
277
+ totalCount?: number // 全集筆數 M(server-side / filter 後);all 模式計數用
278
+ selectable?: boolean | 'single' | 'multi' // default 'multi';single 永遠 include
273
279
  isRowSelectable?: (row: TData) => boolean
274
280
  preserveSelectionOnFilter?: boolean // default false
275
281
  ```
276
282
 
277
- 對齊 `useControllableState` idiom(Field / Switch / Checkbox 已用)。
283
+ 對齊 `useControllableState` idiom(Field / Switch / Checkbox 已用)+ MUI X DataGrid v8 `rowSelectionModel { type:'include'|'exclude', ids }` / AG Grid `selectAll + toggledNodes` 反向選取共識。**計數(consumer)**:`mode==='all' ? totalCount − excluded.length : ids.length`。**向後相容**:傳 `string[]` 自動正規化為 `{ mode:'include' }`;但 `onSelectionChange` 一律 emit union(consumer 讀取端需處理兩 mode)
278
284
 
279
285
  ### 二、Checkbox column
280
286
 
@@ -283,15 +289,16 @@ preserveSelectionOnFilter?: boolean // default false
283
289
  - **顯示時機**:**always visible**(對齊 Linear 2024 / Polaris / Material consensus,不允 hover-show) <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
284
290
  - **Header tri-state**:none / indeterminate / all,使用既有 Checkbox `indeterminate` prop
285
291
 
286
- ### 三、全選邏輯(2-step pattern)
292
+ ### 三、全選邏輯(2-step pattern + 反向選取 inverted)
287
293
 
288
- 對齊 ref 圖 + Linear / Gmail / Notion canonical: <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
294
+ 對齊 ref 圖 + Linear / Gmail / Notion 2-step + MUI X v8 / AG Grid inverted: <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
289
295
 
290
- 1. Header checkbox click(none → all)→ 選**目前可見** rows(filter 後 visible-only)
291
- 2. 全頁可見已選 → BulkActionBar 顯示 hint:「已選取本頁 N 個。**點此選取全部 M 個**」
292
- 3. 點 hint → 擴 dataset 全選,hint 改:「已選取全部 M 個。**清除選取項目**」
296
+ 1. Header checkbox click(none → all)→ 選**目前可見** rows(filter 後 visible-only)= `{ mode:'include', ids:[…visible] }`
297
+ 2. 全頁可見已選 → BulkActionBar hint:「已選取本頁 N 個。**點此選取全部 M 個**」
298
+ 3. 點 hint → consumer `setSelection({ mode:'all', excluded:[] })` 擴 dataset 全選,hint 改:「已選取全部 M 個。**清除選取項目**」
299
+ 4. **反向選取(inverted)**:all 模式下取消勾選某幾筆 → 加進 `excluded`(`選取 = 全集 − excluded`);再勾回 → 移出 `excluded`。對 10k 筆只載 50 筆**不需列舉其餘 ID**,任意 toggle 順序封閉、O(1)。count = `totalCount − excluded.length`,hint 顯示「已選取全部 M 個(排除 K 個)」。
293
300
 
294
- 不直接擴 dataset(避免誤觸大量資料)
301
+ 不**一鍵**直接擴 dataset(避免誤觸大量資料,必先 2-step);擴選後的反向扣除由 inverted 模型自動處理。
295
302
 
296
303
  ### 四、互動
297
304
 
@@ -308,9 +315,10 @@ preserveSelectionOnFilter?: boolean // default false
308
315
 
309
316
  ### 六、Selection × filter / sort 互動
310
317
 
311
- - **filter 套用 → filtered-out 的 selected rows 預設清掉**(對齊 Material / AG Grid / Polaris / GitHub / Gmail consensus) <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
312
- - **opt-in `preserveSelectionOnFilter={true}`** productivity scope(Linear / Airtable 用法),保留 hidden selected,BulkActionBar 顯示「{visible} selected ({hidden} hidden by filter)
313
- - sort 套用selection 全保留(sort 不影響可見性)
318
+ - **`include` 模式**:filter 套用 → filtered-out 的 selected rows 預設清掉(對齊 Material / AG Grid / Polaris / GitHub / Gmail consensus) <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
319
+ - **`all`(反向)模式**:語意 = 「全部**符合當前 filter**的列 excluded」→ filter 變動時 selection set filter **自然重算**(M 跟著變),`excluded` 保留不清( filter 掉的 excluded 列無害,回到該 filter 時仍排除);**不**套用上面的 include-mode 清除。consumer 計數用更新後的 `totalCount`。
320
+ - **opt-in `preserveSelectionOnFilter={true}`**(僅 include 模式) productivity scope(Linear / Airtable 用法),保留 hidden selected,BulkActionBar 顯示「{visible} selected ({hidden} hidden by filter)」
321
+ - sort 套用 → selection 全保留(sort 不影響可見性,兩 mode 同)
314
322
 
315
323
  ### 七、BulkActionBar 整合(inline composition canonical)
316
324
 
@@ -378,12 +386,12 @@ ValueShape ↔ DS picker 對照(canonical 2026-05-02):
378
386
 
379
387
  ### 四、UI canonical
380
388
 
381
- - 第 1 row conjunction 是靜態 `Where` label(`px-3` 對齊下方 Field value 起點 = 12px)
389
+ - 第 1 row conjunction 是靜態 `Where` label(`px-[var(--field-px)]` 對齊下方 Field value 起點 = 12px)
382
390
  - field 未選 → operator + value picker disabled;同 group 共用 conjunction(toggle 任一 → flip 整 group)
383
391
  - **空狀態**:無 condition → 只顯 inline `+ 加篩選` CTA(對齊 Notion / Airtable / Linear,**禁止** auto-create 空 row) <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
384
392
  - **CTA 位置**:緊貼最後一條 row(text variant 輕量,**廢 SurfaceFooter**),條件與「加入」屬同一語境
385
393
  - **Trash / 刪除**:row 是 form-control row → text Button(non Inline Action,違 item-anatomy canonical)
386
- - **And/Or Select** `minRows={2}`(2 選項顯式縮 menu 高度);**Where padding** `px-3` align Field
394
+ - **And/Or Select** `minRows={2}`(2 選項顯式縮 menu 高度);**Where padding** `px-[var(--field-px)]` align Field
387
395
  - Header refresh icon:`value !== defaultValue` 顯;ButtonDivider 串接 close X(對齊欄位顯示 chrome canonical)
388
396
  - **Relative date 群組**:`DATE_RELATIVE_GROUPS` Past / Current / Future,走 `<Select groups>`
389
397
  - Trigger button checked(`aria-pressed`):`value` 有 ≥ 1 active condition → on(語意:資料被篩,獨立於 refresh)
@@ -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>
@@ -39,7 +39,7 @@ const PARTS: Record<PartKey, PartSpec> = {
39
39
  caption: { label: '月份標題', bg: 'transparent', text: '--foreground', border: 'transparent', extra: 'text-body font-medium' },
40
40
  nav: { label: 'Nav 按鈕', bg: 'transparent', text: '--foreground', border: 'transparent', extra: 'Button variant=text size=xs iconOnly · hover 藍圈' },
41
41
  weekday: { label: '星期標頭', bg: 'transparent', text: '--foreground', border: 'transparent', extra: 'text-body font-medium · h-7' },
42
- day: { label: '日格(default)', bg: 'transparent', text: '--foreground', border: 'transparent', extra: 'h-field-sm w-field-sm rounded-full' },
42
+ day: { label: '日格(default)', bg: 'transparent', text: '--foreground', border: 'transparent', extra: 'h-field-sm w-[var(--field-height-sm)] rounded-full' },
43
43
  daySelected: { label: 'Selected', bg: '--primary', text: 'white', border: 'transparent' },
44
44
  dayToday: { label: 'Today(未選)', bg: 'transparent', text: '--foreground', border: 'transparent', extra: '數字下方藍色底線(underline bar)' },
45
45
  dayHover: { label: 'Hover', bg: 'transparent', text: '--foreground', border: '--primary', extra: 'hover 藍圈 1.5px(無填底)' },
@@ -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>
@@ -82,7 +82,7 @@ DateGrid 是 internal primitive(見「定位」),一般 consumer 經 `DatePicker
82
82
 
83
83
  因此用 `[&[data-range-middle=true]]:xxx` 這種 attribute selector **根本不會生效**(舊版做法錯誤)。
84
84
 
85
- **正解**:把 state 樣式放進 `classNames[state]` 物件,v9 的 `getClassNamesForModifiers` 會在對應 modifier 為 true 時把該 key 的 class 附加到 Day CELL。範例:`classNames.range_middle: 'bg-[var(--color-neutral-2)] [&>button]:!bg-transparent'`。
85
+ **正解**:把 state 樣式放進 `classNames[state]` 物件,v9 的 `getClassNamesForModifiers` 會在對應 modifier 為 true 時把該 key 的 class 附加到 Day CELL。範例:`classNames.range_middle: "before:content-[''] before:absolute before:inset-y-0 before:-inset-x-[2px] before:bg-neutral-selected [&>button]:!bg-transparent"`(用 `before:` pseudo + semantic `--neutral-selected` token,對齊下方 Range track canonical 與 tsx)。
86
86
 
87
87
  `[&>button]:xxx` 從 cell 向內選子 button 用於 button-level 樣式(selected / disabled 的藍底白圓等)。
88
88
 
@@ -33,7 +33,7 @@ const COLOR_MAP: Record<ModeKey, Partial<Record<StateKey, ColorSpec>>> = {
33
33
  disabled: { bg: '--bg-disabled', text: '--fg-disabled', border: 'transparent', icon: '--fg-disabled' },
34
34
  },
35
35
  readonly: {
36
- default: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '--fg-muted' },
36
+ default: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '' },
37
37
  },
38
38
  disabled: {
39
39
  default: { bg: '--bg-disabled', text: '--fg-disabled', border: 'transparent', icon: '--fg-disabled' },
@@ -50,9 +50,9 @@ interface SizeSpec {
50
50
  }
51
51
 
52
52
  const SIZE_SPECS: Record<SizeKey, SizeSpec> = {
53
- sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-3', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, clearHover: 18 },
54
- md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-3', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, clearHover: 18 },
55
- lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-3', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body-lg', font: '16px', icon: 20, clearHover: 22 },
53
+ sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, clearHover: 18 },
54
+ md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, clearHover: 18 },
55
+ lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body-lg', font: '16px', icon: 20, clearHover: 22 },
56
56
  }
57
57
 
58
58
  const MODE_DESC: Record<ModeKey, string> = {
@@ -172,7 +172,7 @@ export const Overview = {
172
172
  <div className="flex flex-col gap-4">
173
173
  <div className="flex flex-col gap-1">
174
174
  <H3>結構(Anatomy)</H3>
175
- <Desc>edit 模式:可點擊的觸發列顯示格式化日期文字 + 日曆圖示(固定右側),點任意位置都會展開日期面板。clearable 有值時額外顯示 X 清除按鈕。readonly / disabled 模式:格式化文字 + Calendar icon(類型身份 indicator;disabled fg-disabled),無 X。</Desc>
175
+ <Desc>edit 模式:可點擊的觸發列顯示格式化日期文字 + 日曆圖示(固定右側),點任意位置都會展開日期面板。clearable 有值時額外顯示 X 清除按鈕。readonly 模式:純格式化文字,無 Calendar icon、無 X。disabled 模式:格式化文字 + Calendar icon(類型身份 indicator,切 fg-disabled),無 X。</Desc>
176
176
  </div>
177
177
  <div className="flex gap-8">
178
178
  {/* Edit layout */}
@@ -346,7 +346,7 @@ const InspectorInner = () => {
346
346
  { c: Z.pad, l: '左右內距' },
347
347
  ...(isEdit ? [{ c: Z.input, l: 'trigger text' }] : [{ c: Z.input, l: 'formatted text' }]),
348
348
  ...(showClear ? [{ c: Z.action, l: 'X clear' }] : []),
349
- { c: Z.icon, l: 'Calendar' },
349
+ ...(mode !== 'readonly' ? [{ c: Z.icon, l: 'Calendar' }] : []),
350
350
  ].map(({ c, l }) => (
351
351
  <span key={l} className="inline-flex items-center gap-1">
352
352
  <span className="w-2.5 h-2.5 rounded-md" style={{ background: c.bg, border: `1px dashed ${c.border}` }} />
@@ -364,8 +364,12 @@ const InspectorInner = () => {
364
364
  <BpZone w={44} color={Z.action} label={`${s.icon}px`} sub="clear" />
365
365
  </>
366
366
  )}
367
- <BpZone w={32} color={Z.gap} label={s.gapToken} sub={`${s.gap}px`} />
368
- <BpZone w={44} color={Z.icon} label={`${s.icon}px`} sub="Calendar" />
367
+ {mode !== 'readonly' && (
368
+ <>
369
+ <BpZone w={32} color={Z.gap} label={s.gapToken} sub={`${s.gap}px`} />
370
+ <BpZone w={44} color={Z.icon} label={`${s.icon}px`} sub="Calendar" />
371
+ </>
372
+ )}
369
373
  <BpZone w={44} color={Z.pad} label={s.pxToken} sub={`${s.px}px`} />
370
374
  </div>
371
375
  <div className="ml-3 flex items-center" style={{ height: 52 }}>
@@ -393,7 +397,7 @@ const InspectorInner = () => {
393
397
  <PropRow label="Fill"><TokenValue value={colors.bg} /></PropRow>
394
398
  <PropRow label="Text"><TokenValue value={colors.text} /></PropRow>
395
399
  <PropRow label="Border"><TokenValue value={colors.border} /></PropRow>
396
- {isEdit && (
400
+ {mode !== 'readonly' && (
397
401
  <PropRow label="Calendar">
398
402
  <TokenValue value={colors.icon} />
399
403
  </PropRow>
@@ -424,7 +428,7 @@ const InspectorInner = () => {
424
428
  <PropRow label="高度" dot={Z.dim.text}><TkVal token={s.heightToken} value={s.height} /></PropRow>
425
429
  <PropRow label="左右內距" dot={Z.pad.text}><TkVal token={s.pxToken} value={`${s.px}px`} /></PropRow>
426
430
  <PropRow label="元素間距" dot={Z.gap.text}><TkVal token={s.gapToken} value={`${s.gap}px`} /></PropRow>
427
- {isEdit && (
431
+ {mode !== 'readonly' && (
428
432
  <PropRow label="Calendar" dot={Z.icon.text}>{s.icon}px</PropRow>
429
433
  )}
430
434
  {showClear && (
@@ -686,7 +690,7 @@ export const StateBehavior = {
686
690
  <span className="text-fg-muted text-caption">→</span>
687
691
  <DatePicker mode="disabled" value="2026-04-02" className="w-56" />
688
692
  </div>
689
- <span className="text-[11px] text-fg-muted">左:edit(有 X)→ 中:readonly(無 X,保留 Calendar)→ 右:disabled(無 XCalendar 與文字切 fg-disabled)</span>
693
+ <span className="text-[11px] text-fg-muted">左:edit(有 X)→ 中:readonly(無 X、無 Calendar,純文字)→ 右:disabled(無 X,保留 Calendar,與文字切 fg-disabled)</span>
690
694
  </div>
691
695
  </div>
692
696
 
@@ -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>