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