@qijenchen/design-system 0.1.0-beta.84 → 0.1.0-beta.85

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 (37) hide show
  1. package/dist/components/DataTable/data-table-filter-group.d.ts +47 -0
  2. package/dist/components/DataTable/data-table-filter-group.d.ts.map +1 -0
  3. package/dist/components/DataTable/data-table-filter-group.js +154 -0
  4. package/dist/components/DataTable/data-table-filter-group.js.map +1 -0
  5. package/dist/components/DataTable/data-table-filter-panel.d.ts.map +1 -1
  6. package/dist/components/DataTable/data-table-filter-panel.js +2 -324
  7. package/dist/components/DataTable/data-table-filter-panel.js.map +1 -1
  8. package/dist/components/DataTable/data-table-filter-value-picker.d.ts +29 -0
  9. package/dist/components/DataTable/data-table-filter-value-picker.d.ts.map +1 -0
  10. package/dist/components/DataTable/data-table-filter-value-picker.js +187 -0
  11. package/dist/components/DataTable/data-table-filter-value-picker.js.map +1 -0
  12. package/dist/components/DataTable/data-table.d.ts.map +1 -1
  13. package/dist/components/DataTable/data-table.js.map +1 -1
  14. package/dist/components/Field/index.d.ts +1 -0
  15. package/dist/components/Field/index.d.ts.map +1 -1
  16. package/dist/components/Field/index.js +2 -0
  17. package/dist/components/Field/index.js.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/ds-canonical/hooks/check_datatable_invariants.sh +6 -3
  23. package/llms-full.txt +1 -1
  24. package/llms.txt +1 -1
  25. package/package.json +1 -1
  26. package/src/components/Checkbox/checkbox.spec.md +2 -0
  27. package/src/components/DataTable/data-table-filter-group.tsx +231 -0
  28. package/src/components/DataTable/data-table-filter-panel.tsx +13 -430
  29. package/src/components/DataTable/data-table-filter-value-picker.tsx +237 -0
  30. package/src/components/DataTable/data-table.spec.md +1 -1
  31. package/src/components/DataTable/data-table.tsx +4 -1
  32. package/src/components/DataTable/filter-operators.spec.md +3 -3
  33. package/src/components/Field/index.ts +1 -0
  34. package/src/components/Select/select.spec.md +2 -0
  35. package/src/index.ts +4 -0
  36. package/src/tokens/color/color.spec.md +1 -1
  37. package/src/tokens/uiSize/uiSize.css +1 -1
@@ -0,0 +1,237 @@
1
+ // @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved(2026-07-14 自 data-table-filter-panel.tsx 拆檔搬移,原檔級 escape 隨段落帶入)。
2
+ // @internal — DS-internal 單元(FilterValuePicker value-picker switcher,只被
3
+ // data-table-filter-group.tsx FilterRow 消費);不隨 index.ts re-export 進 npm public surface。
4
+ // ────────────────────────────── 消費的 SSOT ──────────────────────────────
5
+ // - Field controls(Family 4,統一 size="sm"):Input / NumberInput / Select / Combobox /
6
+ // DatePicker + DatePickerRange(showTime)/ PeoplePicker — 按 ValueShape dispatch
7
+ // (ValueShape ↔ picker 對照 canonical:./data-table.spec.md「三、Operator × ValueShape SSOT」)
8
+ // - className 直接 forward 給 inner Field control(不另開 wrapper div)—
9
+ // field-control-group.spec.md「forward className」canonical(2026-05-04 #2 fix)
10
+ // - Operator / ValueShape SSOT:./filter-operators.ts(DATE_RELATIVE_OPTIONS / DATE_RELATIVE_GROUPS)
11
+ // - Person pool:PeoplePicker PersonValue(對齊 cell-registry meta.people SSOT)
12
+ // 設計規則:./filter-operators.spec.md「ValueShape canonical」+ ./data-table.spec.md「進階篩選」段
13
+
14
+ import { Select, type SelectOption } from '@/design-system/components/Select/select'
15
+ import { Combobox } from '@/design-system/components/Combobox/combobox'
16
+ import { Input } from '@/design-system/components/Input/input'
17
+ import { NumberInput } from '@/design-system/components/NumberInput/number-input'
18
+ import { DatePicker, DatePickerRange } from '@/design-system/components/DatePicker/date-picker'
19
+ import { PeoplePicker } from '@/design-system/components/PeoplePicker/people-picker'
20
+ import type { PersonValue } from '@/design-system/components/PeoplePicker/person-display'
21
+ import {
22
+ DATE_RELATIVE_OPTIONS,
23
+ DATE_RELATIVE_GROUPS,
24
+ type ValueShape,
25
+ } from './filter-operators'
26
+
27
+ // ── FilterValuePicker(value-picker switcher per ValueShape)────────────────
28
+ //
29
+ // 2026-05-03 M21 retract:本 helper 原獨立檔 filter-value-picker.tsx(187 行 / 1 consumer),
30
+ // claim「未來 inline filter UI 共用」= premature abstraction → inline 回 panel。
31
+ // 2026-07-14 拆檔 refactor:panel 822 行過 800 hard cap(file-size escalation)→ 重新抽出為
32
+ // @internal 私有模組(非 public API 抽象,與 M21 不衝突;consumer 仍只見 DataTableFilterPanel)。
33
+
34
+ export interface FilterValuePickerColInfo {
35
+ id: string
36
+ label: string
37
+ options?: Array<{ value: string; label: string }>
38
+ /** Person pool — person/multiPerson filter picker 用(2026-05-07 升級,SSOT 對齊 cell-registry) */
39
+ people?: Array<{ name: string; avatarUrl?: string; description?: string }>
40
+ }
41
+
42
+ export interface FilterValuePickerProps {
43
+ shape: ValueShape | null
44
+ value: unknown
45
+ onChange: (v: unknown) => void
46
+ colInfo?: FilterValuePickerColInfo
47
+ disabled?: boolean
48
+ /** 用 column.label 組「{label} 篩選值」(panel 每 row 不顯式 label,a11y 必填) */
49
+ ariaLabel?: string
50
+ /** Forward 給內部 Field control 的 className(2026-05-04 #2 fix)
51
+ * 避免外層包 wrapper div 破壞 FieldControlGroup CSS variants(rounded radii / margin overlap) */
52
+ className?: string
53
+ }
54
+
55
+ export function FilterValuePicker({
56
+ shape,
57
+ value,
58
+ onChange,
59
+ colInfo,
60
+ disabled,
61
+ ariaLabel,
62
+ className,
63
+ }: FilterValuePickerProps) {
64
+ if (!shape || disabled) {
65
+ return <Input size="sm" value="" onChange={() => {}} placeholder="輸入值…" disabled aria-label={ariaLabel} className={className} />
66
+ }
67
+
68
+ switch (shape) {
69
+ case 'none':
70
+ return null
71
+
72
+ case 'text':
73
+ return (
74
+ <Input
75
+ size="sm"
76
+ value={String(value ?? '')}
77
+ onChange={(e) => onChange(e.target.value)}
78
+ placeholder="輸入值…"
79
+ aria-label={ariaLabel}
80
+ className={className}
81
+ />
82
+ )
83
+
84
+ case 'number':
85
+ return (
86
+ <NumberInput
87
+ size="sm"
88
+ value={typeof value === 'number' ? value : null}
89
+ onChange={(v) => onChange(v ?? '')}
90
+ placeholder="輸入數字…"
91
+ aria-label={ariaLabel}
92
+ className={className}
93
+ />
94
+ )
95
+
96
+ case 'date_single':
97
+ return (
98
+ <DatePicker
99
+ size="sm"
100
+ value={typeof value === 'string' ? value : null}
101
+ onChange={(v) => onChange(v ?? '')}
102
+ aria-label={ariaLabel}
103
+ className={className}
104
+ />
105
+ )
106
+
107
+ case 'date_range':
108
+ return (
109
+ <DatePickerRange
110
+ size="sm"
111
+ value={Array.isArray(value) && value.length === 2
112
+ ? (value as [string | null, string | null])
113
+ : null}
114
+ onChange={(v) => onChange(v)}
115
+ aria-label={ariaLabel}
116
+ className={className}
117
+ />
118
+ )
119
+
120
+ case 'date_relative': {
121
+ // 群組分類:Past / Current / Future(對齊 Linear / Notion idiom),走 Select.groups → SelectMenu
122
+ const opts: SelectOption[] = DATE_RELATIVE_OPTIONS.map((o) => ({
123
+ value: o.value,
124
+ label: o.label,
125
+ group: o.group,
126
+ }))
127
+ return (
128
+ <Select
129
+ size="sm"
130
+ options={opts}
131
+ groups={DATE_RELATIVE_GROUPS as unknown as Array<{ key: string; label: string }>}
132
+ value={String(value ?? '')}
133
+ onChange={(v) => onChange(v)}
134
+ placeholder="選擇相對日期"
135
+ aria-label={ariaLabel}
136
+ className={className}
137
+ />
138
+ )
139
+ }
140
+
141
+ case 'select_single': {
142
+ const opts: SelectOption[] = (colInfo?.options ?? []).map((o) => ({
143
+ value: o.value,
144
+ label: o.label,
145
+ }))
146
+ return (
147
+ <Select
148
+ size="sm"
149
+ options={opts}
150
+ value={String(value ?? '')}
151
+ onChange={(v) => onChange(v)}
152
+ placeholder="選擇值"
153
+ aria-label={ariaLabel}
154
+ className={className}
155
+ />
156
+ )
157
+ }
158
+
159
+ case 'select_multi': {
160
+ const opts = (colInfo?.options ?? []).map((o) => ({
161
+ value: o.value,
162
+ label: o.label,
163
+ }))
164
+ const arr = Array.isArray(value) ? (value as string[]) : []
165
+ return (
166
+ <Combobox
167
+ size="sm"
168
+ options={opts}
169
+ value={arr}
170
+ onChange={(v) => onChange(v)}
171
+ placeholder="選擇值…"
172
+ aria-label={ariaLabel}
173
+ className={className}
174
+ />
175
+ )
176
+ }
177
+
178
+ case 'datetime_single':
179
+ return (
180
+ <DatePicker
181
+ size="sm"
182
+ showTime
183
+ value={typeof value === 'string' ? value : null}
184
+ onChange={(v) => onChange(v ?? '')}
185
+ aria-label={ariaLabel}
186
+ className={className}
187
+ />
188
+ )
189
+
190
+ case 'datetime_range':
191
+ return (
192
+ <DatePickerRange
193
+ size="sm"
194
+ showTime
195
+ value={Array.isArray(value) && value.length === 2
196
+ ? (value as [string | null, string | null])
197
+ : null}
198
+ onChange={(v) => onChange(v)}
199
+ aria-label={ariaLabel}
200
+ className={className}
201
+ />
202
+ )
203
+
204
+ // person_single / person_multi — 走 PeoplePicker(2026-05-07 升級,對齊 cell-registry SSOT)。
205
+ // colInfo.people 來自 column meta.people。Filter value:
206
+ // - person_single:存 PersonValue | null(picker emit array,我們取 [0])
207
+ // - person_multi:存 PersonValue[]
208
+ case 'person_single': {
209
+ const v = value as PersonValue | null | undefined
210
+ return (
211
+ <PeoplePicker
212
+ size="sm"
213
+ value={v ?? null}
214
+ people={colInfo?.people ?? []}
215
+ onChange={(next) => onChange(next[0] ?? null)}
216
+ aria-label={ariaLabel}
217
+ className={className}
218
+ />
219
+ )
220
+ }
221
+ case 'person_multi': {
222
+ const v = Array.isArray(value) ? (value as PersonValue[]) : []
223
+ return (
224
+ <PeoplePicker
225
+ size="sm"
226
+ value={v}
227
+ people={colInfo?.people ?? []}
228
+ onChange={(next) => onChange(next)}
229
+ aria-label={ariaLabel}
230
+ className={className}
231
+ />
232
+ )
233
+ }
234
+ default:
235
+ return null
236
+ }
237
+ }
@@ -305,7 +305,7 @@ type FilterTreeNested = { mode: 'nested'; conjunction: 'and'|'or'; children: Fil
305
305
 
306
306
  ### 三、Operator × ValueShape SSOT
307
307
 
308
- `filter-operators.ts` 的 `OPERATOR_REGISTRY: Record<ColumnType, OperatorSpec[]>` 是唯一 truth。Panel 完全 data-driven:field 選 → load op set → 選 op → 由 `valueShape` dispatch picker(`filter-value-picker.tsx`)。`is_set` / `is_not_set` / `is_true` / `is_false`(`ValueShape='none'`)不渲 picker。
308
+ `filter-operators.ts` 的 `OPERATOR_REGISTRY: Record<ColumnType, OperatorSpec[]>` 是唯一 truth。Panel 完全 data-driven:field 選 → load op set → 選 op → 由 `valueShape` dispatch picker(`data-table-filter-value-picker.tsx`,@internal;2026-07-14 file-size 拆檔自 panel)。`is_set` / `is_not_set` / `is_true` / `is_false`(`ValueShape='none'`)不渲 picker。
309
309
 
310
310
  ValueShape ↔ DS picker 對照(canonical 2026-05-02):
311
311
 
@@ -5,13 +5,16 @@
5
5
  // 每個檔過 M21 / M17 / Rule-of-3 三 test:
6
6
  // - data-table.tsx(主,foundational)
7
7
  // - data-table-filter-panel.tsx + data-table-sort-manager.tsx + data-table-column-visibility-panel.tsx(panel state 隔離)
8
+ // - data-table-filter-group.tsx + data-table-filter-value-picker.tsx(@internal;2026-07-14 filter panel
9
+ // 過 800 hard cap 拆檔 — row/nested-group renderer + ValueShape picker,只被 panel 消費)
8
10
  // - cell-registry.tsx(column type → cell display / edit 解析 SSOT)
9
11
  // - data-table-interaction-layer.tsx + active-editor-controller.ts(spreadsheet overlay + portal editor)
10
12
  // - column-types.ts + filter-operators.ts(✓ Rule-of-3 SSOT,3+ consumer)
11
13
  // - filter-tree.ts(pure data + eval,test isolation)
12
14
  // - lib/column-meta.ts(Internal SSOT,消 5 處 `(col as any)`)
13
15
  // - + stories / spec / css
14
- // M21 retract:filter-value-picker.tsx 1 consumer → inline 回 panel
16
+ // M21 retract:filter-value-picker.tsx 1 consumer → 2026-05-03 inline 回 panel(上列 2026-07-14
17
+ // @internal 拆檔非 M21 迴轉:非 public 抽象,file-size hard cap 驅動,consumer 仍只見 panel)。
15
18
  import * as React from 'react'
16
19
  import { createPortal } from 'react-dom'
17
20
  import { Empty } from '@/design-system/components/Empty/empty'
@@ -1,6 +1,6 @@
1
1
  # Advanced Filter 設計原則(Operator × ColumnType × ValueShape SSOT,v3)
2
2
 
3
- > **狀態**:已落地 — registry 寫成 `filter-operators.ts`,`filter-tree.ts` + `data-table-filter-panel.tsx` 消費(原 draft 兩個升級條件皆已達成,2026-06-11 對齊)。
3
+ > **狀態**:已落地 — registry 寫成 `filter-operators.ts`,`filter-tree.ts` + filter panel 家族消費(`data-table-filter-panel.tsx`;row/picker 直接消費點在 2026-07-14 拆檔子模組 `data-table-filter-group.tsx` / `data-table-filter-value-picker.tsx`)(原 draft 兩個升級條件皆已達成,2026-06-11 對齊)。
4
4
  > **M8 benchmark**:對照 ClickUp(CU)/ Airtable(AT)/ Notion(NT)三家。**v3 加 datetime / includeTime + 砍 number.between(2/3 業界共識)**。
5
5
  > **設計路線**:**ClickUp 為 baseline**,合理 + 不衝突 + 真實用的擴充採納。
6
6
  > **檔位**:暫放 DataTable/,若決定抽 `patterns/advanced-filter/` 再搬。
@@ -133,7 +133,7 @@
133
133
  | 1-to-1 比對 | ✓ `is` / `is_not`(picker 多選 OR-語意) | ✗ 不適用(陣列比單值 ambiguous) |
134
134
  | 集合運算 | ✗ 不適用 | ✓ `has_any_of` / `has_all_of` / `has_none_of` |
135
135
 
136
- 差異**根源於 cell 資料形狀**,不是 picker 不同。Value picker 兩者都共用 `<Combobox>` DS 既有元件(code 真值 `data-table-filter-panel.tsx` select_multi case)。
136
+ 差異**根源於 cell 資料形狀**,不是 picker 不同。Value picker 兩者都共用 `<Combobox>` DS 既有元件(code 真值 `data-table-filter-value-picker.tsx` select_multi case;2026-07-14 拆檔自 panel)。
137
137
 
138
138
  ### `person`(4 ops — 跟 select 同邏輯)
139
139
  | op key | 中 | en | valueShape | CU | AT | NT |
@@ -238,7 +238,7 @@
238
238
 
239
239
  **技術坑(信心 85%,Phase B spec.md 詳)**:TanStack `ColumnFiltersState` per-column 一筆 filter value,N 條同 column 會 AND-chain 不能 OR。**業界標準解法**:棄用 `columnFilters`,改自管 FilterTree state + 用 `globalFilter` + 自訂 `globalFilterFn` 走樹求 boolean。
240
240
 
241
- ## 8. 邊界案例(code 真值 — `filter-tree.ts` / `data-table-filter-panel.tsx`)
241
+ ## 8. 邊界案例(code 真值 — `filter-tree.ts` / filter panel 家族:`data-table-filter-panel.tsx` + `data-table-filter-group.tsx`)
242
242
 
243
243
  - **Condition 值空 / 未選 field**:視為 incomplete,求值 **pass-through**(不過濾任何 row);空值 ≠ `is_not_set`(後者是顯式 op)— `isConditionComplete` 守門
244
244
  - **群組刪光 condition**:空群組保留並 pass-through(true),panel 顯示「移除空群組」按鈕由 user 顯式刪除,不自動消失
@@ -3,3 +3,4 @@
3
3
  // import { ... } from '@qijenchen/design-system/{components,patterns}/<Dir>'
4
4
  // Regenerate: node scripts/gen-component-indexes.mjs
5
5
  export * from './field'
6
+ export * from './field-types'
@@ -13,6 +13,8 @@ benchmark:
13
13
  - Polaris Select: github.com/Shopify/polaris/tree/main/polaris-react/src/components/Select
14
14
  ---
15
15
 
16
+ <!-- @spec-budget-exception: SSOT-heavy 例外(CLAUDE.md 行數預算「foundational SSOT 例外 ≤800-1200」)— 本檔持有跨元件 SSOT:「與 RadioGroup 的分界」(~40 行)+ 3 筆拍板記錄(2026-07-13 D2 hidden-input mirror / 2026-05-21 D3 dual-mode / 2026-06-12 R2 open-pair 例外);2026-07-14 knowledge-prune P2 結論「殘餘超標內容全是 canonical,再砍必動 meaning」(.claude/logs/prune-p2-candidates.md)。 -->
17
+
16
18
  <!-- @benchmark-cited: D5 retrofit 2026-05-18 — body claims marked per-claim @benchmark-unverified inline; canonical source URLs in frontmatter benchmark list. -->
17
19
 
18
20
  # Select 設計原則
package/src/index.ts CHANGED
@@ -246,12 +246,16 @@ export {
246
246
  FieldError,
247
247
  FieldGroup,
248
248
  FieldLabel,
249
+ getMenuListMinHeight,
249
250
  useFormValidation,
250
251
  } from './components/Field/index'
251
252
  export type {
252
253
  FieldGroupProps,
253
254
  FieldLabelProps,
255
+ FieldMode,
254
256
  FieldProps,
257
+ FieldVariant,
258
+ FieldWidth,
255
259
  FormFieldInputProps,
256
260
  UseFormValidationOptions,
257
261
  UseFormValidationReturn,
@@ -649,7 +649,7 @@ Dark mode 覆寫:hover/active 方向反轉(hover → step-7,active → ste
649
649
 
650
650
  | Token | 答 | 語意 | 典型場景(real consumer grep verified 2026-05-20) |
651
651
  |---|---|---|---|
652
- | `bg-muted`(neutral-2) | **是** | **靜態非互動 surface** — 退化 / placeholder / locked 視覺 | Skeleton(`skeleton.tsx:10`) / DataTable table header(`data-table.tsx:312 HEADER_BG`) / Calendar 非當月日格(`calendar.tsx:316`)/ Alert neutral(`alert.tsx:30`)/ DataTable filter-panel inner group container(`data-table-filter-panel.tsx:784`)/ tab 容器 / code block / scrollbar track(`semantic.css:367`)/ anatomy `<th>` |
652
+ | `bg-muted`(neutral-2) | **是** | **靜態非互動 surface** — 退化 / placeholder / locked 視覺 | Skeleton(`skeleton.tsx:10`) / DataTable table header(`data-table.tsx:312 HEADER_BG`) / Calendar 非當月日格(`calendar.tsx:316`)/ Alert neutral(`alert.tsx:30`)/ DataTable filter-panel inner group container(`data-table-filter-group.tsx:202`,2026-07-14 拆檔自 filter-panel)/ tab 容器 / code block / scrollbar track(`semantic.css:367`)/ anatomy `<th>` |
653
653
  | `bg-secondary`(neutral-3) | **否,只是視覺退後一級** | **存在且微淡可辨** — 元素是正常狀態,但需要退後一級 | Tag neutral(`tag.tsx`)/ Slider rest track(`slider.tsx`)/ FileItem compact-B progress track(`file-item.tsx`)/ Badge low(`badge.tsx:37`)/ CircularProgress track(`circular-progress.tsx:150`)/ Steps fillBg(`steps.tsx:698,715`)/ ProgressBar track(`progress-bar.tsx`)|
654
654
 
655
655
  **判斷法**:「這個元素是『還沒準備好 / 不可操作』嗎?」
@@ -75,7 +75,7 @@
75
75
  --tree-indent-lg: 1.75rem; /* 28px */
76
76
 
77
77
  /* DataTable Panel Widths — Sort manager / Filter panel cells(SSOT,2026-05-23 deep-audit Phase A.4 Decision 2 codify per user verbatim「決策二照你建議做」+「確保最SSOT最簡潔」)
78
- Consumer:data-table-sort-manager.tsx / data-table-filter-panel.tsx
78
+ Consumer:data-table-sort-manager.tsx / data-table-filter-group.tsx(FilterRow;2026-07-14 拆檔自 filter-panel)
79
79
  不 density-scale(panels 是 floating layout,不該跟密度走)
80
80
  Naming aligned to family `--<component>-<part>-<size>`(per --field-height-* / --table-row-* / --tab-height-* precedent)*/
81
81
  --data-table-sort-panel-width: 30rem; /* 480px — sort manager root container width */