torch-glare 2.4.4 → 2.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.
Files changed (147) hide show
  1. package/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
  2. package/apps/lib/components/DataViews/cell.tsx +324 -0
  3. package/apps/lib/components/DataViews/context.ts +144 -0
  4. package/apps/lib/components/DataViews/data-views.tsx +383 -0
  5. package/apps/lib/components/DataViews/filters/children.tsx +98 -0
  6. package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
  7. package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
  8. package/apps/lib/components/DataViews/filters/index.ts +4 -0
  9. package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
  10. package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
  11. package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
  12. package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
  13. package/apps/lib/components/DataViews/filters/values.ts +173 -0
  14. package/apps/lib/components/DataViews/header.tsx +217 -0
  15. package/apps/lib/components/DataViews/hooks/index.ts +5 -0
  16. package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
  17. package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
  18. package/apps/lib/components/DataViews/index.ts +74 -26
  19. package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
  20. package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
  21. package/apps/lib/components/DataViews/panel/index.ts +3 -0
  22. package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
  23. package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
  24. package/apps/lib/components/DataViews/panel/section.tsx +79 -0
  25. package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
  26. package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
  27. package/apps/lib/components/DataViews/slots.ts +63 -0
  28. package/apps/lib/components/DataViews/states.tsx +38 -0
  29. package/apps/lib/components/DataViews/types.ts +485 -178
  30. package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
  31. package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
  32. package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
  33. package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
  34. package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
  35. package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
  36. package/apps/lib/components/FormBuilder/context.ts +20 -6
  37. package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
  38. package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
  39. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
  40. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
  41. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
  42. package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
  43. package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
  44. package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
  45. package/apps/lib/components/FormBuilder/index.ts +3 -1
  46. package/apps/lib/components/FormBuilder/types.ts +40 -0
  47. package/apps/lib/components/Input.tsx +3 -0
  48. package/apps/lib/components/SearchableTable.tsx +5 -4
  49. package/apps/lib/components/SectionBlock.tsx +58 -11
  50. package/apps/lib/components/Select.tsx +3 -1
  51. package/apps/lib/components/TabSwitch.tsx +16 -4
  52. package/apps/lib/components/Table.tsx +265 -67
  53. package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
  54. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
  55. package/apps/lib/components/TreeFolder/index.ts +1 -1
  56. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
  57. package/apps/lib/hooks/useDragDrop.tsx +365 -0
  58. package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
  59. package/apps/lib/registry.json +159 -4
  60. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  61. package/apps/lib/utils/dataViews/path.ts +67 -0
  62. package/apps/lib/utils/dataViews/query.ts +73 -0
  63. package/apps/lib/utils/dataViews/types.ts +187 -0
  64. package/docs/components/breadcrumb.md +1 -1
  65. package/docs/components/button-group.md +1 -1
  66. package/docs/components/button.md +1 -1
  67. package/docs/components/card.md +1 -1
  68. package/docs/components/checkbox.md +1 -1
  69. package/docs/components/data-views/backend-response.md +324 -0
  70. package/docs/components/data-views/examples/a11y-rtl.md +250 -0
  71. package/docs/components/data-views/examples/api-orders-route.md +130 -0
  72. package/docs/components/data-views/examples/fields.md +362 -0
  73. package/docs/components/data-views/examples/filters.md +308 -0
  74. package/docs/components/data-views/examples/inbox-routing.md +218 -0
  75. package/docs/components/data-views/examples/index.md +29 -0
  76. package/docs/components/data-views/examples/overview.md +244 -0
  77. package/docs/components/data-views/examples/panel.md +212 -0
  78. package/docs/components/data-views/examples/scale.md +231 -0
  79. package/docs/components/data-views/examples/server-side.md +210 -0
  80. package/docs/components/data-views/examples/state.md +250 -0
  81. package/docs/components/data-views/examples/tree-custom.md +388 -0
  82. package/docs/components/data-views/examples/view-registry.md +313 -0
  83. package/docs/components/data-views/examples/views.md +534 -0
  84. package/docs/components/data-views/guide.md +405 -0
  85. package/docs/components/data-views/index.md +1504 -0
  86. package/docs/components/data-views/migration.md +79 -0
  87. package/docs/components/date-picker.md +0 -1
  88. package/docs/components/form-builder.md +19 -8
  89. package/docs/components/form-renderer.md +2 -1
  90. package/docs/components/form.md +1 -1
  91. package/docs/components/input-field.md +1 -1
  92. package/docs/components/input-otp.md +1 -1
  93. package/docs/components/input.md +1 -1
  94. package/docs/components/labeled-check-box.md +1 -1
  95. package/docs/components/labeled-radio.md +1 -1
  96. package/docs/components/radio-card.md +1 -1
  97. package/docs/components/radio.md +1 -1
  98. package/docs/components/search-field.md +1 -1
  99. package/docs/components/section-block.md +79 -3
  100. package/docs/components/select.md +1 -1
  101. package/docs/components/simple-select.md +1 -1
  102. package/docs/components/switch.md +1 -1
  103. package/docs/components/tab-switch.md +1 -1
  104. package/docs/components/table.md +45 -8
  105. package/docs/components/text-editor.md +1 -1
  106. package/docs/components/textarea.md +1 -1
  107. package/docs/components/toggle-button.md +1 -1
  108. package/docs/components/toggle.md +1 -1
  109. package/docs/components/tree-folder.md +110 -0
  110. package/docs/how-to/forms-with-form-builder.md +6 -4
  111. package/docs/reference/components.md +16 -6
  112. package/docs/tutorials/component-composition.md +11 -13
  113. package/package.json +3 -2
  114. package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
  115. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
  116. package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
  117. package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
  118. package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
  119. package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
  120. package/apps/lib/components/DataViews/InboxView.tsx +0 -463
  121. package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
  122. package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
  123. package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
  124. package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
  125. package/apps/lib/components/DataViews/TableView.tsx +0 -212
  126. package/apps/lib/components/DataViews/TreeView.tsx +0 -364
  127. package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
  128. package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
  129. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
  130. package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
  131. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
  132. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
  133. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
  134. package/apps/lib/hooks/useDataViewsState.ts +0 -175
  135. package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
  136. package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
  137. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
  138. package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
  139. package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
  140. package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
  141. package/docs/components/data-views-config-panel.md +0 -208
  142. package/docs/components/data-views-layout.md +0 -291
  143. package/docs/components/inbox-view.md +0 -170
  144. package/docs/components/kanban-view.md +0 -135
  145. package/docs/components/table-view.md +0 -141
  146. package/docs/components/tree-view.md +0 -147
  147. package/docs/how-to/data-views-from-backend-response.md +0 -194
@@ -1,299 +0,0 @@
1
- "use client";
2
-
3
- import type { ReactNode } from "react";
4
- import { Badge } from "../Badge";
5
- import { Avatar, AvatarFallback, AvatarImage } from "../Avatar";
6
- import { cn } from "../../utils/cn";
7
- import type { CurrencyOptions, DynamicRecord, FieldConfig, FieldType } from "./types";
8
- import { getByPath } from "../../utils/dataViews/pathUtils";
9
- import { resolveBadgeVariant } from "./badgeAdapter";
10
-
11
- type RenderArgs = {
12
- value: unknown;
13
- field: FieldConfig;
14
- row: DynamicRecord;
15
- };
16
-
17
- const NULL_PLACEHOLDER = <span className="text-content-presentation-global-tertiary">-</span>;
18
-
19
- export function renderField(value: unknown, field: FieldConfig, row: DynamicRecord): ReactNode {
20
- if (field.render) return field.render(value, row);
21
-
22
- const type = field.type ?? "text";
23
- if (type === "hidden") return null;
24
-
25
- if (value == null && type !== "boolean" && type !== "progress-bar") {
26
- return NULL_PLACEHOLDER;
27
- }
28
-
29
- const renderer = RENDERERS[type] ?? renderText;
30
- return renderer({ value, field, row });
31
- }
32
-
33
- function renderText({ value }: RenderArgs): ReactNode {
34
- return <span className="text-content-presentation-global-primary">{String(value)}</span>;
35
- }
36
-
37
- function renderNumber({ value }: RenderArgs): ReactNode {
38
- return (
39
- <span className="font-mono text-content-presentation-global-primary">
40
- {typeof value === "number" ? value.toLocaleString() : String(value)}
41
- </span>
42
- );
43
- }
44
-
45
- function renderDate({ value }: RenderArgs): ReactNode {
46
- return <span className="text-content-presentation-global-primary">{String(value)}</span>;
47
- }
48
-
49
- function renderDateFormat({ value, field }: RenderArgs): ReactNode {
50
- const opts = field.dateFormat;
51
- let formatted = String(value);
52
- try {
53
- const d = value instanceof Date ? value : new Date(value as string | number);
54
- if (!isNaN(d.getTime())) {
55
- if (typeof opts === "object" && opts) {
56
- formatted = new Intl.DateTimeFormat(undefined, opts).format(d);
57
- } else if (typeof opts === "string") {
58
- formatted = formatWithToken(d, opts);
59
- } else {
60
- formatted = new Intl.DateTimeFormat(undefined, {
61
- year: "numeric",
62
- month: "short",
63
- day: "numeric",
64
- }).format(d);
65
- }
66
- }
67
- } catch {
68
- /* fall through to raw string */
69
- }
70
- return <span className="text-content-presentation-global-primary">{formatted}</span>;
71
- }
72
-
73
- function formatWithToken(d: Date, token: string): string {
74
- const pad = (n: number) => String(n).padStart(2, "0");
75
- return token
76
- .replace(/YYYY/g, String(d.getFullYear()))
77
- .replace(/MM/g, pad(d.getMonth() + 1))
78
- .replace(/DD/g, pad(d.getDate()))
79
- .replace(/HH/g, pad(d.getHours()))
80
- .replace(/mm/g, pad(d.getMinutes()))
81
- .replace(/ss/g, pad(d.getSeconds()));
82
- }
83
-
84
- function renderBoolean({ value, field }: RenderArgs): ReactNode {
85
- const isTrue = !!value;
86
- const variant = isTrue ? (field.trueVariant ?? "green") : (field.falseVariant ?? "gray");
87
- const badgeProps = resolveBadgeVariant(variant);
88
- return (
89
- <Badge
90
- {...badgeProps}
91
- label={isTrue ? (field.trueLabel ?? "Yes") : (field.falseLabel ?? "No")}
92
- size="S"
93
- />
94
- );
95
- }
96
-
97
- function renderEnumBadge({ value, field }: RenderArgs): ReactNode {
98
- const key = String(value);
99
- const variant = field.variants?.[key] ?? field.defaultVariant ?? "gray";
100
- const badgeProps = resolveBadgeVariant(variant);
101
- // Use medium size so the glare badge dot indicator is clearly visible
102
- return <Badge {...badgeProps} label={key} size="S" />;
103
- }
104
-
105
- function renderBadgeArray({ value, field }: RenderArgs): ReactNode {
106
- const badgeProps = resolveBadgeVariant(field.variant ?? "blue");
107
- if (!Array.isArray(value)) {
108
- return <Badge {...badgeProps} label={String(value)} size="XS" />;
109
- }
110
- const limit = field.limit ?? value.length;
111
- const head = value.slice(0, limit);
112
- const overflow = value.length - head.length;
113
- const overflowProps = resolveBadgeVariant("gray");
114
- return (
115
- <div className="flex flex-wrap gap-1">
116
- {head.map((v, i) => (
117
- <Badge key={i} {...badgeProps} label={String(v)} size="XS" />
118
- ))}
119
- {overflow > 0 && <Badge {...overflowProps} label={`+${overflow}`} size="XS" />}
120
- </div>
121
- );
122
- }
123
-
124
- function renderCurrency({ value, field }: RenderArgs): ReactNode {
125
- const num = Number(value);
126
- if (Number.isNaN(num)) return renderText({ value, field, row: {} as DynamicRecord });
127
-
128
- const opts: CurrencyOptions =
129
- typeof field.currency === "string" ? { code: field.currency } : (field.currency ?? {});
130
-
131
- let formatted: string;
132
- if (opts.code) {
133
- try {
134
- formatted = new Intl.NumberFormat(opts.locale, {
135
- style: "currency",
136
- currency: opts.code,
137
- minimumFractionDigits: opts.decimals,
138
- maximumFractionDigits: opts.decimals,
139
- }).format(num);
140
- } catch {
141
- formatted = `${opts.symbol ?? "$"}${num.toLocaleString(opts.locale, {
142
- minimumFractionDigits: opts.decimals,
143
- maximumFractionDigits: opts.decimals,
144
- })}`;
145
- }
146
- } else {
147
- formatted = `${opts.symbol ?? "$"}${num.toLocaleString(opts.locale, {
148
- minimumFractionDigits: opts.decimals,
149
- maximumFractionDigits: opts.decimals,
150
- })}`;
151
- }
152
-
153
- return <span className="font-semibold text-green-600">{formatted}</span>;
154
- }
155
-
156
- function renderNumberFormat({ value, field }: RenderArgs): ReactNode {
157
- const num = Number(value);
158
- if (Number.isNaN(num)) return renderText({ value, field, row: {} as DynamicRecord });
159
- const formatted = new Intl.NumberFormat(undefined, field.format).format(num);
160
- return <span className="font-mono text-content-presentation-global-primary">{formatted}</span>;
161
- }
162
-
163
- function renderProgressBar({ value, field }: RenderArgs): ReactNode {
164
- const raw = Number(value);
165
- const num = Number.isFinite(raw) ? raw : 0;
166
- const pct = Math.max(0, Math.min(100, num));
167
- const [warn, ok] = field.thresholds ?? [40, 70];
168
- const color = num >= ok ? "bg-green-500" : num >= warn ? "bg-yellow-500" : "bg-red-500";
169
-
170
- return (
171
- <div className="flex items-center gap-2 min-w-[120px]">
172
- <div className="flex-1 h-2 rounded-full bg-background-presentation-form-field-primary overflow-hidden">
173
- <div className={cn("h-full transition-all", color)} style={{ width: `${pct}%` }} />
174
- </div>
175
- <span className="text-xs font-medium tabular-nums text-content-presentation-global-primary w-10 text-right">
176
- {Math.round(num)}%
177
- </span>
178
- </div>
179
- );
180
- }
181
-
182
- function renderStarRating({ value, field }: RenderArgs): ReactNode {
183
- const num = Number(value);
184
- const max = field.max ?? 5;
185
- const filled = Math.max(0, Math.min(max, Math.floor(num)));
186
-
187
- return (
188
- <div className="flex items-center gap-2">
189
- <div className="flex">
190
- {Array.from({ length: max }).map((_, i) => (
191
- <span key={i} className={i < filled ? "text-yellow-500" : "text-gray-300"} aria-hidden>
192
-
193
- </span>
194
- ))}
195
- </div>
196
- <span className="text-sm font-semibold tabular-nums">{Number.isFinite(num) ? num : "-"}</span>
197
- </div>
198
- );
199
- }
200
-
201
- function renderIconText({ value, field }: RenderArgs): ReactNode {
202
- const icon = field.icon ?? "";
203
- const after = field.iconPosition === "after";
204
- return (
205
- <span className="inline-flex items-center gap-1.5 text-content-presentation-global-primary">
206
- {!after && icon && <IconNode icon={icon} />}
207
- <span>{String(value)}</span>
208
- {after && icon && <IconNode icon={icon} />}
209
- </span>
210
- );
211
- }
212
-
213
- function IconNode({ icon }: { icon: string }) {
214
- if (/^ri-/.test(icon)) return <i className={icon} />;
215
- return <span aria-hidden>{icon}</span>;
216
- }
217
-
218
- function renderTwoLine({ value, field, row }: RenderArgs): ReactNode {
219
- const secondary = field.secondaryPath != null ? getByPath(row, field.secondaryPath) : null;
220
- return (
221
- <div className="leading-tight">
222
- <div className="font-semibold text-content-presentation-global-primary">{String(value)}</div>
223
- {secondary != null && (
224
- <div className="text-xs text-content-presentation-global-secondary">
225
- {String(secondary)}
226
- </div>
227
- )}
228
- </div>
229
- );
230
- }
231
-
232
- function renderAvatar({ value, field, row }: RenderArgs): ReactNode {
233
- const src = typeof value === "string" ? value : null;
234
- const fallbackSource = field.fallbackPath != null ? getByPath(row, field.fallbackPath) : null;
235
- const initials = toInitials(fallbackSource ?? src ?? "?");
236
-
237
- return (
238
- <Avatar>
239
- {src && <AvatarImage src={src} alt={String(initials)} />}
240
- <AvatarFallback>{initials}</AvatarFallback>
241
- </Avatar>
242
- );
243
- }
244
-
245
- function toInitials(s: string): string {
246
- if (!s) return "?";
247
- const parts = String(s).trim().split(/\s+/).slice(0, 2);
248
- return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
249
- }
250
-
251
- function renderLink({ value, field }: RenderArgs): ReactNode {
252
- const v = String(value);
253
- let href = v;
254
- if (field.linkType === "mailto" && !v.startsWith("mailto:")) href = `mailto:${v}`;
255
- else if (field.linkType === "tel" && !v.startsWith("tel:")) href = `tel:${v}`;
256
-
257
- return (
258
- <a
259
- href={href}
260
- target={field.linkType === "url" ? "_blank" : undefined}
261
- rel={field.linkType === "url" ? "noopener noreferrer" : undefined}
262
- className="text-blue-600 hover:underline"
263
- onClick={(e) => e.stopPropagation()}
264
- >
265
- {v}
266
- </a>
267
- );
268
- }
269
-
270
- function renderImage({ value }: RenderArgs): ReactNode {
271
- if (typeof value !== "string" || !value) return NULL_PLACEHOLDER;
272
- return (
273
- <img
274
- src={value}
275
- alt=""
276
- className="h-10 w-10 rounded object-cover border border-border-presentation-global-primary"
277
- />
278
- );
279
- }
280
-
281
- const RENDERERS: Record<FieldType, (a: RenderArgs) => ReactNode> = {
282
- text: renderText,
283
- number: renderNumber,
284
- date: renderDate,
285
- boolean: renderBoolean,
286
- hidden: () => null,
287
- "enum-badge": renderEnumBadge,
288
- "badge-array": renderBadgeArray,
289
- currency: renderCurrency,
290
- "number-format": renderNumberFormat,
291
- "progress-bar": renderProgressBar,
292
- "star-rating": renderStarRating,
293
- "icon-text": renderIconText,
294
- "two-line": renderTwoLine,
295
- avatar: renderAvatar,
296
- link: renderLink,
297
- image: renderImage,
298
- "date-format": renderDateFormat,
299
- };
@@ -1,87 +0,0 @@
1
- "use client";
2
-
3
- import { type ChangeEventHandler } from "react";
4
- import { DatePicker } from "../../DatePicker";
5
- import { InputField } from "../../InputField";
6
- import { ActionButton } from "../../ActionButton";
7
- import type { DateRangeFilter } from "../types";
8
- import { toIsoDate } from "../../../utils/dataViews/rangeUtils";
9
-
10
- type Props = {
11
- value: DateRangeFilter | undefined;
12
- onChange: (next: DateRangeFilter) => void;
13
- };
14
-
15
- /** Parse an ISO `yyyy-MM-dd` string to a local Date (noon-safe, no TZ drift). */
16
- function parseIso(iso?: string): Date | undefined {
17
- return iso ? new Date(iso + "T00:00:00") : undefined;
18
- }
19
-
20
- /** One labeled single-date Glare DatePicker bound to one end of the range. */
21
- function DateBound({
22
- caption,
23
- iso,
24
- placeholder,
25
- onPick,
26
- }: {
27
- caption: string;
28
- iso?: string;
29
- placeholder: string;
30
- onPick: (next?: string) => void;
31
- }) {
32
- return (
33
- <label className="flex items-center gap-2">
34
- <span className="w-9 shrink-0 typography-body-small-regular text-content-presentation-global-secondary">
35
- {caption}
36
- </span>
37
- <div className="flex-1 min-w-0">
38
- <DatePicker
39
- mode="single"
40
- size="S"
41
- value={parseIso(iso) ?? (undefined as never)}
42
- dateFormat="yyyy-MM-dd"
43
- // Single mode reports `{ target: { value: Date } }` (it casts internally).
44
- onChange={
45
- ((e: { target: { value: Date | undefined } }) => {
46
- const d = e?.target?.value;
47
- onPick(d instanceof Date ? toIsoDate(d) : undefined);
48
- }) as unknown as ChangeEventHandler<HTMLInputElement>
49
- }
50
- >
51
- <InputField
52
- readOnly
53
- value={iso ?? ""}
54
- placeholder={placeholder}
55
- childrenSide={
56
- <ActionButton type="button" size="S" aria-label={`Pick ${caption} date`}>
57
- <i className="ri-calendar-event-line" />
58
- </ActionButton>
59
- }
60
- />
61
- </DatePicker>
62
- </div>
63
- </label>
64
- );
65
- }
66
-
67
- /**
68
- * Date-range filter as two separate Glare DatePickers — a "From" and a "To"
69
- * single-date picker. Each writes only its own bound, preserving the other, so
70
- * the result is still a `DateRangeFilter` ({ from, to }).
71
- */
72
- export function DatePickerRangeFilter({ value, onChange }: Props) {
73
- const setBound = (key: "from" | "to") => (next?: string) =>
74
- onChange({ kind: "date", from: value?.from, to: value?.to, [key]: next });
75
-
76
- return (
77
- <div className="space-y-2">
78
- <DateBound
79
- caption="From"
80
- iso={value?.from}
81
- placeholder="Start date"
82
- onPick={setBound("from")}
83
- />
84
- <DateBound caption="To" iso={value?.to} placeholder="End date" onPick={setBound("to")} />
85
- </div>
86
- );
87
- }
@@ -1,120 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import { DayPicker, type DateRange } from "react-day-picker";
5
- import "react-day-picker/style.css";
6
- import * as PopoverPrimitive from "@radix-ui/react-popover";
7
- import { Calendar, X } from "lucide-react";
8
- import { cn } from "../../../utils/cn";
9
- import type { DateRangeFilter, FieldConfig, FilterValue } from "../types";
10
- import { toIsoDate } from "../../../utils/dataViews/rangeUtils";
11
- import { PresetChips } from "./PresetChips";
12
-
13
- type Props = {
14
- value: DateRangeFilter | undefined;
15
- onChange: (next: DateRangeFilter) => void;
16
- presets: FieldConfig["presets"];
17
- };
18
-
19
- export function DateRangePopover({ value, onChange, presets }: Props) {
20
- const [open, setOpen] = useState(false);
21
-
22
- const range: DateRange | undefined =
23
- value && (value.from || value.to)
24
- ? {
25
- from: value.from ? new Date(value.from + "T00:00:00") : undefined,
26
- to: value.to ? new Date(value.to + "T00:00:00") : undefined,
27
- }
28
- : undefined;
29
-
30
- const label =
31
- value?.from && value?.to
32
- ? `${value.from} → ${value.to}`
33
- : value?.from
34
- ? `from ${value.from}`
35
- : value?.to
36
- ? `until ${value.to}`
37
- : "Any date";
38
-
39
- const handleSelect = (next: DateRange | undefined) => {
40
- onChange({
41
- kind: "date",
42
- from: next?.from ? toIsoDate(next.from) : undefined,
43
- to: next?.to ? toIsoDate(next.to) : undefined,
44
- });
45
- };
46
-
47
- const isActive = !!(value?.from || value?.to);
48
-
49
- return (
50
- <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
51
- <PopoverPrimitive.Trigger asChild>
52
- <button
53
- type="button"
54
- className={cn(
55
- "flex items-center gap-2 w-full text-xs px-2.5 py-2 rounded-md border bg-background-presentation-body-primary text-left",
56
- isActive
57
- ? "border-content-presentation-action-primary text-content-presentation-global-primary"
58
- : "border-border-presentation-global-primary text-content-presentation-global-secondary hover:text-content-presentation-global-primary",
59
- )}
60
- >
61
- <Calendar className="w-3.5 h-3.5 shrink-0" />
62
- <span className="flex-1 truncate">{label}</span>
63
- {isActive && (
64
- <span
65
- role="button"
66
- tabIndex={0}
67
- aria-label="Clear date filter"
68
- onClick={(e) => {
69
- e.stopPropagation();
70
- onChange({ kind: "date" });
71
- }}
72
- onKeyDown={(e) => {
73
- if (e.key === "Enter" || e.key === " ") {
74
- e.stopPropagation();
75
- e.preventDefault();
76
- onChange({ kind: "date" });
77
- }
78
- }}
79
- className="text-content-presentation-global-tertiary hover:text-content-presentation-global-primary cursor-pointer"
80
- >
81
- <X className="w-3 h-3" />
82
- </span>
83
- )}
84
- </button>
85
- </PopoverPrimitive.Trigger>
86
- <PopoverPrimitive.Portal>
87
- <PopoverPrimitive.Content
88
- align="start"
89
- sideOffset={6}
90
- className="z-50 bg-background-presentation-body-primary border border-border-presentation-global-primary rounded-lg shadow-lg p-3 space-y-3"
91
- >
92
- {presets && presets.length > 0 && (
93
- <PresetChips
94
- presets={presets}
95
- current={value}
96
- onSelect={onChange as (value: FilterValue) => void}
97
- />
98
- )}
99
- <DayPicker
100
- mode="range"
101
- selected={range}
102
- onSelect={handleSelect}
103
- numberOfMonths={1}
104
- showOutsideDays
105
- className="rdp-glare"
106
- />
107
- <div className="flex justify-end pt-1 border-t border-border-presentation-global-primary">
108
- <button
109
- type="button"
110
- onClick={() => setOpen(false)}
111
- className="text-xs px-3 py-1 rounded-md bg-content-presentation-action-primary text-white hover:opacity-90"
112
- >
113
- Done
114
- </button>
115
- </div>
116
- </PopoverPrimitive.Content>
117
- </PopoverPrimitive.Portal>
118
- </PopoverPrimitive.Root>
119
- );
120
- }
@@ -1,45 +0,0 @@
1
- "use client";
2
-
3
- import { cn } from "../../../utils/cn";
4
- import type { FieldPreset, FilterValue } from "../types";
5
- import { isPresetActive, presetToFilterValue } from "../../../utils/dataViews/rangeUtils";
6
-
7
- type PresetChipsProps = {
8
- presets: FieldPreset[];
9
- current: FilterValue | undefined;
10
- onSelect: (value: FilterValue) => void;
11
- };
12
-
13
- export function PresetChips({ presets, current, onSelect }: PresetChipsProps) {
14
- if (!presets || presets.length === 0) return null;
15
-
16
- return (
17
- <div className="flex flex-wrap gap-1.5">
18
- {presets.map((p) => {
19
- const active = isPresetActive(p, current);
20
- return (
21
- <button
22
- key={p.label}
23
- type="button"
24
- onClick={() => {
25
- if (active) {
26
- const v = presetToFilterValue(p);
27
- onSelect(v.kind === "number" ? { kind: "number" } : { kind: "date" });
28
- } else {
29
- onSelect(presetToFilterValue(p));
30
- }
31
- }}
32
- className={cn(
33
- "text-xs px-2 py-1 rounded-md border transition-colors whitespace-nowrap",
34
- active
35
- ? "bg-content-presentation-action-primary text-white border-content-presentation-action-primary"
36
- : "bg-background-presentation-form-field-primary border-border-presentation-global-primary text-content-presentation-global-secondary hover:text-content-presentation-global-primary",
37
- )}
38
- >
39
- {p.label}
40
- </button>
41
- );
42
- })}
43
- </div>
44
- );
45
- }