torch-glare 2.1.1 → 2.1.2
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/BadgeField.tsx +2 -2
- package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +416 -0
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +126 -0
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +300 -0
- package/apps/lib/components/DataViews/FilterPanel.tsx +324 -0
- package/apps/lib/components/DataViews/InboxView.tsx +514 -0
- package/apps/lib/components/DataViews/KanbanView.tsx +242 -0
- package/apps/lib/components/DataViews/PanelControls.tsx +80 -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 +363 -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 +30 -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 +177 -0
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +387 -0
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +235 -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 +68 -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/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 +8 -1
- 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/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,514 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react"
|
|
4
|
+
import { Badge } from "../Badge"
|
|
5
|
+
import { FilterPanel } from "./FilterPanel"
|
|
6
|
+
import {
|
|
7
|
+
Search,
|
|
8
|
+
Star,
|
|
9
|
+
Archive,
|
|
10
|
+
Trash2,
|
|
11
|
+
MoreHorizontal,
|
|
12
|
+
Reply,
|
|
13
|
+
Forward,
|
|
14
|
+
Paperclip,
|
|
15
|
+
InboxIcon,
|
|
16
|
+
Mail,
|
|
17
|
+
AlertCircle,
|
|
18
|
+
} from "lucide-react"
|
|
19
|
+
import { cn } from "../../utils/cn"
|
|
20
|
+
import type {
|
|
21
|
+
DynamicRecord,
|
|
22
|
+
ViewConfig,
|
|
23
|
+
DynamicColumnConfig,
|
|
24
|
+
DynamicFilterConfig,
|
|
25
|
+
FilterState,
|
|
26
|
+
FilterValue,
|
|
27
|
+
FieldConfig,
|
|
28
|
+
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"
|
|
42
|
+
|
|
43
|
+
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
|
|
60
|
+
if (fallbackPath) {
|
|
61
|
+
const v = getByPath(item, fallbackPath)
|
|
62
|
+
if (v != null) return v
|
|
63
|
+
}
|
|
64
|
+
return idx
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
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("") || "?"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function InboxView({
|
|
78
|
+
data,
|
|
79
|
+
columns,
|
|
80
|
+
fields,
|
|
81
|
+
inboxConfig: userInboxConfig,
|
|
82
|
+
config,
|
|
83
|
+
onDataUpdate,
|
|
84
|
+
filters: filterConfig,
|
|
85
|
+
filterState: externalFilterState,
|
|
86
|
+
onFilterChange,
|
|
87
|
+
showFilters = true,
|
|
88
|
+
}: 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>({})
|
|
94
|
+
|
|
95
|
+
const activeFilters = externalFilterState ?? internalFilters
|
|
96
|
+
|
|
97
|
+
const displayFields = useMemo(
|
|
98
|
+
() => visibleFields(fields).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
|
99
|
+
[fields],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const inboxCfg = useMemo(() => resolveInboxConfig(data, userInboxConfig), [data, userInboxConfig])
|
|
103
|
+
const idPath = displayFields[0]?.path
|
|
104
|
+
|
|
105
|
+
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])
|
|
110
|
+
|
|
111
|
+
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])
|
|
118
|
+
|
|
119
|
+
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
|
|
123
|
+
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
|
+
}
|
|
130
|
+
const isHighPriority = (item: DynamicRecord) => {
|
|
131
|
+
if (!inboxCfg.priorityField) return false
|
|
132
|
+
const v = getByPath(item, inboxCfg.priorityField)
|
|
133
|
+
return String(v).toLowerCase() === "high"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const toggleStar = (itemId: any) => {
|
|
137
|
+
if (!inboxCfg.starredField) return
|
|
138
|
+
const updatedData = data.map((item, idx) => {
|
|
139
|
+
const cur = getId(item, idPath, idx)
|
|
140
|
+
if (cur === itemId) {
|
|
141
|
+
const next = !getByPath(item, inboxCfg.starredField!)
|
|
142
|
+
return setByPath(item, inboxCfg.starredField!, next)
|
|
143
|
+
}
|
|
144
|
+
return item
|
|
145
|
+
})
|
|
146
|
+
onDataUpdate?.(updatedData)
|
|
147
|
+
|
|
148
|
+
if (selectedItem && getId(selectedItem, idPath, -1) === itemId) {
|
|
149
|
+
setSelectedItem((prev) =>
|
|
150
|
+
prev && inboxCfg.starredField
|
|
151
|
+
? setByPath(prev, inboxCfg.starredField, !getByPath(prev, inboxCfg.starredField))
|
|
152
|
+
: prev,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
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
|
+
}
|
|
168
|
+
|
|
169
|
+
const handleSelectItem = (item: DynamicRecord, idx: number) => {
|
|
170
|
+
setSelectedItem(item)
|
|
171
|
+
markAsRead(getId(item, idPath, idx))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleFilterChange = (path: string, value: FilterValue) => {
|
|
175
|
+
const newFilters: FilterState = { ...activeFilters, [path]: value }
|
|
176
|
+
if (onFilterChange) onFilterChange(newFilters)
|
|
177
|
+
else setInternalFilters(newFilters)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const clearAllFilters = () => {
|
|
181
|
+
if (onFilterChange) onFilterChange({})
|
|
182
|
+
else setInternalFilters({})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const filteredData = useMemo(() => {
|
|
186
|
+
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
|
+
const matchesInboxFilter =
|
|
196
|
+
inboxFilter === "all" ||
|
|
197
|
+
(inboxFilter === "unread" && !isRead(item)) ||
|
|
198
|
+
(inboxFilter === "starred" && isStarred(item)) ||
|
|
199
|
+
(inboxFilter === "priority" && isHighPriority(item))
|
|
200
|
+
|
|
201
|
+
const matchesFilters = Object.entries(activeFilters).every(([path, filterValues]) =>
|
|
202
|
+
matchesFilterValues(item, path, filterValues),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return matchesSearch && matchesInboxFilter && matchesFilters
|
|
206
|
+
})
|
|
207
|
+
// 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
|
+
}))
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div className="flex h-full flex-col md:flex-row bg-background-presentation-body-primary">
|
|
225
|
+
{filtersEnabled && !isMobile && (
|
|
226
|
+
<div className="w-64 border-r border-border-presentation-global-primary bg-background-presentation-body-overlay-primary flex flex-col">
|
|
227
|
+
<div className="p-4 space-y-1">
|
|
228
|
+
<TabFormItem
|
|
229
|
+
componentType="side"
|
|
230
|
+
onClick={() => setInboxFilter("all")}
|
|
231
|
+
className="w-full justify-start gap-2"
|
|
232
|
+
>
|
|
233
|
+
<InboxIcon className="h-4 w-4" />
|
|
234
|
+
All Items
|
|
235
|
+
<Badge {...countBadge} label={String(data.length)} className="ml-auto" size="XS" />
|
|
236
|
+
</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
|
+
{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 {...countBadge} label={String(starredCount)} className="ml-auto" size="XS" />
|
|
260
|
+
)}
|
|
261
|
+
</TabFormItem>
|
|
262
|
+
)}
|
|
263
|
+
{inboxCfg.priorityField && (
|
|
264
|
+
<TabFormItem
|
|
265
|
+
componentType="side"
|
|
266
|
+
className="w-full justify-start gap-2"
|
|
267
|
+
onClick={() => setInboxFilter("priority")}
|
|
268
|
+
>
|
|
269
|
+
<AlertCircle className="h-4 w-4" />
|
|
270
|
+
Priority
|
|
271
|
+
{priorityCount > 0 && (
|
|
272
|
+
<Badge {...countBadge} label={String(priorityCount)} className="ml-auto" size="XS" />
|
|
273
|
+
)}
|
|
274
|
+
</TabFormItem>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<Divider />
|
|
279
|
+
|
|
280
|
+
<div className="flex-1 overflow-y-auto">
|
|
281
|
+
<FilterPanel
|
|
282
|
+
data={data}
|
|
283
|
+
fields={fields}
|
|
284
|
+
filters={activeFilters}
|
|
285
|
+
onFilterChange={handleFilterChange}
|
|
286
|
+
onClearAll={clearAllFilters}
|
|
287
|
+
filterConfig={filterConfig}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
|
|
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>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div className="flex-1 overflow-y-auto">
|
|
310
|
+
{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
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div
|
|
320
|
+
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
|
+
)
|
|
400
|
+
})}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
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">
|
|
407
|
+
<div className="flex items-center gap-2">
|
|
408
|
+
<Button variant="BorderStyle" buttonType="icon">
|
|
409
|
+
<Archive className="h-4 w-4" />
|
|
410
|
+
</Button>
|
|
411
|
+
<Button variant="BorderStyle" buttonType="icon">
|
|
412
|
+
<Trash2 className="h-4 w-4" />
|
|
413
|
+
</Button>
|
|
414
|
+
<Divider orientation="vertical" className="h-6" />
|
|
415
|
+
<Button variant="BorderStyle" buttonType="icon">
|
|
416
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
417
|
+
</Button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<div className="flex-1 overflow-y-auto p-6">
|
|
422
|
+
<Card>
|
|
423
|
+
<div className="flex items-start gap-4 mb-6">
|
|
424
|
+
<Avatar className="h-12 w-12">
|
|
425
|
+
<AvatarFallback className="bg-background-presentation-action-primary text-content-presentation-action-primary">
|
|
426
|
+
{getInitials(previewField ? getByPath(selectedItem, previewField.path) : "")}
|
|
427
|
+
</AvatarFallback>
|
|
428
|
+
</Avatar>
|
|
429
|
+
<div className="flex-1">
|
|
430
|
+
<div className="flex items-start justify-between gap-4 mb-2">
|
|
431
|
+
<div>
|
|
432
|
+
<h2 className="text-xl font-semibold text-content-presentation-global-primary mb-1">
|
|
433
|
+
{String(titleField ? getByPath(selectedItem, titleField.path) : "")}
|
|
434
|
+
</h2>
|
|
435
|
+
{previewField && (
|
|
436
|
+
<p className="text-sm text-content-presentation-global-tertiary">
|
|
437
|
+
{previewField.label}:{" "}
|
|
438
|
+
<span className="text-content-presentation-global-primary">
|
|
439
|
+
{String(getByPath(selectedItem, previewField.path))}
|
|
440
|
+
</span>
|
|
441
|
+
</p>
|
|
442
|
+
)}
|
|
443
|
+
</div>
|
|
444
|
+
{inboxCfg.starredField && (
|
|
445
|
+
<button
|
|
446
|
+
onClick={() => toggleStar(getId(selectedItem, idPath, -1))}
|
|
447
|
+
className="hover:text-content-presentation-badge-yellow transition-colors"
|
|
448
|
+
aria-label="Toggle star"
|
|
449
|
+
>
|
|
450
|
+
<Star
|
|
451
|
+
className={cn(
|
|
452
|
+
"h-5 w-5",
|
|
453
|
+
isStarred(selectedItem)
|
|
454
|
+
? "fill-content-presentation-badge-yellow text-content-presentation-badge-yellow"
|
|
455
|
+
: "text-content-presentation-global-tertiary",
|
|
456
|
+
)}
|
|
457
|
+
/>
|
|
458
|
+
</button>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
462
|
+
{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>
|
|
466
|
+
})}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<Divider className="my-6" />
|
|
472
|
+
|
|
473
|
+
{renderDetailView(
|
|
474
|
+
selectedItem,
|
|
475
|
+
fallbackColumns.filter((c) => c.visible),
|
|
476
|
+
(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>
|
|
480
|
+
},
|
|
481
|
+
)}
|
|
482
|
+
|
|
483
|
+
{hasAttachment(selectedItem) && (
|
|
484
|
+
<>
|
|
485
|
+
<Divider className="my-6" />
|
|
486
|
+
<div className="flex items-center gap-2 p-3 rounded-lg bg-background-presentation-form-field-primary">
|
|
487
|
+
<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>
|
|
490
|
+
</div>
|
|
491
|
+
</>
|
|
492
|
+
)}
|
|
493
|
+
</Card>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div className="flex items-center gap-2 p-4 border-t border-border-presentation-global-primary bg-background-presentation-body-primary">
|
|
497
|
+
<Button className="gap-2">
|
|
498
|
+
<Reply className="h-4 w-4" />
|
|
499
|
+
Reply
|
|
500
|
+
</Button>
|
|
501
|
+
<Button variant="BorderStyle" className="gap-2 bg-transparent">
|
|
502
|
+
<Forward className="h-4 w-4" />
|
|
503
|
+
Forward
|
|
504
|
+
</Button>
|
|
505
|
+
</div>
|
|
506
|
+
</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>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
)
|
|
514
|
+
}
|