torch-glare 2.1.2 → 2.1.4

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