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.
- package/apps/lib/components/Avatar.tsx +1 -1
- package/apps/lib/components/BadgeField.tsx +2 -2
- package/apps/lib/components/Card.tsx +68 -54
- package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
- package/apps/lib/components/DataViews/DataViewRadio.tsx +47 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +427 -0
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +228 -0
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +330 -0
- package/apps/lib/components/DataViews/FilterPanel.tsx +469 -0
- package/apps/lib/components/DataViews/HeaderSearch.tsx +97 -0
- package/apps/lib/components/DataViews/InboxView.tsx +495 -0
- package/apps/lib/components/DataViews/InboxViewCard.tsx +136 -0
- package/apps/lib/components/DataViews/KanbanView.tsx +353 -0
- package/apps/lib/components/DataViews/PanelControls.tsx +49 -0
- package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
- package/apps/lib/components/DataViews/TableView.tsx +232 -0
- package/apps/lib/components/DataViews/TreeView.tsx +392 -0
- package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
- package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
- package/apps/lib/components/DataViews/index.ts +36 -0
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
- package/apps/lib/components/DataViews/types.ts +206 -0
- package/apps/lib/components/Radio.tsx +18 -21
- package/apps/lib/components/Switch.tsx +3 -1
- package/apps/lib/components/Table.tsx +1 -1
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +410 -0
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +363 -0
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
- package/apps/lib/components/TreeFolder/icons.tsx +63 -0
- package/apps/lib/components/TreeFolder/index.ts +17 -0
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
- package/apps/lib/components/TreeFolder/types.ts +77 -0
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
- package/apps/lib/hooks/useDataViewsState.ts +169 -0
- package/apps/lib/hooks/useIsMobile.ts +21 -0
- package/apps/lib/layouts/DataViewCard.tsx +76 -0
- package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
- package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
- package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
- package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
- package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
- package/dist/bin/index.js +3 -3
- package/dist/bin/index.js.map +1 -1
- package/dist/src/commands/add.d.ts.map +1 -1
- package/dist/src/commands/add.js +29 -6
- package/dist/src/commands/add.js.map +1 -1
- package/dist/src/commands/utils.d.ts.map +1 -1
- package/dist/src/commands/utils.js +22 -2
- package/dist/src/commands/utils.js.map +1 -1
- package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
- package/dist/src/shared/copyComponentsRecursively.js +17 -2
- package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
- package/docs/components/data-views-config-panel.md +204 -0
- package/docs/components/data-views-layout.md +270 -0
- package/docs/components/form-stepper.md +244 -0
- package/docs/components/stepper.md +215 -0
- package/docs/components/timeline.md +248 -0
- package/package.json +6 -6
- package/apps/lib/components/Charts-dev.tsx +0 -365
- package/apps/lib/components/Command-dev.tsx +0 -151
- package/apps/lib/components/IosDatePicker-dev.tsx +0 -341
- /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
- /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";
|