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,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";