torch-glare 2.1.1 → 2.1.3

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 (73) hide show
  1. package/apps/lib/components/Avatar.tsx +1 -1
  2. package/apps/lib/components/BadgeField.tsx +2 -2
  3. package/apps/lib/components/Card.tsx +68 -54
  4. package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
  5. package/apps/lib/components/DataViews/DataViewRadio.tsx +47 -0
  6. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +427 -0
  7. package/apps/lib/components/DataViews/DataViewsHeader.tsx +228 -0
  8. package/apps/lib/components/DataViews/DataViewsLayout.tsx +330 -0
  9. package/apps/lib/components/DataViews/FilterPanel.tsx +469 -0
  10. package/apps/lib/components/DataViews/HeaderSearch.tsx +97 -0
  11. package/apps/lib/components/DataViews/InboxView.tsx +495 -0
  12. package/apps/lib/components/DataViews/InboxViewCard.tsx +136 -0
  13. package/apps/lib/components/DataViews/KanbanView.tsx +353 -0
  14. package/apps/lib/components/DataViews/PanelControls.tsx +49 -0
  15. package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
  16. package/apps/lib/components/DataViews/TableView.tsx +232 -0
  17. package/apps/lib/components/DataViews/TreeView.tsx +392 -0
  18. package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
  19. package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
  20. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
  21. package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
  22. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
  23. package/apps/lib/components/DataViews/index.ts +36 -0
  24. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
  25. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
  26. package/apps/lib/components/DataViews/types.ts +206 -0
  27. package/apps/lib/components/Radio.tsx +18 -21
  28. package/apps/lib/components/Switch.tsx +3 -1
  29. package/apps/lib/components/Table.tsx +1 -1
  30. package/apps/lib/components/TreeFolder/TreeFolder.tsx +410 -0
  31. package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
  32. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +363 -0
  33. package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
  34. package/apps/lib/components/TreeFolder/icons.tsx +63 -0
  35. package/apps/lib/components/TreeFolder/index.ts +17 -0
  36. package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
  37. package/apps/lib/components/TreeFolder/types.ts +77 -0
  38. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
  39. package/apps/lib/hooks/useDataViewsState.ts +169 -0
  40. package/apps/lib/hooks/useIsMobile.ts +21 -0
  41. package/apps/lib/layouts/DataViewCard.tsx +76 -0
  42. package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
  43. package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
  44. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
  45. package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
  46. package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
  47. package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
  48. package/dist/bin/index.js +3 -3
  49. package/dist/bin/index.js.map +1 -1
  50. package/dist/src/commands/add.d.ts.map +1 -1
  51. package/dist/src/commands/add.js +29 -6
  52. package/dist/src/commands/add.js.map +1 -1
  53. package/dist/src/commands/utils.d.ts.map +1 -1
  54. package/dist/src/commands/utils.js +22 -2
  55. package/dist/src/commands/utils.js.map +1 -1
  56. package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
  57. package/dist/src/shared/copyComponentsRecursively.js +17 -2
  58. package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
  59. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
  60. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
  61. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
  62. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
  63. package/docs/components/data-views-config-panel.md +204 -0
  64. package/docs/components/data-views-layout.md +270 -0
  65. package/docs/components/form-stepper.md +244 -0
  66. package/docs/components/stepper.md +215 -0
  67. package/docs/components/timeline.md +248 -0
  68. package/package.json +6 -6
  69. package/apps/lib/components/Charts-dev.tsx +0 -365
  70. package/apps/lib/components/Command-dev.tsx +0 -151
  71. package/apps/lib/components/IosDatePicker-dev.tsx +0 -341
  72. /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
  73. /package/docs/components/{tree-dropdown.md → tree-drop-down.md} +0 -0
@@ -0,0 +1,495 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
4
+ import { Badge } from "../Badge";
5
+ import { FilterPanel } from "./FilterPanel";
6
+ import {
7
+ Star,
8
+ Archive,
9
+ Trash2,
10
+ MoreHorizontal,
11
+ Reply,
12
+ Forward,
13
+ Paperclip,
14
+ InboxIcon,
15
+ AlertCircle,
16
+ } from "lucide-react";
17
+ import { cn } from "../../utils/cn";
18
+ import type {
19
+ DynamicRecord,
20
+ ViewConfig,
21
+ DynamicColumnConfig,
22
+ DynamicFilterConfig,
23
+ FilterState,
24
+ FilterValue,
25
+ FieldConfig,
26
+ InboxConfig,
27
+ } from "./types";
28
+ import TabFormItem from "../TabFormItem";
29
+ import { Button } from "../Button";
30
+ import { Avatar, AvatarFallback } from "../Avatar";
31
+ import { Card } from "../Card";
32
+ import { Divider } from "../Divider";
33
+ import { renderDetailView } from "../../utils/dataViews/nestedDataUtils";
34
+ import {
35
+ getByPath,
36
+ setByPath,
37
+ matchesFilterValues,
38
+ } from "../../utils/dataViews/pathUtils";
39
+ import { renderField } from "./fieldRenderers";
40
+ import {
41
+ resolveInboxConfig,
42
+ visibleFields,
43
+ } from "../../utils/dataViews/fieldUtils";
44
+ import { useIsMobile } from "../../hooks/useIsMobile";
45
+ import { resolveBadgeVariant } from "./badgeAdapter";
46
+ import { InboxViewCard } from "./InboxViewCard";
47
+
48
+ export type InboxViewProps = {
49
+ data: DynamicRecord[];
50
+ columns?: DynamicColumnConfig[];
51
+ fields: FieldConfig[];
52
+ inboxConfig?: InboxConfig;
53
+ config: ViewConfig;
54
+ onDataUpdate?: (data: DynamicRecord[]) => void;
55
+ filters?: DynamicFilterConfig[];
56
+ filterState?: FilterState;
57
+ onFilterChange?: (filters: FilterState) => void;
58
+ showFilters?: boolean;
59
+ itemHref?: (item: DynamicRecord, id: any) => string;
60
+ selectedItemId?: any;
61
+ renderDetail?: (item: DynamicRecord | null) => ReactNode;
62
+ };
63
+
64
+ type InboxFilter = "all" | "starred" | "priority";
65
+
66
+ function getId(
67
+ item: DynamicRecord,
68
+ fallbackPath: string | undefined,
69
+ idx: number,
70
+ ): any {
71
+ if (item?.id != null) return item.id;
72
+ if (fallbackPath) {
73
+ const v = getByPath(item, fallbackPath);
74
+ if (v != null) return v;
75
+ }
76
+ return idx;
77
+ }
78
+
79
+ function getInitials(name: any): string {
80
+ const s = String(name ?? "?").trim();
81
+ if (!s) return "?";
82
+ return (
83
+ s
84
+ .split(/\s+/)
85
+ .slice(0, 2)
86
+ .map((p) => p[0]?.toUpperCase() ?? "")
87
+ .join("") || "?"
88
+ );
89
+ }
90
+
91
+ export function InboxView({
92
+ data,
93
+ columns,
94
+ fields,
95
+ inboxConfig: userInboxConfig,
96
+ config,
97
+ onDataUpdate,
98
+ filters: filterConfig,
99
+ filterState: externalFilterState,
100
+ onFilterChange,
101
+ showFilters = true,
102
+ itemHref,
103
+ selectedItemId,
104
+ renderDetail,
105
+ }: InboxViewProps) {
106
+ const isMobile = useIsMobile();
107
+ const [selectedItem, setSelectedItem] = useState<DynamicRecord | null>(
108
+ data[0] || null,
109
+ );
110
+ const [inboxFilter, setInboxFilter] = useState<InboxFilter>("all");
111
+ const [internalFilters, setInternalFilters] = useState<FilterState>({});
112
+
113
+ const activeFilters = externalFilterState ?? internalFilters;
114
+
115
+ const displayFields = useMemo(
116
+ () => visibleFields(fields).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
117
+ [fields],
118
+ );
119
+
120
+ const inboxCfg = useMemo(
121
+ () => resolveInboxConfig(data, userInboxConfig),
122
+ [data, userInboxConfig],
123
+ );
124
+ const idPath = displayFields[0]?.path;
125
+
126
+ useEffect(() => {
127
+ if (selectedItemId == null) return;
128
+ const match = data.find((item, idx) => {
129
+ const cur = getId(item, idPath, idx);
130
+ return String(cur) === String(selectedItemId);
131
+ });
132
+ if (match) setSelectedItem(match);
133
+ }, [selectedItemId, data, idPath]);
134
+
135
+ const titleField = useMemo(() => {
136
+ const path = inboxCfg.titlePath;
137
+ if (path)
138
+ return (
139
+ fields.find((f) => f.path === path) ?? {
140
+ path,
141
+ label: path,
142
+ type: "text" as const,
143
+ }
144
+ );
145
+ return displayFields[0];
146
+ }, [inboxCfg.titlePath, displayFields, fields]);
147
+
148
+ const previewField = useMemo(() => {
149
+ const path = inboxCfg.previewPath;
150
+ if (path)
151
+ return (
152
+ fields.find((f) => f.path === path) ?? {
153
+ path,
154
+ label: path,
155
+ type: "text" as const,
156
+ }
157
+ );
158
+ return displayFields[1];
159
+ }, [inboxCfg.previewPath, displayFields, fields]);
160
+
161
+ const isStarred = (item: DynamicRecord) =>
162
+ inboxCfg.starredField ? !!getByPath(item, inboxCfg.starredField) : false;
163
+ const hasAttachment = (item: DynamicRecord) => {
164
+ if (!inboxCfg.attachmentField) return false;
165
+ const v = getByPath(item, inboxCfg.attachmentField);
166
+ if (typeof v === "boolean") return v;
167
+ if (Array.isArray(v)) return v.length > 0;
168
+ return v != null;
169
+ };
170
+ const isHighPriority = (item: DynamicRecord) => {
171
+ if (!inboxCfg.priorityField) return false;
172
+ const v = getByPath(item, inboxCfg.priorityField);
173
+ return String(v).toLowerCase() === "high";
174
+ };
175
+
176
+ const toggleStar = (itemId: any) => {
177
+ if (!inboxCfg.starredField) return;
178
+ const updatedData = data.map((item, idx) => {
179
+ const cur = getId(item, idPath, idx);
180
+ if (cur === itemId) {
181
+ const next = !getByPath(item, inboxCfg.starredField!);
182
+ return setByPath(item, inboxCfg.starredField!, next);
183
+ }
184
+ return item;
185
+ });
186
+ onDataUpdate?.(updatedData);
187
+
188
+ if (selectedItem && getId(selectedItem, idPath, -1) === itemId) {
189
+ setSelectedItem((prev) =>
190
+ prev && inboxCfg.starredField
191
+ ? setByPath(
192
+ prev,
193
+ inboxCfg.starredField,
194
+ !getByPath(prev, inboxCfg.starredField),
195
+ )
196
+ : prev,
197
+ );
198
+ }
199
+ };
200
+
201
+ const handleSelectItem = (item: DynamicRecord) => {
202
+ setSelectedItem(item);
203
+ };
204
+
205
+ const handleFilterChange = (path: string, value: FilterValue) => {
206
+ const newFilters: FilterState = { ...activeFilters, [path]: value };
207
+ if (onFilterChange) onFilterChange(newFilters);
208
+ else setInternalFilters(newFilters);
209
+ };
210
+
211
+ const clearAllFilters = () => {
212
+ if (onFilterChange) onFilterChange({});
213
+ else setInternalFilters({});
214
+ };
215
+
216
+ const filteredData = useMemo(() => {
217
+ return data.filter((item) => {
218
+ const matchesInboxFilter =
219
+ inboxFilter === "all" ||
220
+ (inboxFilter === "starred" && isStarred(item)) ||
221
+ (inboxFilter === "priority" && isHighPriority(item));
222
+
223
+ const matchesFilters = Object.entries(activeFilters).every(
224
+ ([path, filterValues]) => matchesFilterValues(item, path, filterValues),
225
+ );
226
+
227
+ return matchesInboxFilter && matchesFilters;
228
+ });
229
+ // eslint-disable-next-line react-hooks/exhaustive-deps
230
+ }, [data, inboxFilter, activeFilters, inboxCfg]);
231
+
232
+ const starredCount = inboxCfg.starredField
233
+ ? data.filter((i) => isStarred(i)).length
234
+ : 0;
235
+ const priorityCount = inboxCfg.priorityField
236
+ ? data.filter((i) => isHighPriority(i)).length
237
+ : 0;
238
+
239
+ const filtersEnabled = showFilters && config.showFilters !== false;
240
+ const countBadge = resolveBadgeVariant("gray");
241
+ const fallbackColumns: DynamicColumnConfig[] =
242
+ columns ??
243
+ displayFields.map((f, i) => ({
244
+ id: f.path,
245
+ label: f.label ?? f.path,
246
+ visible: true,
247
+ order: i,
248
+ }));
249
+
250
+ return (
251
+ <div className="flex h-full flex-col md:flex-row gap-2">
252
+ {filtersEnabled && !isMobile && (
253
+ <div className="w-64 border-r border-border-presentation-global-primary bg-background-presentation-body-overlay-primary flex flex-col">
254
+ <div className="p-4 space-y-1">
255
+ <TabFormItem
256
+ componentType="side"
257
+ onClick={() => setInboxFilter("all")}
258
+ className="w-full justify-start gap-2"
259
+ >
260
+ <InboxIcon className="h-4 w-4" />
261
+ All Items
262
+ <Badge
263
+ {...countBadge}
264
+ label={String(data.length)}
265
+ className="ml-auto"
266
+ size="XS"
267
+ />
268
+ </TabFormItem>
269
+ {inboxCfg.starredField && (
270
+ <TabFormItem
271
+ componentType="side"
272
+ className="w-full justify-start gap-2"
273
+ onClick={() => setInboxFilter("starred")}
274
+ >
275
+ <Star className="h-4 w-4" />
276
+ Starred
277
+ {starredCount > 0 && (
278
+ <Badge
279
+ {...countBadge}
280
+ label={String(starredCount)}
281
+ className="ml-auto"
282
+ size="XS"
283
+ />
284
+ )}
285
+ </TabFormItem>
286
+ )}
287
+ {inboxCfg.priorityField && (
288
+ <TabFormItem
289
+ componentType="side"
290
+ className="w-full justify-start gap-2"
291
+ onClick={() => setInboxFilter("priority")}
292
+ >
293
+ <AlertCircle className="h-4 w-4" />
294
+ Priority
295
+ {priorityCount > 0 && (
296
+ <Badge
297
+ {...countBadge}
298
+ label={String(priorityCount)}
299
+ className="ml-auto"
300
+ size="XS"
301
+ />
302
+ )}
303
+ </TabFormItem>
304
+ )}
305
+ </div>
306
+
307
+ <Divider />
308
+
309
+ <div className="flex-1 overflow-y-auto">
310
+ <FilterPanel
311
+ data={data}
312
+ fields={fields}
313
+ filters={activeFilters}
314
+ onFilterChange={handleFilterChange}
315
+ onClearAll={clearAllFilters}
316
+ filterConfig={filterConfig}
317
+ />
318
+ </div>
319
+ </div>
320
+ )}
321
+
322
+ <div
323
+ className={cn(
324
+ "border rounded-[16px] border-border-presentation-global-primary flex flex-col bg-background-presentation-form-base overflow-hidden",
325
+ isMobile ? "flex-1" : "w-full md:w-96",
326
+ )}
327
+ >
328
+ <div className="px-3 py-2 border-b border-border-presentation-global-primary">
329
+ <span
330
+ style={{ fontFeatureSettings: "'cv05' on" }}
331
+ className="typography-display-medium-medium uppercase text-content-presentation-global-primary"
332
+ >
333
+ inbox
334
+ </span>
335
+ </div>
336
+ <div className="flex-1 flex flex-col overflow-y-auto gap-1 py-1 bg-background-presentation-button-disabled">
337
+ {filteredData.map((item, idx) => {
338
+ const itemId = getId(item, idPath, idx);
339
+ const selected =
340
+ (selectedItemId != null &&
341
+ String(selectedItemId) === String(itemId)) ||
342
+ (selectedItem != null &&
343
+ getId(selectedItem, idPath, -1) === itemId);
344
+
345
+ return (
346
+ <InboxViewCard
347
+ key={itemId}
348
+ item={item}
349
+ rowFields={displayFields}
350
+ selected={selected}
351
+ onSelect={() => handleSelectItem(item)}
352
+ href={itemHref?.(item, itemId)}
353
+ />
354
+ );
355
+ })}
356
+ </div>
357
+ </div>
358
+
359
+ {renderDetail && !isMobile ? (
360
+ <div className="flex-1 flex flex-col bg-background-presentation-form-base overflow-hidden rounded-[16px]">
361
+ {renderDetail(selectedItem)}
362
+ </div>
363
+ ) : config.showPreviewPane && !isMobile && selectedItem ? (
364
+ <div className="flex-1 flex flex-col bg-background-presentation-form-base overflow-hidden rounded-[16px] border border-border-presentation-global-primary">
365
+ <div className="flex items-center justify-between gap-4 p-4 border-b border-border-presentation-global-primary bg-background-presentation-form-base">
366
+ <div className="flex items-center gap-2">
367
+ <Button variant="BorderStyle" buttonType="icon">
368
+ <Archive className="h-4 w-4" />
369
+ </Button>
370
+ <Button variant="BorderStyle" buttonType="icon">
371
+ <Trash2 className="h-4 w-4" />
372
+ </Button>
373
+ <Divider orientation="vertical" className="h-6" />
374
+ <Button variant="BorderStyle" buttonType="icon">
375
+ <MoreHorizontal className="h-4 w-4" />
376
+ </Button>
377
+ </div>
378
+ </div>
379
+
380
+ <div className="flex-1 overflow-y-auto p-6">
381
+ <Card>
382
+ <div className="flex items-start gap-4 mb-6">
383
+ <Avatar className="h-12 w-12">
384
+ <AvatarFallback className="bg-background-presentation-action-primary text-content-presentation-action-primary">
385
+ {getInitials(
386
+ previewField
387
+ ? getByPath(selectedItem, previewField.path)
388
+ : "",
389
+ )}
390
+ </AvatarFallback>
391
+ </Avatar>
392
+ <div className="flex-1">
393
+ <div className="flex items-start justify-between gap-4 mb-2">
394
+ <div>
395
+ <h2 className="text-xl font-semibold text-content-presentation-global-primary mb-1">
396
+ {String(
397
+ titleField
398
+ ? getByPath(selectedItem, titleField.path)
399
+ : "",
400
+ )}
401
+ </h2>
402
+ {previewField && (
403
+ <p className="text-sm text-content-presentation-global-tertiary">
404
+ {previewField.label}:{" "}
405
+ <span className="text-content-presentation-global-primary">
406
+ {String(getByPath(selectedItem, previewField.path))}
407
+ </span>
408
+ </p>
409
+ )}
410
+ </div>
411
+ {inboxCfg.starredField && (
412
+ <button
413
+ onClick={() =>
414
+ toggleStar(getId(selectedItem, idPath, -1))
415
+ }
416
+ className="hover:text-content-presentation-badge-yellow transition-colors"
417
+ aria-label="Toggle star"
418
+ >
419
+ <Star
420
+ className={cn(
421
+ "h-5 w-5",
422
+ isStarred(selectedItem)
423
+ ? "fill-content-presentation-badge-yellow text-content-presentation-badge-yellow"
424
+ : "text-content-presentation-global-tertiary",
425
+ )}
426
+ />
427
+ </button>
428
+ )}
429
+ </div>
430
+ <div className="flex items-center gap-2 flex-wrap">
431
+ {displayFields.slice(3).map((field) => {
432
+ const value = getByPath(selectedItem, field.path);
433
+ if (value == null) return null;
434
+ return (
435
+ <span key={field.path}>
436
+ {renderField(value, field, selectedItem)}
437
+ </span>
438
+ );
439
+ })}
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ <Divider className="my-6" />
445
+
446
+ {renderDetailView(
447
+ selectedItem,
448
+ fallbackColumns.filter((c) => c.visible),
449
+ (value, column, row) => {
450
+ const f = fields.find((field) => field.path === column.id);
451
+ if (f) return renderField(value, f, row);
452
+ return <span>{String(value ?? "")}</span>;
453
+ },
454
+ )}
455
+
456
+ {hasAttachment(selectedItem) && (
457
+ <>
458
+ <Divider className="my-6" />
459
+ <div className="flex items-center gap-2 p-3 rounded-lg bg-background-presentation-form-field-primary">
460
+ <Paperclip className="h-4 w-4 text-content-presentation-global-tertiary" />
461
+ <span className="text-sm text-content-presentation-global-primary">
462
+ attachment.pdf
463
+ </span>
464
+ <span className="text-xs text-content-presentation-global-tertiary">
465
+ (2.4 MB)
466
+ </span>
467
+ </div>
468
+ </>
469
+ )}
470
+ </Card>
471
+ </div>
472
+
473
+ <div className="flex items-center gap-2 p-4 border-t border-border-presentation-global-primary bg-background-presentation-form-base">
474
+ <Button className="gap-2">
475
+ <Reply className="h-4 w-4" />
476
+ Reply
477
+ </Button>
478
+ <Button variant="BorderStyle" className="gap-2 bg-transparent">
479
+ <Forward className="h-4 w-4" />
480
+ Forward
481
+ </Button>
482
+ </div>
483
+ </div>
484
+ ) : (
485
+ !isMobile && (
486
+ <div className="flex-1 flex items-center justify-center bg-background-presentation-form-base overflow-hidden rounded-[16px] border border-border-presentation-global-primary">
487
+ <p className="text-content-presentation-global-tertiary">
488
+ Select an item to view details
489
+ </p>
490
+ </div>
491
+ )
492
+ )}
493
+ </div>
494
+ );
495
+ }
@@ -0,0 +1,136 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type ElementType, type ReactNode } from "react";
4
+ import { cn } from "../../utils/cn";
5
+ import { Divider } from "../Divider";
6
+ import { getByPath } from "../../utils/dataViews/pathUtils";
7
+ import { renderField } from "./fieldRenderers";
8
+ import type { DynamicRecord, FieldConfig } from "./types";
9
+
10
+ export interface InboxViewCardProps {
11
+ item: DynamicRecord;
12
+ rowFields?: FieldConfig[];
13
+ titleField?: FieldConfig;
14
+ previewField?: FieldConfig;
15
+ detailField?: FieldConfig;
16
+ metaFields?: FieldConfig[];
17
+ dateField?: FieldConfig;
18
+ dateLabel?: string;
19
+ selected?: boolean;
20
+ onSelect?: () => void;
21
+ href?: string;
22
+ /**
23
+ * Component used to render the link when `href` is set. Defaults to a plain
24
+ * `<a>` so the component stays framework-agnostic. Pass your router's link
25
+ * (e.g. Next.js `Link`, React Router `Link`) for client-side navigation.
26
+ */
27
+ linkComponent?: ElementType;
28
+ className?: string;
29
+ }
30
+
31
+ function pickRowFields(props: InboxViewCardProps): FieldConfig[] {
32
+ if (props.rowFields && props.rowFields.length) return props.rowFields;
33
+ const collected: FieldConfig[] = [];
34
+ if (props.previewField) collected.push(props.previewField);
35
+ if (props.titleField && props.titleField.path !== props.previewField?.path) {
36
+ collected.push(props.titleField);
37
+ }
38
+ if (props.detailField) collected.push(props.detailField);
39
+ if (props.metaFields?.length) collected.push(...props.metaFields);
40
+ return collected;
41
+ }
42
+
43
+ function pickDateField(
44
+ rowFields: FieldConfig[],
45
+ explicit?: FieldConfig,
46
+ ): FieldConfig | undefined {
47
+ if (explicit) return explicit;
48
+ return rowFields.find((f) => f.type === "date");
49
+ }
50
+
51
+ export const InboxViewCard = forwardRef<HTMLDivElement, InboxViewCardProps>(
52
+ (props, ref) => {
53
+ const { item, selected = false, onSelect, href, linkComponent, className } =
54
+ props;
55
+ const allRowFields = pickRowFields(props);
56
+ const dateField = pickDateField(allRowFields, props.dateField);
57
+ const rowFields = dateField
58
+ ? allRowFields.filter((f) => f.path !== dateField.path)
59
+ : allRowFields;
60
+ const dateLabel = props.dateLabel ?? "Created at:";
61
+
62
+ const cardClass = cn(
63
+ "flex flex-col gap-2 p-3 cursor-pointer transition-colors",
64
+ "bg-background-presentation-form-base",
65
+ "border-y-2 border-transparent",
66
+ !selected &&
67
+ "hover:bg-[image:linear-gradient(0deg,rgba(151,72,255,0.05)_0%,rgba(151,72,255,0.05)_100%)] hover:border-y-[#AE71FF]",
68
+ selected &&
69
+ "bg-[image:linear-gradient(0deg,rgba(0,117,255,0.05)_0%,rgba(0,117,255,0.05)_100%)] border-y-border-presentation-state-focus",
70
+ className,
71
+ );
72
+
73
+ const content: ReactNode = (
74
+ <>
75
+ <div className="flex flex-col gap-1 w-full">
76
+ {rowFields.map((field, idx) => {
77
+ const value = getByPath(item, field.path);
78
+ return (
79
+ <div key={field.path ?? idx} className="flex flex-col">
80
+ <div className="flex items-center gap-2">
81
+ <span className="w-[100px] shrink-0 typography-body-large-semibold text-content-presentation-global-secondary">
82
+ {field.label ?? field.path}:
83
+ </span>
84
+ <span className="h-full py-0.5 flex items-center">
85
+ <span className="block h-full w-px bg-black-alpha-15" />
86
+ </span>
87
+ <span className="flex-1 min-w-0 truncate typography-body-large-medium text-content-presentation-global-primary">
88
+ {renderField(value, field, item)}
89
+ </span>
90
+ </div>
91
+ {idx < rowFields.length - 1 && <Divider className="mt-1" />}
92
+ </div>
93
+ );
94
+ })}
95
+ </div>
96
+
97
+ {dateField && (
98
+ <div className="flex items-center justify-end">
99
+ <div className="inline-flex items-center gap-0.5 p-0.5 rounded-md bg-black-alpha-10">
100
+ <div className="px-1 rounded-sm">
101
+ <span className="typography-labels-medium-semibold text-content-presentation-global-primary">
102
+ {dateLabel}
103
+ </span>
104
+ </div>
105
+ <div className="px-1 rounded-sm bg-black-alpha-075">
106
+ <span className="typography-labels-medium-semibold text-content-presentation-global-primary">
107
+ {renderField(getByPath(item, dateField.path), dateField, item)}
108
+ </span>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ )}
113
+ </>
114
+ );
115
+
116
+ if (href) {
117
+ const LinkTag = (linkComponent ?? "a") as ElementType;
118
+ return (
119
+ <LinkTag
120
+ href={href}
121
+ className={cn(cardClass, "no-underline text-inherit")}
122
+ >
123
+ {content}
124
+ </LinkTag>
125
+ );
126
+ }
127
+
128
+ return (
129
+ <div ref={ref} onClick={onSelect} className={cardClass}>
130
+ {content}
131
+ </div>
132
+ );
133
+ },
134
+ );
135
+
136
+ InboxViewCard.displayName = "InboxViewCard";