@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
@@ -1,33 +1,23 @@
1
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.
2
2
  // same-row-mixed-allow: header chrome corner buttons(close)跟 row inline actions(trash)不在同 row
3
- // code-quality-allow: file-size — 2026-05-03 M21 retract:filter-value-picker.tsx(187 行 / 1 consumer)inline 回本檔(505 → 687 行)。2026-07-14 deep-audit 誠實更新:現 822 行,**已過 800 hard cap** — 非 stale escape 留著;拆檔候選 = filter-value-picker 區段(等第 2 個 consumer 接入)+ nested-group renderer,屬結構 refactor 需獨立 branch 做 + 完整驗證,列入 file-size escalation 追蹤,禁再增量。
4
3
  import * as React from 'react'
5
- import { Plus, Trash2, X as XIcon, RotateCcw } from 'lucide-react'
4
+ import { Plus, X as XIcon, RotateCcw } from 'lucide-react'
6
5
  import type { ColumnDef } from '@tanstack/react-table'
7
6
  import { cn } from '@/lib/utils'
8
7
  import { Button } from '@/design-system/components/Button/button'
9
- import { Select, type SelectOption } from '@/design-system/components/Select/select'
10
- import { Combobox } from '@/design-system/components/Combobox/combobox'
11
- import { Input } from '@/design-system/components/Input/input'
12
- import { NumberInput } from '@/design-system/components/NumberInput/number-input'
13
- import { DatePicker, DatePickerRange } from '@/design-system/components/DatePicker/date-picker'
14
- import { PeoplePicker } from '@/design-system/components/PeoplePicker/people-picker'
15
- import type { PersonValue } from '@/design-system/components/PeoplePicker/person-display'
8
+ import type { SelectOption } from '@/design-system/components/Select/select'
16
9
  import { SurfaceHeader, SurfaceBody, COMPACT_HEADER_SLOT } from '@/design-system/patterns/overlay-surface/overlay-surface'
17
10
  import { PopoverTitle, PopoverClose } from '@/design-system/components/Popover/popover'
18
11
  import { ButtonDivider } from '@/design-system/components/Button/button-group'
19
- import { FieldControlGroup } from '@/design-system/components/FieldControlGroup/field-control-group'
20
12
  import type { ColumnType } from './column-types'
21
13
  import { getColumnId, getColumnLabel, getColumnMeta } from './lib/column-meta'
22
14
  import {
23
- OPERATOR_REGISTRY,
24
- DEFAULT_OPERATOR,
25
- DATE_RELATIVE_OPTIONS,
26
- DATE_RELATIVE_GROUPS,
27
- getOperatorSpec,
28
- getValueShape,
29
- type ValueShape,
30
- } from './filter-operators'
15
+ FilterRow,
16
+ GroupBlock,
17
+ getDefaultOperator,
18
+ datePrecisionOf,
19
+ type FilterColumn,
20
+ } from './data-table-filter-group'
31
21
  import {
32
22
  createEmptyFilterTree,
33
23
  isFilterTreeActive,
@@ -64,6 +54,9 @@ export type { Conjunction, FilterCondition, FilterGroup, FilterTree, FilterTreeF
64
54
  * Source-of-truth:
65
55
  * - Operator definitions:`./filter-operators.ts` `OPERATOR_REGISTRY`(SSOT,禁 hardcode op 字串)
66
56
  * - Filter state:**FilterTree**(本檔自管;搭配 TanStack `globalFilter` 求值)
57
+ * - Row / nested-group renderer:`./data-table-filter-group.tsx`;value picker:
58
+ * `./data-table-filter-value-picker.tsx`(皆 @internal — 2026-07-14 file-size 拆檔,
59
+ * 本檔只留 orchestration:columns 抽取 / FilterTree mutators / panel chrome)
67
60
  *
68
61
  * 詳:`./filter-operators.spec.md`
69
62
  */
@@ -73,17 +66,8 @@ export type { Conjunction, FilterCondition, FilterGroup, FilterTree, FilterTreeF
73
66
  let _idSeed = 0
74
67
  const newId = () => `f${++_idSeed}-${Date.now().toString(36)}`
75
68
 
76
- // ── Helpers — internal types ────────────────────────────────────────────
77
-
78
- interface FilterColumn {
79
- id: string
80
- label: string
81
- type: ColumnType
82
- options?: Array<{ value: string; label: string }>
83
- /** People pool for person/multiPerson filter picker(對齊 cell-registry meta.people SSOT)*/
84
- people?: Array<{ name: string; avatarUrl?: string; description?: string }>
85
- includeTime?: boolean
86
- }
69
+ // ── Helpers — column extraction + condition factories ──────────────────
70
+ // (FilterColumn type + operator helpers 住 ./data-table-filter-group.tsx)
87
71
 
88
72
  function extractColumns<TData>(columns: ColumnDef<TData, any>[]): FilterColumn[] {
89
73
  const out: FilterColumn[] = []
@@ -106,20 +90,6 @@ function extractColumns<TData>(columns: ColumnDef<TData, any>[]): FilterColumn[]
106
90
  return out
107
91
  }
108
92
 
109
- function getOperatorOptions(type?: ColumnType): SelectOption[] {
110
- const registry = type && OPERATOR_REGISTRY[type] ? OPERATOR_REGISTRY[type] : OPERATOR_REGISTRY.string
111
- return registry.map((op) => ({ value: op.op, label: op.label }))
112
- }
113
-
114
- function getDefaultOperator(type?: ColumnType): string {
115
- return (type && DEFAULT_OPERATOR[type]) || DEFAULT_OPERATOR.string
116
- }
117
-
118
- // Q6(2026-07-04):date 欄位把比對精度寫進 condition(includeTime=true → ms,否則 day)。
119
- // 'time' 欄暫不寫(值非完整日期,truncate 語意不成立;見 filter-operators.spec v3 time 註)。
120
- const datePrecisionOf = (col: FilterColumn | undefined): 'day' | 'ms' | undefined =>
121
- col?.type === 'date' ? (col.includeTime ? 'ms' : 'day') : undefined
122
-
123
93
  const newCondition = (firstCol: FilterColumn | undefined): FilterCondition => ({
124
94
  kind: 'cond',
125
95
  id: newId(),
@@ -153,217 +123,6 @@ const newEmptyGroup = (): FilterGroup => ({
153
123
  children: [newEmptyCondition()],
154
124
  })
155
125
 
156
- // ── Internal — FilterValuePicker(value-picker switcher per ValueShape)──
157
- //
158
- // 2026-05-03 M21 retract:本 helper 原本獨立檔 `filter-value-picker.tsx`,
159
- // claim「未來 inline filter UI 共用」但只 1 consumer(本 panel)= 違 M21
160
- // premature abstraction。inline 回 panel,日後若真有第 2 consumer 再抽。
161
-
162
- interface FilterValuePickerColInfo {
163
- id: string
164
- label: string
165
- options?: Array<{ value: string; label: string }>
166
- /** Person pool — person/multiPerson filter picker 用(2026-05-07 升級,SSOT 對齊 cell-registry) */
167
- people?: Array<{ name: string; avatarUrl?: string; description?: string }>
168
- }
169
-
170
- interface FilterValuePickerProps {
171
- shape: ValueShape | null
172
- value: unknown
173
- onChange: (v: unknown) => void
174
- colInfo?: FilterValuePickerColInfo
175
- disabled?: boolean
176
- /** 用 column.label 組「{label} 篩選值」(panel 每 row 不顯式 label,a11y 必填) */
177
- ariaLabel?: string
178
- /** Forward 給內部 Field control 的 className(2026-05-04 #2 fix)
179
- * 避免外層包 wrapper div 破壞 FieldControlGroup CSS variants(rounded radii / margin overlap) */
180
- className?: string
181
- }
182
-
183
- function FilterValuePicker({
184
- shape,
185
- value,
186
- onChange,
187
- colInfo,
188
- disabled,
189
- ariaLabel,
190
- className,
191
- }: FilterValuePickerProps) {
192
- if (!shape || disabled) {
193
- return <Input size="sm" value="" onChange={() => {}} placeholder="輸入值…" disabled aria-label={ariaLabel} className={className} />
194
- }
195
-
196
- switch (shape) {
197
- case 'none':
198
- return null
199
-
200
- case 'text':
201
- return (
202
- <Input
203
- size="sm"
204
- value={String(value ?? '')}
205
- onChange={(e) => onChange(e.target.value)}
206
- placeholder="輸入值…"
207
- aria-label={ariaLabel}
208
- className={className}
209
- />
210
- )
211
-
212
- case 'number':
213
- return (
214
- <NumberInput
215
- size="sm"
216
- value={typeof value === 'number' ? value : null}
217
- onChange={(v) => onChange(v ?? '')}
218
- placeholder="輸入數字…"
219
- aria-label={ariaLabel}
220
- className={className}
221
- />
222
- )
223
-
224
- case 'date_single':
225
- return (
226
- <DatePicker
227
- size="sm"
228
- value={typeof value === 'string' ? value : null}
229
- onChange={(v) => onChange(v ?? '')}
230
- aria-label={ariaLabel}
231
- className={className}
232
- />
233
- )
234
-
235
- case 'date_range':
236
- return (
237
- <DatePickerRange
238
- size="sm"
239
- value={Array.isArray(value) && value.length === 2
240
- ? (value as [string | null, string | null])
241
- : null}
242
- onChange={(v) => onChange(v)}
243
- aria-label={ariaLabel}
244
- className={className}
245
- />
246
- )
247
-
248
- case 'date_relative': {
249
- // 群組分類:Past / Current / Future(對齊 Linear / Notion idiom),走 Select.groups → SelectMenu
250
- const opts: SelectOption[] = DATE_RELATIVE_OPTIONS.map((o) => ({
251
- value: o.value,
252
- label: o.label,
253
- group: o.group,
254
- }))
255
- return (
256
- <Select
257
- size="sm"
258
- options={opts}
259
- groups={DATE_RELATIVE_GROUPS as unknown as Array<{ key: string; label: string }>}
260
- value={String(value ?? '')}
261
- onChange={(v) => onChange(v)}
262
- placeholder="選擇相對日期"
263
- aria-label={ariaLabel}
264
- className={className}
265
- />
266
- )
267
- }
268
-
269
- case 'select_single': {
270
- const opts: SelectOption[] = (colInfo?.options ?? []).map((o) => ({
271
- value: o.value,
272
- label: o.label,
273
- }))
274
- return (
275
- <Select
276
- size="sm"
277
- options={opts}
278
- value={String(value ?? '')}
279
- onChange={(v) => onChange(v)}
280
- placeholder="選擇值"
281
- aria-label={ariaLabel}
282
- className={className}
283
- />
284
- )
285
- }
286
-
287
- case 'select_multi': {
288
- const opts = (colInfo?.options ?? []).map((o) => ({
289
- value: o.value,
290
- label: o.label,
291
- }))
292
- const arr = Array.isArray(value) ? (value as string[]) : []
293
- return (
294
- <Combobox
295
- size="sm"
296
- options={opts}
297
- value={arr}
298
- onChange={(v) => onChange(v)}
299
- placeholder="選擇值…"
300
- aria-label={ariaLabel}
301
- className={className}
302
- />
303
- )
304
- }
305
-
306
- case 'datetime_single':
307
- return (
308
- <DatePicker
309
- size="sm"
310
- showTime
311
- value={typeof value === 'string' ? value : null}
312
- onChange={(v) => onChange(v ?? '')}
313
- aria-label={ariaLabel}
314
- className={className}
315
- />
316
- )
317
-
318
- case 'datetime_range':
319
- return (
320
- <DatePickerRange
321
- size="sm"
322
- showTime
323
- value={Array.isArray(value) && value.length === 2
324
- ? (value as [string | null, string | null])
325
- : null}
326
- onChange={(v) => onChange(v)}
327
- aria-label={ariaLabel}
328
- className={className}
329
- />
330
- )
331
-
332
- // person_single / person_multi — 走 PeoplePicker(2026-05-07 升級,對齊 cell-registry SSOT)。
333
- // colInfo.people 來自 column meta.people。Filter value:
334
- // - person_single:存 PersonValue | null(picker emit array,我們取 [0])
335
- // - person_multi:存 PersonValue[]
336
- case 'person_single': {
337
- const v = value as PersonValue | null | undefined
338
- return (
339
- <PeoplePicker
340
- size="sm"
341
- value={v ?? null}
342
- people={colInfo?.people ?? []}
343
- onChange={(next) => onChange(next[0] ?? null)}
344
- aria-label={ariaLabel}
345
- className={className}
346
- />
347
- )
348
- }
349
- case 'person_multi': {
350
- const v = Array.isArray(value) ? (value as PersonValue[]) : []
351
- return (
352
- <PeoplePicker
353
- size="sm"
354
- value={v}
355
- people={colInfo?.people ?? []}
356
- onChange={(next) => onChange(next)}
357
- aria-label={ariaLabel}
358
- className={className}
359
- />
360
- )
361
- }
362
- default:
363
- return null
364
- }
365
- }
366
-
367
126
  // ── Component Props ─────────────────────────────────────────────────────
368
127
 
369
128
  export interface DataTableFilterPanelProps<TData> {
@@ -644,179 +403,3 @@ export const DataTableFilterPanel = React.forwardRef(DataTableFilterPanelInner)
644
403
  props: DataTableFilterPanelProps<TData> & { ref?: React.ForwardedRef<HTMLDivElement> }
645
404
  ) => React.ReactElement
646
405
  ;(DataTableFilterPanel as { displayName?: string }).displayName = 'DataTableFilterPanel'
647
-
648
- // ── ConjunctionLabel ───────────────────────────────────────────────────
649
-
650
- const CONJ_OPTIONS: SelectOption[] = [
651
- { value: 'and', label: 'And' },
652
- { value: 'or', label: 'Or' },
653
- ]
654
-
655
- function ConjunctionLabel({
656
- index, conjunction, onChange,
657
- }: { index: number; conjunction: Conjunction; onChange: (c: Conjunction) => void }) {
658
- // index === 0:首 row 顯示靜態「Where」label
659
- // index === 1:**唯一可改**的 AND/OR Select(連動整 group conjunction)
660
- // index ≥ 2:被連動的 row,read-only 顯示當前 conjunction 文字(同 Where 視覺,A6 canonical)
661
- // 對齊 Airtable / Notion / Linear 共識 @benchmark-unverified(non-OSS)
662
- // px-[var(--field-px)] 對齊 Field 內部 padding 12px(Q13)
663
- if (index === 0) {
664
- return <div className="w-20 shrink-0 text-body text-fg-muted px-[var(--field-px)] self-center">Where</div>
665
- }
666
- if (index >= 2) {
667
- const label = conjunction === 'and' ? 'And' : 'Or'
668
- return <div className="w-20 shrink-0 text-body text-fg-muted px-[var(--field-px)] self-center">{label}</div>
669
- }
670
- // index === 1:可切換的 AND/OR Select
671
- // minRows={2} — And/Or 2 選項,顯式縮 menu 高度避免 reserve 3 row 空白(Q5)
672
- return (
673
- <div className="w-20 shrink-0">
674
- <Select
675
- size="sm"
676
- options={CONJ_OPTIONS}
677
- value={conjunction}
678
- onChange={(v) => onChange(v as Conjunction)}
679
- minRows={2}
680
- aria-label="連接詞 — 同 group 共用"
681
- />
682
- </div>
683
- )
684
- }
685
-
686
- // ── FilterRow(flat 用 + group 內共用) ──────────────────────────────
687
-
688
- function FilterRow({
689
- index, condition, conjunction, filterableColumns, fieldOptions,
690
- onChangeConjunction, onChangeField, onChangeOp, onChangeValue, onRemove,
691
- }: {
692
- index: number
693
- condition: FilterCondition
694
- conjunction: Conjunction
695
- filterableColumns: FilterColumn[]
696
- fieldOptions: SelectOption[]
697
- onChangeConjunction: (c: Conjunction) => void
698
- onChangeField: (v: string) => void
699
- onChangeOp: (v: string) => void
700
- onChangeValue: (v: unknown) => void
701
- onRemove: () => void
702
- }) {
703
- const colInfo = filterableColumns.find((c) => c.id === condition.field)
704
- const operatorOptions = getOperatorOptions(colInfo?.type)
705
- const hasField = !!condition.field
706
- const opSpec = colInfo ? getOperatorSpec(colInfo.type, condition.op) : null
707
- const valueShape: ValueShape | null = colInfo && opSpec
708
- ? getValueShape(opSpec, colInfo.type, colInfo.includeTime)
709
- : null
710
- // op 'is_set' / 'is_not_set' 等 shape='none' → 無 value cell,op 自動 expand 填剩餘寬
711
- // 對齊 Notion / Airtable / Linear filter row 行為
712
- // 注意:valueShape=null(初始無 field 選)時仍 render value cell(disabled placeholder)— 只 'none' 才 fold
713
- const hasValueCell = valueShape !== 'none'
714
-
715
- // FieldControlGroup 接合 field + op + value 視覺(2026-05-04 E refactor + 多輪 fix):
716
- // - border collapse 取代 3 顆獨立 Select 並排,對齊 Airtable / Linear / Notion filter row idiom
717
- // - ConjunctionLabel + Trash 在 group 外層(meta actions,不屬 control 一體)
718
- // - **#5 fix**:row 內水平 gap = `gap-2` (8px),layoutSpace 規則 5 緊密相關
719
- // - **#9 fix**:cell 用 `min-w-[]`(field 160 / op 120),value flex-1 min-w-0,讓 long label 可撐寬
720
- // - **#2 fix**:FilterValuePicker 直接是 FieldControlGroup direct child(無 wrapper div),CSS variants 命中正確
721
- return (
722
- <div className="flex items-center gap-2">
723
- <ConjunctionLabel index={index} conjunction={conjunction} onChange={onChangeConjunction} />
724
- {/* **#9 fix(2026-05-04 v4)**:Field controls trigger `w-full` override 外 className,改用 Tailwind `!`
725
- important 強制 override(`!w-[160px]` / `!w-[120px]`),value 用 `!flex-1 !min-w-0`。
726
- Select 元件本身沒 destructure `style` prop 所以 inline style flex-basis 行不通,只能用 className。 */}
727
- <FieldControlGroup block className="flex-1 min-w-0">
728
- {/* 2026-05-23 Phase A.4 Decision 2:`!w-[160px]` / `!w-[120px]` → tokens
729
- `--data-table-filter-field-width` / `--data-table-filter-op-width`(SSOT in uiSize.css)
730
- Behavior preserved 完好如初:flat + nested 同 width(token 是 design constant) */}
731
- <Select
732
- className="!w-[var(--data-table-filter-field-width)] flex-shrink-0"
733
- size="sm"
734
- options={fieldOptions}
735
- value={condition.field}
736
- onChange={onChangeField}
737
- placeholder="選擇欄位"
738
- aria-label="篩選欄位"
739
- />
740
- <Select
741
- className={hasValueCell ? '!w-[var(--data-table-filter-op-width)] flex-shrink-0' : '!flex-1 !min-w-0'}
742
- size="sm"
743
- options={operatorOptions}
744
- value={condition.op}
745
- onChange={onChangeOp}
746
- disabled={!hasField}
747
- placeholder="運算子"
748
- aria-label="篩選運算子"
749
- />
750
- {hasValueCell && (
751
- <FilterValuePicker
752
- shape={valueShape}
753
- value={condition.value}
754
- onChange={onChangeValue}
755
- colInfo={colInfo}
756
- disabled={!hasField}
757
- ariaLabel={colInfo ? `${colInfo.label} 篩選值` : '篩選值'}
758
- className="!flex-1 !min-w-0"
759
- />
760
- )}
761
- </FieldControlGroup>
762
- {/* Trash 用 text Button — filter row 是 form-control row,Field 同高對齊(28 md) */}
763
- <Button variant="text" size="sm" iconOnly startIcon={Trash2} aria-label="刪除" onClick={onRemove} />
764
- </div>
765
- )
766
- }
767
-
768
- // ── GroupBlock(nested 用) ────────────────────────────────────────────
769
-
770
- function GroupBlock({
771
- index, group, rootConjunction, filterableColumns, fieldOptions,
772
- onChangeRootConjunction, onChangeGroupConjunction,
773
- onChangeCondition, onRemoveCondition, onAddCondition, onRemoveGroup,
774
- }: {
775
- index: number
776
- group: FilterGroup
777
- rootConjunction: Conjunction
778
- filterableColumns: FilterColumn[]
779
- fieldOptions: SelectOption[]
780
- onChangeRootConjunction: (c: Conjunction) => void
781
- onChangeGroupConjunction: (c: Conjunction) => void
782
- onChangeCondition: (condId: string, patch: Partial<FilterCondition>) => void
783
- onRemoveCondition: (condId: string) => void
784
- onAddCondition: () => void
785
- onRemoveGroup: () => void
786
- }) {
787
- return (
788
- <div className="flex items-start gap-2">
789
- <div className="pt-2">
790
- <ConjunctionLabel index={index} conjunction={rootConjunction} onChange={onChangeRootConjunction} />
791
- </div>
792
- {/* Group container 灰底 — `bg-muted`(`--muted` neutral-2,user 2026-05-09 拍板 Q3 A)。對齊 color.spec.md L651-654 「table header / tab / code block / skeleton」靜態低重要 surface semantic */}
793
- <div className="flex-1 min-w-0 rounded-md bg-muted p-2 flex flex-col gap-2">
794
- {group.children.map((cond, cIdx) => (
795
- <FilterRow
796
- key={cond.id}
797
- index={cIdx}
798
- condition={cond}
799
- conjunction={group.conjunction}
800
- filterableColumns={filterableColumns}
801
- fieldOptions={fieldOptions}
802
- onChangeConjunction={onChangeGroupConjunction}
803
- onChangeField={(v) => {
804
- const newCol = filterableColumns.find((c) => c.id === v)
805
- onChangeCondition(cond.id, { field: v, op: getDefaultOperator(newCol?.type), value: '', datePrecision: datePrecisionOf(newCol) })
806
- }}
807
- onChangeOp={(v) => onChangeCondition(cond.id, { op: v, value: '' })}
808
- onChangeValue={(v) => onChangeCondition(cond.id, { value: v })}
809
- onRemove={() => onRemoveCondition(cond.id)}
810
- />
811
- ))}
812
- {/* Q9 — text variant 對齊 inline 派 + 視覺輕量 */}
813
- <div className="flex items-center justify-between">
814
- <Button variant="text" size="sm" startIcon={Plus} onClick={onAddCondition}>加入巢狀篩選</Button>
815
- {group.children.length === 0 && (
816
- <Button variant="text" size="sm" startIcon={Trash2} danger onClick={onRemoveGroup}>移除空群組</Button>
817
- )}
818
- </div>
819
- </div>
820
- </div>
821
- )
822
- }