@undevy-org/tablewright 0.3.0 → 0.5.0

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.
@@ -27,8 +27,13 @@ function AppliedStateChips({
27
27
  onClearAll,
28
28
  columnFilterStats,
29
29
  requestOpenFilterId,
30
- onRequestOpenHandled
30
+ onRequestOpenHandled,
31
+ labels: labelOverrides,
32
+ columnFilterLabels,
33
+ compoundFilterLabels,
34
+ filterPopoverFooterLabels
31
35
  }) {
36
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
32
37
  const hasFilters = filters.length > 0;
33
38
  const hasSort = currentSort !== null;
34
39
  const hasHiddenColumns = hiddenColumnsCount > 0;
@@ -39,13 +44,13 @@ function AppliedStateChips({
39
44
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 pb-3", children: [
40
45
  /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: onClearAll, className: "shrink-0", children: [
41
46
  /* @__PURE__ */ jsx(X, { size: 14 }),
42
- DEFAULT_LABELS.clearAll
47
+ labels.clearAll
43
48
  ] }),
44
49
  hasHiddenColumns && /* @__PURE__ */ jsx(
45
50
  FilterChip,
46
51
  {
47
- label: DEFAULT_LABELS.columns,
48
- value: DEFAULT_LABELS.hiddenCount(hiddenColumnsCount),
52
+ label: labels.columns,
53
+ value: labels.hiddenCount(hiddenColumnsCount),
49
54
  onRemove: onResetColumns,
50
55
  onClick: onOpenColumnsMenu,
51
56
  className: "cursor-pointer"
@@ -68,7 +73,9 @@ function AppliedStateChips({
68
73
  chipClassName: isEmpty ? "border-dashed" : void 0,
69
74
  columnFilterStats,
70
75
  requestOpen: filter.columnId === requestOpenFilterId,
71
- onRequestOpenHandled
76
+ onRequestOpenHandled,
77
+ labels: compoundFilterLabels,
78
+ footerLabels: filterPopoverFooterLabels
72
79
  },
73
80
  filter.columnId
74
81
  );
@@ -84,7 +91,9 @@ function AppliedStateChips({
84
91
  chipClassName: isEmpty ? "border-dashed" : void 0,
85
92
  stats: columnFilterStats?.[filter.columnId],
86
93
  requestOpen: filter.columnId === requestOpenFilterId,
87
- onRequestOpenHandled
94
+ onRequestOpenHandled,
95
+ labels: columnFilterLabels,
96
+ footerLabels: filterPopoverFooterLabels
88
97
  },
89
98
  filter.columnId
90
99
  );
@@ -18,6 +18,9 @@ const DEFAULT_LABELS = {
18
18
  chipTo: "to",
19
19
  chipMin: "min",
20
20
  chipMax: "max",
21
+ contains: "Contains",
22
+ orBadge: "OR",
23
+ numberInputPlaceholder: "0",
21
24
  valuesCount: (n) => `${n} values`,
22
25
  selectedCount: (n) => `${n} selected`
23
26
  };
@@ -48,8 +51,11 @@ function ColumnFilterPopover({
48
51
  chipClassName,
49
52
  stats,
50
53
  requestOpen,
51
- onRequestOpenHandled
54
+ onRequestOpenHandled,
55
+ labels: labelOverrides,
56
+ footerLabels
52
57
  }) {
58
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
53
59
  const [isOpen, setIsOpen] = useState(false);
54
60
  const onRequestOpenHandledRef = useRef(onRequestOpenHandled);
55
61
  useEffect(() => {
@@ -186,7 +192,7 @@ function ColumnFilterPopover({
186
192
  if (!textStats || textStats.type !== "text") {
187
193
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 flex-wrap", children: [
188
194
  /* @__PURE__ */ jsx("span", { className: "text-[10px] text-[var(--text-tertiary)] whitespace-nowrap min-w-[54px]", children: label }),
189
- /* @__PURE__ */ jsx("span", { className: "rounded bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[11px] text-[var(--text-tertiary)] italic", children: DEFAULT_LABELS.noData })
195
+ /* @__PURE__ */ jsx("span", { className: "rounded bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[11px] text-[var(--text-tertiary)] italic", children: labels.noData })
190
196
  ] });
191
197
  }
192
198
  const { samples, totalUnique } = textStats;
@@ -216,8 +222,8 @@ function ColumnFilterPopover({
216
222
  const filtText = filteredStats?.type === "text" ? filteredStats : null;
217
223
  return /* @__PURE__ */ jsxs("div", { className: "p-3 w-64 space-y-3", children: [
218
224
  /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: [
219
- "Contains",
220
- draftTags.length >= 2 && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] font-semibold text-[var(--tag-blue-text)] bg-[var(--tag-blue-bg)] rounded px-1 py-0.5 uppercase", children: "OR" })
225
+ labels.contains,
226
+ draftTags.length >= 2 && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] font-semibold text-[var(--tag-blue-text)] bg-[var(--tag-blue-bg)] rounded px-1 py-0.5 uppercase", children: labels.orBadge })
221
227
  ] }),
222
228
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1 rounded-md border border-[var(--border-subtle)] bg-[var(--bg-surface)] px-2 py-1.5 focus-within:border-[var(--border-focus)]", children: [
223
229
  draftTags.map((tag) => /* @__PURE__ */ jsxs(
@@ -244,7 +250,7 @@ function ColumnFilterPopover({
244
250
  {
245
251
  autoFocus: true,
246
252
  className: "flex-1 min-w-[60px] bg-transparent text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-tertiary)]",
247
- placeholder: draftTags.length === 0 ? DEFAULT_LABELS.searchPlaceholder : DEFAULT_LABELS.addValuePlaceholder,
253
+ placeholder: draftTags.length === 0 ? labels.searchPlaceholder : labels.addValuePlaceholder,
248
254
  value: draftInput,
249
255
  onChange: (e) => setDraftInput(e.target.value),
250
256
  onKeyDown: handleTextKeyDown
@@ -252,8 +258,8 @@ function ColumnFilterPopover({
252
258
  )
253
259
  ] }),
254
260
  allText && /* @__PURE__ */ jsxs("div", { className: "border-t border-[var(--border-subtle)] pt-2 space-y-2", children: [
255
- renderTextRow(DEFAULT_LABELS.statsTextAllPrefix, allText),
256
- filteredStats !== null && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderTextRow(DEFAULT_LABELS.statsTextFilteredPrefix, filtText) })
261
+ renderTextRow(labels.statsTextAllPrefix, allText),
262
+ filteredStats !== null && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderTextRow(labels.statsTextFilteredPrefix, filtText) })
257
263
  ] })
258
264
  ] });
259
265
  }
@@ -292,7 +298,7 @@ function ColumnFilterPopover({
292
298
  if (!dateStats || dateStats.type !== "date") {
293
299
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 flex-wrap", children: [
294
300
  /* @__PURE__ */ jsx("span", { className: "text-[10px] text-[var(--text-tertiary)] whitespace-nowrap min-w-[54px]", children: label }),
295
- /* @__PURE__ */ jsx("span", { className: "rounded bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[11px] text-[var(--text-tertiary)] italic", children: DEFAULT_LABELS.noData })
301
+ /* @__PURE__ */ jsx("span", { className: "rounded bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[11px] text-[var(--text-tertiary)] italic", children: labels.noData })
296
302
  ] });
297
303
  }
298
304
  const { min, max } = dateStats;
@@ -320,7 +326,7 @@ function ColumnFilterPopover({
320
326
  className: chipCls(draftDate.from === min),
321
327
  onClick: () => setDraftDate((prev) => ({ ...prev, from: min })),
322
328
  children: [
323
- DEFAULT_LABELS.chipFrom,
329
+ labels.chipFrom,
324
330
  " ",
325
331
  formatChipDate(min)
326
332
  ]
@@ -333,7 +339,7 @@ function ColumnFilterPopover({
333
339
  className: chipCls(draftDate.to === max),
334
340
  onClick: () => setDraftDate((prev) => ({ ...prev, to: max })),
335
341
  children: [
336
- DEFAULT_LABELS.chipTo,
342
+ labels.chipTo,
337
343
  " ",
338
344
  formatChipDate(max)
339
345
  ]
@@ -345,7 +351,7 @@ function ColumnFilterPopover({
345
351
  const filtDate = filteredStats?.type === "date" ? filteredStats : null;
346
352
  return /* @__PURE__ */ jsxs("div", { className: "p-3 w-64 space-y-4", children: [
347
353
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
348
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: DEFAULT_LABELS.chipFrom }),
354
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: labels.chipFrom }),
349
355
  /* @__PURE__ */ jsx(
350
356
  Input,
351
357
  {
@@ -356,7 +362,7 @@ function ColumnFilterPopover({
356
362
  )
357
363
  ] }),
358
364
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
359
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: DEFAULT_LABELS.chipTo }),
365
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: labels.chipTo }),
360
366
  /* @__PURE__ */ jsx(
361
367
  Input,
362
368
  {
@@ -367,8 +373,8 @@ function ColumnFilterPopover({
367
373
  )
368
374
  ] }),
369
375
  allDate && /* @__PURE__ */ jsxs("div", { className: "border-t border-[var(--border-subtle)] pt-2 space-y-2", children: [
370
- renderDateRow(DEFAULT_LABELS.statsAllPrefix, allDate),
371
- filteredStats !== null && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderDateRow(DEFAULT_LABELS.statsFilteredPrefix, filtDate) })
376
+ renderDateRow(labels.statsAllPrefix, allDate),
377
+ filteredStats !== null && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderDateRow(labels.statsFilteredPrefix, filtDate) })
372
378
  ] })
373
379
  ] });
374
380
  }
@@ -377,7 +383,7 @@ function ColumnFilterPopover({
377
383
  if (!numStats || numStats.type !== "number-range") {
378
384
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 flex-wrap", children: [
379
385
  /* @__PURE__ */ jsx("span", { className: "text-[10px] text-[var(--text-tertiary)] whitespace-nowrap min-w-[54px]", children: label }),
380
- /* @__PURE__ */ jsx("span", { className: "rounded bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[11px] text-[var(--text-tertiary)] italic", children: DEFAULT_LABELS.noData })
386
+ /* @__PURE__ */ jsx("span", { className: "rounded bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[11px] text-[var(--text-tertiary)] italic", children: labels.noData })
381
387
  ] });
382
388
  }
383
389
  const { min, max } = numStats;
@@ -408,7 +414,7 @@ function ColumnFilterPopover({
408
414
  className: chipCls(draftNumberFrom === String(min)),
409
415
  onClick: () => setDraftNumberFrom(String(min)),
410
416
  children: [
411
- DEFAULT_LABELS.chipMin,
417
+ labels.chipMin,
412
418
  " ",
413
419
  min
414
420
  ]
@@ -421,7 +427,7 @@ function ColumnFilterPopover({
421
427
  className: chipCls(draftNumberTo === String(max)),
422
428
  onClick: () => setDraftNumberTo(String(max)),
423
429
  children: [
424
- DEFAULT_LABELS.chipMax,
430
+ labels.chipMax,
425
431
  " ",
426
432
  max
427
433
  ]
@@ -433,13 +439,13 @@ function ColumnFilterPopover({
433
439
  const filtNum = filteredStats?.type === "number-range" ? filteredStats : null;
434
440
  return /* @__PURE__ */ jsxs("div", { className: "p-3 w-64 space-y-4", children: [
435
441
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
436
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: DEFAULT_LABELS.chipMin }),
442
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: labels.chipMin }),
437
443
  /* @__PURE__ */ jsx(
438
444
  Input,
439
445
  {
440
446
  type: "number",
441
447
  autoFocus: true,
442
- placeholder: allNum ? String(allNum.min) : "0",
448
+ placeholder: allNum ? String(allNum.min) : labels.numberInputPlaceholder,
443
449
  value: draftNumberFrom,
444
450
  onChange: (e) => setDraftNumberFrom(e.target.value),
445
451
  onKeyDown: (e) => {
@@ -452,12 +458,12 @@ function ColumnFilterPopover({
452
458
  )
453
459
  ] }),
454
460
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
455
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: DEFAULT_LABELS.chipMax }),
461
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider block", children: labels.chipMax }),
456
462
  /* @__PURE__ */ jsx(
457
463
  Input,
458
464
  {
459
465
  type: "number",
460
- placeholder: allNum ? String(allNum.max) : "0",
466
+ placeholder: allNum ? String(allNum.max) : labels.numberInputPlaceholder,
461
467
  value: draftNumberTo,
462
468
  onChange: (e) => setDraftNumberTo(e.target.value),
463
469
  onKeyDown: (e) => {
@@ -470,8 +476,8 @@ function ColumnFilterPopover({
470
476
  )
471
477
  ] }),
472
478
  allNum && /* @__PURE__ */ jsxs("div", { className: "border-t border-[var(--border-subtle)] pt-2 space-y-2", children: [
473
- renderNumRow(DEFAULT_LABELS.statsAllPrefix, allNum),
474
- filteredStats !== null && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderNumRow(DEFAULT_LABELS.statsFilteredPrefix, filtNum) })
479
+ renderNumRow(labels.statsAllPrefix, allNum),
480
+ filteredStats !== null && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderNumRow(labels.statsFilteredPrefix, filtNum) })
475
481
  ] })
476
482
  ] });
477
483
  }
@@ -488,21 +494,21 @@ function ColumnFilterPopover({
488
494
  }
489
495
  if (value.length === 2) {
490
496
  const joined = value.join(", ");
491
- return joined.length > 24 ? `${value.length} values` : joined;
497
+ return joined.length > 24 ? labels.valuesCount(value.length) : joined;
492
498
  }
493
- return DEFAULT_LABELS.valuesCount(value.length);
499
+ return labels.valuesCount(value.length);
494
500
  }
495
501
  if (config.type === "enum" && Array.isArray(value)) {
496
502
  if (value.length === 1) {
497
503
  return config.options?.find((o) => o.value === value[0])?.label ?? value[0];
498
504
  }
499
- return DEFAULT_LABELS.selectedCount(value.length);
505
+ return labels.selectedCount(value.length);
500
506
  }
501
507
  if (config.type === "date") {
502
508
  const { from, to } = getDateRange(value);
503
509
  if (from && to) return `${from} — ${to}`;
504
- if (from) return `${DEFAULT_LABELS.chipFrom} ${from}`;
505
- if (to) return `${DEFAULT_LABELS.chipTo} ${to}`;
510
+ if (from) return `${labels.chipFrom} ${from}`;
511
+ if (to) return `${labels.chipTo} ${to}`;
506
512
  }
507
513
  if (config.type === "number-range") {
508
514
  const { from, to } = getNumberRange(value);
@@ -531,7 +537,14 @@ function ColumnFilterPopover({
531
537
  className: "w-auto p-0 [box-shadow:var(--shadow-menu)] border-[var(--border-subtle)] flex flex-col max-h-[min(var(--radix-popover-content-available-height),520px)] overflow-hidden",
532
538
  children: [
533
539
  /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-y-auto overscroll-contain", children: renderContent() }),
534
- /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 bg-[var(--bg-surface)]", children: /* @__PURE__ */ jsx(FilterPopoverFooter, { onApply: handleApply, onClear: handleClear }) })
540
+ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 bg-[var(--bg-surface)]", children: /* @__PURE__ */ jsx(
541
+ FilterPopoverFooter,
542
+ {
543
+ onApply: handleApply,
544
+ onClear: handleClear,
545
+ labels: footerLabels
546
+ }
547
+ ) })
535
548
  ]
536
549
  }
537
550
  )
@@ -10,10 +10,16 @@ const DEFAULT_LABELS = {
10
10
  searchPlaceholder: "Search...",
11
11
  addPlaceholder: "Add...",
12
12
  contains: "Contains",
13
+ orBadge: "OR",
13
14
  chipMin: "min",
14
15
  chipMax: "max",
15
16
  statsAllPrefix: "All:",
16
- statsFilteredPrefix: "Filt:"
17
+ statsFilteredPrefix: "Filt:",
18
+ numberInputPlaceholder: "0",
19
+ resetSectionTitle: (subLabel) => `Reset ${subLabel}`,
20
+ filtersCount: (n) => `${n} filters`,
21
+ subValuesCount: (n) => `${n} values`,
22
+ subSelectedCount: (n) => `${n} selected`
17
23
  };
18
24
  function initDraft(sub, compound) {
19
25
  const val = compound[sub.key];
@@ -48,7 +54,7 @@ function draftToValue(draft) {
48
54
  const all = trimmed.length > 0 && !draft.tags.includes(trimmed) ? [...draft.tags, trimmed] : [...draft.tags];
49
55
  return all.length > 0 ? all : void 0;
50
56
  }
51
- function formatSubDisplay(sub, val) {
57
+ function formatSubDisplay(sub, val, labels) {
52
58
  if (isFilterValueEmpty(val)) return null;
53
59
  if (sub.type === "number-range") {
54
60
  const { from, to } = getNumberRange(val);
@@ -62,14 +68,14 @@ function formatSubDisplay(sub, val) {
62
68
  const label = sub.options?.find((o) => o.value === val[0])?.label ?? val[0];
63
69
  return `${sub.label}: ${label}`;
64
70
  }
65
- return `${sub.label}: ${val.length} selected`;
71
+ return `${sub.label}: ${labels.subSelectedCount(val.length)}`;
66
72
  }
67
73
  if (Array.isArray(val) && val.length > 0) {
68
74
  if (val.length === 1) {
69
75
  const display = val[0].length > 15 ? `${val[0].slice(0, 15)}…` : val[0];
70
76
  return `${sub.label}: ${display}`;
71
77
  }
72
- return `${sub.label}: ${val.length} values`;
78
+ return `${sub.label}: ${labels.subValuesCount(val.length)}`;
73
79
  }
74
80
  return null;
75
81
  }
@@ -82,8 +88,11 @@ function CompoundFilterPopover({
82
88
  chipClassName,
83
89
  columnFilterStats,
84
90
  requestOpen,
85
- onRequestOpenHandled
91
+ onRequestOpenHandled,
92
+ labels: labelOverrides,
93
+ footerLabels
86
94
  }) {
95
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
87
96
  const subFilters = config.subFilters ?? [];
88
97
  const compound = getCompoundValue(filter.value);
89
98
  const [isOpen, setIsOpen] = useState(false);
@@ -159,12 +168,12 @@ function CompoundFilterPopover({
159
168
  if (isFilterValueEmpty(compound)) return void 0;
160
169
  const parts = [];
161
170
  for (const sub of subFilters) {
162
- const display = formatSubDisplay(sub, compound[sub.key]);
171
+ const display = formatSubDisplay(sub, compound[sub.key], labels);
163
172
  if (display) parts.push(display);
164
173
  }
165
174
  if (parts.length === 0) return void 0;
166
175
  const joined = parts.join(" & ");
167
- return joined.length > 30 ? `${parts.length} filters` : joined;
176
+ return joined.length > 30 ? labels.filtersCount(parts.length) : joined;
168
177
  };
169
178
  const updateDraft = (key, updater) => {
170
179
  setDrafts((prev) => ({ ...prev, [key]: updater(prev[key]) }));
@@ -187,7 +196,7 @@ function CompoundFilterPopover({
187
196
  className: chipCls(draft.from === String(min)),
188
197
  onClick: () => updateDraft(sub.key, (d) => ({ ...d, from: String(min) })),
189
198
  children: [
190
- DEFAULT_LABELS.chipMin,
199
+ labels.chipMin,
191
200
  " ",
192
201
  min.toLocaleString()
193
202
  ]
@@ -200,7 +209,7 @@ function CompoundFilterPopover({
200
209
  className: chipCls(draft.to === String(max)),
201
210
  onClick: () => updateDraft(sub.key, (d) => ({ ...d, to: String(max) })),
202
211
  children: [
203
- DEFAULT_LABELS.chipMax,
212
+ labels.chipMax,
204
213
  " ",
205
214
  max.toLocaleString()
206
215
  ]
@@ -211,13 +220,13 @@ function CompoundFilterPopover({
211
220
  return /* @__PURE__ */ jsxs(Fragment, { children: [
212
221
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
213
222
  /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-1", children: [
214
- /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--text-tertiary)] block", children: DEFAULT_LABELS.chipMin }),
223
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--text-tertiary)] block", children: labels.chipMin }),
215
224
  /* @__PURE__ */ jsx(
216
225
  Input,
217
226
  {
218
227
  type: "number",
219
228
  autoFocus: subFilters.indexOf(sub) === 0,
220
- placeholder: allNum ? String(allNum.min) : "0",
229
+ placeholder: allNum ? String(allNum.min) : labels.numberInputPlaceholder,
221
230
  value: draft.from,
222
231
  onChange: (e) => updateDraft(sub.key, (prev) => ({
223
232
  ...prev,
@@ -233,12 +242,12 @@ function CompoundFilterPopover({
233
242
  )
234
243
  ] }),
235
244
  /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-1", children: [
236
- /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--text-tertiary)] block", children: DEFAULT_LABELS.chipMax }),
245
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--text-tertiary)] block", children: labels.chipMax }),
237
246
  /* @__PURE__ */ jsx(
238
247
  Input,
239
248
  {
240
249
  type: "number",
241
- placeholder: allNum ? String(allNum.max) : "0",
250
+ placeholder: allNum ? String(allNum.max) : labels.numberInputPlaceholder,
242
251
  value: draft.to,
243
252
  onChange: (e) => updateDraft(sub.key, (prev) => ({
244
253
  ...prev,
@@ -255,8 +264,8 @@ function CompoundFilterPopover({
255
264
  ] })
256
265
  ] }),
257
266
  allNum && /* @__PURE__ */ jsxs("div", { className: "border-t border-[var(--border-subtle)] mt-3 pt-2 space-y-2", children: [
258
- renderNumRow(DEFAULT_LABELS.statsAllPrefix, allNum),
259
- filtNum && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderNumRow(DEFAULT_LABELS.statsFilteredPrefix, filtNum) })
267
+ renderNumRow(labels.statsAllPrefix, allNum),
268
+ filtNum && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderNumRow(labels.statsFilteredPrefix, filtNum) })
260
269
  ] })
261
270
  ] });
262
271
  };
@@ -353,8 +362,8 @@ function CompoundFilterPopover({
353
362
  };
354
363
  return /* @__PURE__ */ jsxs(Fragment, { children: [
355
364
  /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--text-tertiary)] block mb-2", children: [
356
- DEFAULT_LABELS.contains,
357
- draft.tags.length >= 2 && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] font-semibold text-[var(--tag-blue-text)] bg-[var(--tag-blue-bg)] rounded px-1 py-0.5 uppercase", children: "OR" })
365
+ labels.contains,
366
+ draft.tags.length >= 2 && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] font-semibold text-[var(--tag-blue-text)] bg-[var(--tag-blue-bg)] rounded px-1 py-0.5 uppercase", children: labels.orBadge })
358
367
  ] }),
359
368
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1 rounded-md border border-[var(--border-subtle)] bg-[var(--bg-surface)] px-2 py-1.5 focus-within:border-[var(--border-focus)]", children: [
360
369
  draft.tags.map((tag) => /* @__PURE__ */ jsxs(
@@ -384,7 +393,7 @@ function CompoundFilterPopover({
384
393
  {
385
394
  autoFocus: subFilters.indexOf(sub) === 0,
386
395
  className: "flex-1 min-w-[50px] bg-transparent text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-tertiary)]",
387
- placeholder: draft.tags.length === 0 ? DEFAULT_LABELS.searchPlaceholder : DEFAULT_LABELS.addPlaceholder,
396
+ placeholder: draft.tags.length === 0 ? labels.searchPlaceholder : labels.addPlaceholder,
388
397
  value: draft.input,
389
398
  onChange: (e) => updateDraft(sub.key, (prev) => ({ ...prev, input: e.target.value })),
390
399
  onKeyDown: handleTextKeyDown
@@ -392,8 +401,8 @@ function CompoundFilterPopover({
392
401
  )
393
402
  ] }),
394
403
  allText && /* @__PURE__ */ jsxs("div", { className: "border-t border-[var(--border-subtle)] mt-3 pt-2 space-y-2", children: [
395
- renderTextRow(DEFAULT_LABELS.statsAllPrefix, allText),
396
- filtText && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderTextRow(DEFAULT_LABELS.statsFilteredPrefix, filtText) })
404
+ renderTextRow(labels.statsAllPrefix, allText),
405
+ filtText && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderTextRow(labels.statsFilteredPrefix, filtText) })
397
406
  ] })
398
407
  ] });
399
408
  };
@@ -475,7 +484,7 @@ function CompoundFilterPopover({
475
484
  type: "button",
476
485
  className: "text-[11px] text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] transition-colors",
477
486
  onClick: () => handleResetSection(sub.key),
478
- title: `Reset ${sub.label}`,
487
+ title: labels.resetSectionTitle(sub.label),
479
488
  children: "✕"
480
489
  }
481
490
  )
@@ -487,7 +496,14 @@ function CompoundFilterPopover({
487
496
  },
488
497
  sub.key
489
498
  )) }),
490
- /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 bg-[var(--bg-surface)]", children: /* @__PURE__ */ jsx(FilterPopoverFooter, { onApply: handleApply, onClear: handleClearAll }) })
499
+ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 bg-[var(--bg-surface)]", children: /* @__PURE__ */ jsx(
500
+ FilterPopoverFooter,
501
+ {
502
+ onApply: handleApply,
503
+ onClear: handleClearAll,
504
+ labels: footerLabels
505
+ }
506
+ ) })
491
507
  ]
492
508
  }
493
509
  )
@@ -8,6 +8,21 @@ import { Input } from "../ui/input.js";
8
8
  import { Combobox } from "../ui/combobox.js";
9
9
  import { isFilterValueEmpty, getDateRange, getNumberRange, getCompoundValue } from "./filter-values.js";
10
10
  import { hasActiveManagedFilters } from "./managed-filters.js";
11
+ const DEFAULT_LABELS = {
12
+ closeFiltersAriaLabel: "Close filters",
13
+ closeFiltersTitle: "Close filters",
14
+ apply: "Apply",
15
+ clear: "Clear",
16
+ anyPlaceholder: "Any",
17
+ rangeFromPlaceholder: "From",
18
+ rangeToPlaceholder: "To",
19
+ fieldRangeFromAriaLabel: (fieldLabel) => `${fieldLabel} — from`,
20
+ fieldRangeToAriaLabel: (fieldLabel) => `${fieldLabel} — to`,
21
+ compoundSubRangeFromAriaLabel: (subLabel) => `${subLabel} — from`,
22
+ compoundSubRangeToAriaLabel: (subLabel) => `${subLabel} — to`,
23
+ compoundSubRangeFromPlaceholder: (subLabel) => `${subLabel} from`,
24
+ compoundSubRangeToPlaceholder: (subLabel) => `${subLabel} to`
25
+ };
11
26
  function deriveInitialState(fields, filters) {
12
27
  const byId = new Map(filters.map((f) => [f.columnId, f.value]));
13
28
  const next = {};
@@ -24,8 +39,10 @@ function FilterFormPanel({
24
39
  className,
25
40
  id,
26
41
  onClose,
27
- hiddenSubFilterKeys
42
+ hiddenSubFilterKeys,
43
+ labels: labelOverrides
28
44
  }) {
45
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
29
46
  const hiddenSubSet = useMemo(() => new Set(hiddenSubFilterKeys ?? []), [hiddenSubFilterKeys]);
30
47
  const renderableFields = useMemo(
31
48
  () => fields.filter((id2) => {
@@ -100,8 +117,8 @@ function FilterFormPanel({
100
117
  type: "button",
101
118
  variant: "ghost",
102
119
  size: "icon",
103
- "aria-label": "Close filters",
104
- title: "Close filters",
120
+ "aria-label": labels.closeFiltersAriaLabel,
121
+ title: labels.closeFiltersTitle,
105
122
  onClick: onClose,
106
123
  className: "h-7 w-7",
107
124
  children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
@@ -118,7 +135,8 @@ function FilterFormPanel({
118
135
  value: state[fieldId],
119
136
  onChange: (v) => setField(fieldId, v),
120
137
  fieldId,
121
- hiddenSubSet
138
+ hiddenSubSet,
139
+ labels
122
140
  },
123
141
  fieldId
124
142
  );
@@ -132,16 +150,16 @@ function FilterFormPanel({
132
150
  size: "sm",
133
151
  onClick: handleClear,
134
152
  disabled: isClearDisabled,
135
- children: "Clear"
153
+ children: labels.clear
136
154
  }
137
155
  ),
138
- /* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", children: "Apply" })
156
+ /* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", children: labels.apply })
139
157
  ] }) })
140
158
  ]
141
159
  }
142
160
  );
143
161
  }
144
- function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
162
+ function Field({ config, value, onChange, fieldId, hiddenSubSet, labels }) {
145
163
  if (config.type === "text") {
146
164
  const text = Array.isArray(value) ? value[0] ?? "" : "";
147
165
  return /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 flex-col gap-1.5", children: [
@@ -170,7 +188,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
170
188
  value: selected || null,
171
189
  onChange: (v) => onChange(v ? [v] : void 0),
172
190
  options: config.options ?? [],
173
- placeholder: "Any",
191
+ placeholder: labels.anyPlaceholder,
174
192
  triggerAriaLabel: config.label,
175
193
  triggerClassName: "h-9 min-w-0"
176
194
  }
@@ -191,7 +209,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
191
209
  Input,
192
210
  {
193
211
  type: "date",
194
- "aria-label": `${config.label} — from`,
212
+ "aria-label": labels.fieldRangeFromAriaLabel(config.label),
195
213
  value: range.from ?? "",
196
214
  onChange: update("from"),
197
215
  className: "h-9 min-w-0 flex-1"
@@ -201,7 +219,7 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
201
219
  Input,
202
220
  {
203
221
  type: "date",
204
- "aria-label": `${config.label} — to`,
222
+ "aria-label": labels.fieldRangeToAriaLabel(config.label),
205
223
  value: range.to ?? "",
206
224
  onChange: update("to"),
207
225
  className: "h-9 min-w-0 flex-1"
@@ -225,10 +243,10 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
225
243
  Input,
226
244
  {
227
245
  type: "number",
228
- "aria-label": `${config.label} — from`,
246
+ "aria-label": labels.fieldRangeFromAriaLabel(config.label),
229
247
  value: range.from ?? "",
230
248
  onChange: update("from"),
231
- placeholder: "From",
249
+ placeholder: labels.rangeFromPlaceholder,
232
250
  className: "h-9 min-w-0 flex-1"
233
251
  }
234
252
  ),
@@ -236,10 +254,10 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
236
254
  Input,
237
255
  {
238
256
  type: "number",
239
- "aria-label": `${config.label} — to`,
257
+ "aria-label": labels.fieldRangeToAriaLabel(config.label),
240
258
  value: range.to ?? "",
241
259
  onChange: update("to"),
242
- placeholder: "To",
260
+ placeholder: labels.rangeToPlaceholder,
243
261
  className: "h-9 min-w-0 flex-1"
244
262
  }
245
263
  )
@@ -319,10 +337,10 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
319
337
  Input,
320
338
  {
321
339
  type: "number",
322
- "aria-label": `${sub.label} — from`,
340
+ "aria-label": labels.compoundSubRangeFromAriaLabel(sub.label),
323
341
  value: range.from ?? "",
324
342
  onChange: updateRange("from"),
325
- placeholder: `${sub.label} from`,
343
+ placeholder: labels.compoundSubRangeFromPlaceholder(sub.label),
326
344
  className: "h-9 min-w-0 flex-1"
327
345
  }
328
346
  ),
@@ -330,10 +348,10 @@ function Field({ config, value, onChange, fieldId, hiddenSubSet }) {
330
348
  Input,
331
349
  {
332
350
  type: "number",
333
- "aria-label": `${sub.label} — to`,
351
+ "aria-label": labels.compoundSubRangeToAriaLabel(sub.label),
334
352
  value: range.to ?? "",
335
353
  onChange: updateRange("to"),
336
- placeholder: `${sub.label} to`,
354
+ placeholder: labels.compoundSubRangeToPlaceholder(sub.label),
337
355
  className: "h-9 min-w-0 flex-1"
338
356
  }
339
357
  )
@@ -2,16 +2,24 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../../lib/utils.js";
3
3
  import { Button } from "../ui/button.js";
4
4
  import { ButtonFooter } from "../ui/button-footer.js";
5
+ const DEFAULT_LABELS = {
6
+ apply: "Apply",
7
+ clear: "Clear"
8
+ };
5
9
  function FilterPopoverFooter({
6
10
  onApply,
7
11
  onClear,
8
- applyLabel = "Apply",
9
- clearLabel = "Clear",
12
+ labels: labelOverrides,
13
+ applyLabel,
14
+ clearLabel,
10
15
  className
11
16
  }) {
17
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
18
+ const apply = applyLabel ?? labels.apply;
19
+ const clear = clearLabel ?? labels.clear;
12
20
  return /* @__PURE__ */ jsxs(ButtonFooter, { className: cn("px-3 py-2.5", className), children: [
13
- onClear && /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onClear, children: clearLabel }),
14
- /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: onApply, children: applyLabel })
21
+ onClear && /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onClear, children: clear }),
22
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: onApply, children: apply })
15
23
  ] });
16
24
  }
17
25
  export {