@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.
- package/CLAUDE.md +1 -1
- package/dist/components/AppShell/app-shell.d.ts +2 -2
- package/dist/components/AppShell/app-shell.js.map +1 -1
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js.map +1 -1
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Chart/chart.d.ts +1 -1
- package/dist/components/Chart/chart.js.map +1 -1
- package/dist/components/Checkbox/checkbox.d.ts +1 -1
- package/dist/components/Checkbox/checkbox.js +1 -1
- package/dist/components/Checkbox/checkbox.js.map +1 -1
- package/dist/components/Combobox/combobox.d.ts +1 -1
- package/dist/components/Combobox/combobox.d.ts.map +1 -1
- package/dist/components/Combobox/combobox.js +13 -10
- package/dist/components/Combobox/combobox.js.map +1 -1
- package/dist/components/Command/command.d.ts +1 -1
- package/dist/components/Command/command.js +3 -3
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
- package/dist/components/DataTable/cell-registry.js +2 -2
- package/dist/components/DataTable/cell-registry.js.map +1 -1
- package/dist/components/DataTable/data-table.d.ts +27 -6
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +57 -34
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +2 -2
- package/dist/components/DatePicker/date-picker.js.map +1 -1
- package/dist/components/DescriptionList/description-list.d.ts +1 -1
- package/dist/components/DescriptionList/description-list.js +2 -2
- package/dist/components/DescriptionList/description-list.js.map +1 -1
- package/dist/components/Empty/empty.d.ts +2 -0
- package/dist/components/Empty/empty.d.ts.map +1 -1
- package/dist/components/Empty/empty.js.map +1 -1
- package/dist/components/Field/field-wrapper.js +4 -4
- package/dist/components/Field/field-wrapper.js.map +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.js +2 -2
- package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js +2 -2
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/ProfileCard/profile-card.d.ts +1 -1
- package/dist/components/ProfileCard/profile-card.js +2 -1
- package/dist/components/ProfileCard/profile-card.js.map +1 -1
- package/dist/components/Rating/rating.js.map +1 -1
- package/dist/components/Select/select.js +4 -4
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/Textarea/textarea.d.ts +1 -1
- package/dist/components/Textarea/textarea.js +2 -2
- package/dist/components/Textarea/textarea.js.map +1 -1
- package/dist/components/TimePicker/time-picker.d.ts.map +1 -1
- package/dist/components/TimePicker/time-picker.js +14 -23
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts +1 -1
- package/dist/components/TreeView/tree-view.js +1 -1
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/ds-canonical/fork/governance.lock +1 -1
- package/ds-canonical/fork/preamble.md +2 -2
- package/ds-canonical/hooks/check_field_controls_contracts.sh +30 -3
- package/ds-canonical/hooks/check_story_invariants.sh +26 -0
- package/ds-canonical/hooks/tests/test_check_story_invariants.sh +30 -0
- package/ds-canonical/references/props-naming.md +15 -1
- package/ds-canonical/rules/ui-development.md +2 -2
- package/llms-full.txt +7 -2
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/components/Accordion/accordion.principles.stories.tsx +3 -3
- package/src/components/Alert/alert.anatomy.stories.tsx +4 -4
- package/src/components/Alert/alert.principles.stories.tsx +5 -5
- package/src/components/AppShell/app-shell.principles.stories.tsx +6 -6
- package/src/components/AppShell/app-shell.spec.md +4 -4
- package/src/components/AppShell/app-shell.tsx +2 -2
- package/src/components/AspectRatio/aspect-ratio.principles.stories.tsx +1 -1
- package/src/components/Avatar/avatar.principles.stories.tsx +3 -3
- package/src/components/Avatar/avatar.tsx +1 -1
- package/src/components/Badge/badge.principles.stories.tsx +3 -3
- package/src/components/Breadcrumb/breadcrumb.principles.stories.tsx +3 -3
- package/src/components/Breadcrumb/breadcrumb.spec.md +11 -1
- package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +3 -3
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +4 -2
- package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +2 -2
- package/src/components/BulkActionBar/bulk-action-bar.tsx +3 -2
- package/src/components/Button/button.principles.stories.tsx +3 -3
- package/src/components/Button/button.tsx +0 -10
- package/src/components/Calendar/calendar.anatomy.stories.tsx +1 -1
- package/src/components/Calendar/calendar.principles.stories.tsx +3 -3
- package/src/components/Carousel/carousel.principles.stories.tsx +2 -2
- package/src/components/Chart/chart.principles.stories.tsx +4 -4
- package/src/components/Chart/chart.tsx +1 -1
- package/src/components/Checkbox/checkbox.principles.stories.tsx +2 -2
- package/src/components/Checkbox/checkbox.tsx +1 -1
- package/src/components/Chip/chip.principles.stories.tsx +3 -3
- package/src/components/Coachmark/coachmark.anatomy.stories.tsx +1 -1
- package/src/components/Coachmark/coachmark.principles.stories.tsx +3 -3
- package/src/components/Coachmark/coachmark.spec.md +2 -2
- package/src/components/Combobox/combobox.anatomy.stories.tsx +14 -14
- package/src/components/Combobox/combobox.principles.stories.tsx +6 -6
- package/src/components/Combobox/combobox.spec.md +1 -1
- package/src/components/Combobox/combobox.tsx +25 -14
- package/src/components/Command/command.anatomy.stories.tsx +2 -0
- package/src/components/Command/command.principles.stories.tsx +7 -7
- package/src/components/Command/command.tsx +2 -2
- package/src/components/DataTable/cell-registry.tsx +6 -2
- package/src/components/DataTable/data-table-filter-panel.tsx +3 -3
- package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
- package/src/components/DataTable/data-table.css +1 -1
- package/src/components/DataTable/data-table.principles.stories.tsx +3 -3
- package/src/components/DataTable/data-table.spec.md +25 -17
- package/src/components/DataTable/data-table.stories.tsx +29 -25
- package/src/components/DataTable/data-table.tsx +92 -44
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +1 -1
- package/src/components/DateGrid/date-grid.principles.stories.tsx +4 -4
- package/src/components/DateGrid/date-grid.spec.md +1 -1
- package/src/components/DatePicker/date-picker.anatomy.stories.tsx +15 -11
- package/src/components/DatePicker/date-picker.principles.stories.tsx +5 -5
- package/src/components/DatePicker/date-picker.spec.md +1 -1
- package/src/components/DatePicker/date-picker.tsx +9 -6
- package/src/components/DescriptionList/description-list.principles.stories.tsx +5 -5
- package/src/components/DescriptionList/description-list.tsx +1 -1
- package/src/components/Dialog/dialog.anatomy.stories.tsx +1 -1
- package/src/components/Dialog/dialog.principles.stories.tsx +4 -4
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +1 -1
- package/src/components/DropdownMenu/dropdown-menu.principles.stories.tsx +5 -5
- package/src/components/DropdownMenu/dropdown-menu.spec.md +1 -1
- package/src/components/Empty/empty.principles.stories.tsx +2 -2
- package/src/components/Empty/empty.tsx +2 -0
- package/src/components/Field/field-controls.spec.md +9 -6
- package/src/components/Field/field-wrapper.tsx +4 -4
- package/src/components/Field/field.principles.stories.tsx +4 -4
- package/src/components/FileItem/file-item.principles.stories.tsx +6 -5
- package/src/components/FileUpload/file-upload.principles.stories.tsx +6 -6
- package/src/components/FileUpload/file-upload.spec.md +1 -1
- package/src/components/FileViewer/file-viewer.principles.stories.tsx +5 -5
- package/src/components/HoverCard/hover-card.principles.stories.tsx +6 -6
- package/src/components/Input/input.anatomy.stories.tsx +3 -3
- package/src/components/Input/input.principles.stories.tsx +4 -4
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +3 -3
- package/src/components/LinkInput/link-input.principles.stories.tsx +5 -5
- package/src/components/Menu/menu-item.principles.stories.tsx +7 -7
- package/src/components/Notice/notice.anatomy.stories.tsx +1 -1
- package/src/components/Notice/notice.principles.stories.tsx +7 -7
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +8 -7
- package/src/components/NumberInput/number-input.principles.stories.tsx +4 -4
- package/src/components/NumberInput/number-input.spec.md +1 -1
- package/src/components/OverflowIndicator/overflow-indicator.principles.stories.tsx +5 -5
- package/src/components/OverflowIndicator/overflow-indicator.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.principles.stories.tsx +3 -3
- package/src/components/PeoplePicker/people-picker.spec.md +3 -3
- package/src/components/PeoplePicker/people-picker.tsx +6 -6
- package/src/components/Popover/popover.principles.stories.tsx +5 -5
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.principles.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.tsx +1 -1
- package/src/components/ProgressBar/progress-bar.principles.stories.tsx +2 -2
- package/src/components/ProgressBar/progress-bar.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.principles.stories.tsx +2 -2
- package/src/components/Rating/rating.anatomy.stories.tsx +2 -2
- package/src/components/Rating/rating.principles.stories.tsx +3 -3
- package/src/components/Rating/rating.spec.md +1 -1
- package/src/components/Rating/rating.tsx +1 -1
- package/src/components/ScrollArea/scroll-area.principles.stories.tsx +4 -4
- package/src/components/Select/select.anatomy.stories.tsx +18 -18
- package/src/components/Select/select.principles.stories.tsx +5 -5
- package/src/components/Select/select.spec.md +1 -1
- package/src/components/Select/select.tsx +7 -7
- package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +1 -1
- package/src/components/SelectMenu/select-menu.principles.stories.tsx +8 -8
- package/src/components/SelectionControl/selection-item.principles.stories.tsx +7 -7
- package/src/components/Separator/separator.principles.stories.tsx +4 -4
- package/src/components/Sheet/sheet.principles.stories.tsx +2 -2
- package/src/components/Sidebar/sidebar.principles.stories.tsx +4 -4
- package/src/components/Sidebar/sidebar.spec.md +2 -2
- package/src/components/Skeleton/skeleton.principles.stories.tsx +5 -5
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Slider/slider.principles.stories.tsx +3 -3
- package/src/components/Steps/steps.principles.stories.tsx +4 -4
- package/src/components/Steps/steps.spec.md +2 -2
- package/src/components/Switch/switch.principles.stories.tsx +1 -1
- package/src/components/Tabs/tabs.principles.stories.tsx +3 -3
- package/src/components/Tabs/tabs.spec.md +1 -1
- package/src/components/Tag/tag.principles.stories.tsx +3 -3
- package/src/components/Textarea/textarea.principles.stories.tsx +2 -2
- package/src/components/Textarea/textarea.tsx +3 -3
- package/src/components/TimePicker/time-picker.principles.stories.tsx +5 -5
- package/src/components/TimePicker/time-picker.spec.md +1 -1
- package/src/components/TimePicker/time-picker.tsx +11 -12
- package/src/components/Toast/toast.principles.stories.tsx +2 -2
- package/src/components/Tooltip/tooltip.principles.stories.tsx +3 -3
- package/src/components/TreeView/tree-view.principles.stories.tsx +5 -5
- package/src/components/TreeView/tree-view.stories.tsx +1 -1
- package/src/components/TreeView/tree-view.tsx +1 -1
- package/src/patterns/element-anatomy/item-anatomy.spec.md +1 -1
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +1 -1
- package/src/patterns/overlay-surface/overlay-surface.spec.md +1 -0
- package/src/patterns/resize-handle/resize-handle.spec.md +1 -1
- package/src/tokens/color/color.spec.md +2 -0
- package/src/tokens/color/semantic.css +1 -1
- package/src/tokens/uiSize/uiSize.css +5 -0
- 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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
312
|
-
-
|
|
313
|
-
-
|
|
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-
|
|
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-
|
|
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<
|
|
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
|
-
//
|
|
972
|
-
//
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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 ||
|
|
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
|
-
|
|
1106
|
+
isAll ? (
|
|
1104
1107
|
<>
|
|
1105
|
-
已選取全部 {TOTAL}
|
|
1108
|
+
已選取全部 {TOTAL} 個項目{selection.excluded.length > 0 ? `(排除 ${selection.excluded.length} 個)` : ''}。{' '}
|
|
1106
1109
|
<button
|
|
1107
1110
|
type="button"
|
|
1108
|
-
onClick={() => {
|
|
1109
|
-
className="text-primary hover:
|
|
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
|
-
已選取本頁全部 {
|
|
1119
|
+
已選取本頁全部 {selectedCount} 個。{' '}
|
|
1117
1120
|
<button
|
|
1118
1121
|
type="button"
|
|
1119
|
-
onClick={() =>
|
|
1120
|
-
className="text-primary hover:
|
|
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
|
-
{
|
|
1132
|
+
{selectedCount > 0 && (
|
|
1130
1133
|
<BulkActionBar
|
|
1131
|
-
selection={
|
|
1132
|
-
|
|
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
|
-
/**
|
|
138
|
-
selection?: string[]
|
|
139
|
-
/** 預設選取(uncontrolled) */
|
|
140
|
-
defaultSelection?: string[]
|
|
141
|
-
/** Selection 變更 callback */
|
|
142
|
-
onSelectionChange?: (next:
|
|
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<
|
|
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={
|
|
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
|
-
|
|
1683
|
-
|
|
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
|
|
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
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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(
|
|
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 = !
|
|
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
|
-
|
|
1753
|
-
|
|
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,
|
|
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 =>
|
|
1874
|
+
setSelection(prev => applySelectIds(prev, selectableVisibleIds, true))
|
|
1827
1875
|
return
|
|
1828
1876
|
}
|
|
1829
1877
|
// Esc:clear selection
|
|
1830
|
-
if (e.key === 'Escape' &&
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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: '
|
|
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: '
|
|
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-
|
|
54
|
-
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-
|
|
55
|
-
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-
|
|
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
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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:
|
|
51
|
-
<li><LinkTo kind="Design System/Components/DatePicker/展示" name="可清除"><span className="text-primary hover:
|
|
52
|
-
<li><LinkTo kind="Design System/Components/DatePicker/展示" name="尺寸"><span className="text-primary hover:
|
|
53
|
-
<li><LinkTo kind="Design System/Components/DatePicker/展示" name="範圍模式:訂房 / 訂機票情境"><span className="text-primary hover:
|
|
54
|
-
<li><LinkTo kind="Design System/Components/DatePicker/展示" name="展示樣式"><span className="text-primary hover:
|
|
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>
|