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.
- package/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
- package/apps/lib/components/DataViews/cell.tsx +324 -0
- package/apps/lib/components/DataViews/context.ts +144 -0
- package/apps/lib/components/DataViews/data-views.tsx +383 -0
- package/apps/lib/components/DataViews/filters/children.tsx +98 -0
- package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
- package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
- package/apps/lib/components/DataViews/filters/index.ts +4 -0
- package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
- package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
- package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
- package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
- package/apps/lib/components/DataViews/filters/values.ts +173 -0
- package/apps/lib/components/DataViews/header.tsx +217 -0
- package/apps/lib/components/DataViews/hooks/index.ts +5 -0
- package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
- package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
- package/apps/lib/components/DataViews/index.ts +74 -26
- package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
- package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
- package/apps/lib/components/DataViews/panel/index.ts +3 -0
- package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
- package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
- package/apps/lib/components/DataViews/panel/section.tsx +79 -0
- package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
- package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
- package/apps/lib/components/DataViews/slots.ts +63 -0
- package/apps/lib/components/DataViews/states.tsx +38 -0
- package/apps/lib/components/DataViews/types.ts +485 -178
- package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
- package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
- package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
- package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
- package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
- package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
- package/apps/lib/components/FormBuilder/context.ts +20 -6
- package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
- package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
- package/apps/lib/components/FormBuilder/index.ts +3 -1
- package/apps/lib/components/FormBuilder/types.ts +40 -0
- package/apps/lib/components/Input.tsx +3 -0
- package/apps/lib/components/SearchableTable.tsx +5 -4
- package/apps/lib/components/SectionBlock.tsx +58 -11
- package/apps/lib/components/Select.tsx +3 -1
- package/apps/lib/components/TabSwitch.tsx +16 -4
- package/apps/lib/components/Table.tsx +265 -67
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
- package/apps/lib/components/TreeFolder/index.ts +1 -1
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
- package/apps/lib/hooks/useDragDrop.tsx +365 -0
- package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
- package/apps/lib/registry.json +159 -4
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/apps/lib/utils/dataViews/path.ts +67 -0
- package/apps/lib/utils/dataViews/query.ts +73 -0
- package/apps/lib/utils/dataViews/types.ts +187 -0
- package/docs/components/breadcrumb.md +1 -1
- package/docs/components/button-group.md +1 -1
- package/docs/components/button.md +1 -1
- package/docs/components/card.md +1 -1
- package/docs/components/checkbox.md +1 -1
- package/docs/components/data-views/backend-response.md +324 -0
- package/docs/components/data-views/examples/a11y-rtl.md +250 -0
- package/docs/components/data-views/examples/api-orders-route.md +130 -0
- package/docs/components/data-views/examples/fields.md +362 -0
- package/docs/components/data-views/examples/filters.md +308 -0
- package/docs/components/data-views/examples/inbox-routing.md +218 -0
- package/docs/components/data-views/examples/index.md +29 -0
- package/docs/components/data-views/examples/overview.md +244 -0
- package/docs/components/data-views/examples/panel.md +212 -0
- package/docs/components/data-views/examples/scale.md +231 -0
- package/docs/components/data-views/examples/server-side.md +210 -0
- package/docs/components/data-views/examples/state.md +250 -0
- package/docs/components/data-views/examples/tree-custom.md +388 -0
- package/docs/components/data-views/examples/view-registry.md +313 -0
- package/docs/components/data-views/examples/views.md +534 -0
- package/docs/components/data-views/guide.md +405 -0
- package/docs/components/data-views/index.md +1504 -0
- package/docs/components/data-views/migration.md +79 -0
- package/docs/components/date-picker.md +0 -1
- package/docs/components/form-builder.md +19 -8
- package/docs/components/form-renderer.md +2 -1
- package/docs/components/form.md +1 -1
- package/docs/components/input-field.md +1 -1
- package/docs/components/input-otp.md +1 -1
- package/docs/components/input.md +1 -1
- package/docs/components/labeled-check-box.md +1 -1
- package/docs/components/labeled-radio.md +1 -1
- package/docs/components/radio-card.md +1 -1
- package/docs/components/radio.md +1 -1
- package/docs/components/search-field.md +1 -1
- package/docs/components/section-block.md +79 -3
- package/docs/components/select.md +1 -1
- package/docs/components/simple-select.md +1 -1
- package/docs/components/switch.md +1 -1
- package/docs/components/tab-switch.md +1 -1
- package/docs/components/table.md +45 -8
- package/docs/components/text-editor.md +1 -1
- package/docs/components/textarea.md +1 -1
- package/docs/components/toggle-button.md +1 -1
- package/docs/components/toggle.md +1 -1
- package/docs/components/tree-folder.md +110 -0
- package/docs/how-to/forms-with-form-builder.md +6 -4
- package/docs/reference/components.md +16 -6
- package/docs/tutorials/component-composition.md +11 -13
- package/package.json +3 -2
- package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
- package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
- package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
- package/apps/lib/components/DataViews/InboxView.tsx +0 -463
- package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
- package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
- package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
- package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
- package/apps/lib/components/DataViews/TableView.tsx +0 -212
- package/apps/lib/components/DataViews/TreeView.tsx +0 -364
- package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
- package/apps/lib/hooks/useDataViewsState.ts +0 -175
- package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
- package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
- package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
- package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
- package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
- package/docs/components/data-views-config-panel.md +0 -208
- package/docs/components/data-views-layout.md +0 -291
- package/docs/components/inbox-view.md +0 -170
- package/docs/components/kanban-view.md +0 -135
- package/docs/components/table-view.md +0 -141
- package/docs/components/tree-view.md +0 -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
|
-
}
|