@schandlergarcia/sf-web-components 1.2.6 → 1.2.8
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/package.json +2 -1
- package/scripts/postinstall.mjs +69 -93
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Account.cls +196 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/AccountHistory.cls +25 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Asset.cls +138 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Attachment.cls +35 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Case.cls +111 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Contact.cls +167 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Contract.cls +96 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Domain.cls +29 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Lead.cls +128 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Note.cls +32 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Opportunity.cls +113 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Order.cls +127 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Pricebook2.cls +47 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/PricebookEntry.cls +47 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Product2.cls +91 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/RecordType.cls +35 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Report.cls +47 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/Task.cls +79 -0
- package/src/components/library/.sfdx/tools/sobjects/standardObjects/User.cls +2318 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Account.json +2952 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/AccountHistory.json +875 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Asset.json +1699 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Attachment.json +362 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Case.json +1371 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Contact.json +2309 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Contract.json +1304 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Domain.json +293 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Lead.json +1977 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Note.json +303 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Opportunity.json +1470 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Order.json +1646 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Pricebook2.json +482 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/PricebookEntry.json +433 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Product2.json +1039 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/RecordType.json +2576 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Report.json +486 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Task.json +4296 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/User.json +30415 -0
- package/src/components/library/.sfdx/tools/soqlMetadata/typeNames.json +78 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Account.d.ts +264 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/AccountHistory.d.ts +44 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Asset.d.ts +240 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Attachment.d.ts +76 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Case.d.ts +172 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Contact.d.ts +264 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Contract.d.ts +188 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Domain.d.ts +52 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Lead.d.ts +252 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Note.d.ts +64 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Opportunity.d.ts +200 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Order.d.ts +260 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Pricebook2.d.ts +64 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/PricebookEntry.d.ts +76 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Product2.d.ts +96 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/RecordType.d.ts +64 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Report.d.ts +80 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/Task.d.ts +184 -0
- package/src/components/library/.sfdx/typings/lwc/sobjects/User.d.ts +752 -0
- package/src/components/library/cards/ActionList.jsx +38 -0
- package/src/components/library/cards/ActivityCard.jsx +56 -0
- package/src/components/library/cards/BaseCard.jsx +109 -0
- package/src/components/library/cards/CalloutCard.jsx +37 -0
- package/src/components/library/cards/ChartCard.jsx +105 -0
- package/src/components/library/cards/FeedPanel.jsx +39 -0
- package/src/components/library/cards/ListCard.jsx +193 -0
- package/src/components/library/cards/MetricCard.jsx +109 -0
- package/src/components/library/cards/MetricsStrip.jsx +78 -0
- package/src/components/library/cards/SectionCard.jsx +83 -0
- package/src/components/library/cards/SemanticMetricCard.jsx +52 -0
- package/src/components/library/cards/SemanticMetricCardWithLoading.jsx +23 -0
- package/src/components/library/cards/SemanticTableCard.jsx +48 -0
- package/src/components/library/cards/SemanticTableCardWithLoading.jsx +22 -0
- package/src/components/library/cards/StatusCard.jsx +220 -0
- package/src/components/library/cards/TableCard.jsx +337 -0
- package/src/components/library/cards/WidgetCard.jsx +90 -0
- package/src/components/library/charts/D3Chart.jsx +109 -0
- package/src/components/library/charts/D3ChartTemplates.jsx +126 -0
- package/src/components/library/charts/GeoMap.jsx +293 -0
- package/src/components/library/chat/ChatBar.jsx +256 -0
- package/src/components/library/chat/ChatInput.jsx +89 -0
- package/src/components/library/chat/ChatMessage.jsx +178 -0
- package/src/components/library/chat/ChatMessageList.jsx +73 -0
- package/src/components/library/chat/ChatPanel.jsx +97 -0
- package/src/components/library/chat/ChatSuggestions.jsx +28 -0
- package/src/components/library/chat/ChatToolCall.jsx +100 -0
- package/src/components/library/chat/ChatTypingIndicator.jsx +23 -0
- package/src/components/library/chat/ChatWelcome.jsx +43 -0
- package/src/components/library/chat/index.jsx +10 -0
- package/src/components/library/chat/useChatState.jsx +130 -0
- package/src/components/library/data/DataModeProvider.jsx +67 -0
- package/src/components/library/data/DataModeToggle.jsx +36 -0
- package/src/components/library/data/chartDataProvider.jsx +61 -0
- package/src/components/library/data/filterUtils.jsx +141 -0
- package/src/components/library/data/useDataSource.jsx +33 -0
- package/src/components/library/data/usePageFilters.jsx +99 -0
- package/src/components/library/filters/FilterBar.jsx +95 -0
- package/src/components/library/filters/SearchFilter.jsx +36 -0
- package/src/components/library/filters/SelectFilter.jsx +55 -0
- package/src/components/library/filters/ToggleFilter.jsx +52 -0
- package/src/components/library/filters/index.jsx +4 -0
- package/src/components/library/forms/FormField.jsx +291 -0
- package/src/components/library/forms/FormModal.jsx +201 -0
- package/src/components/library/forms/FormRenderer.jsx +46 -0
- package/src/components/library/forms/FormSection.jsx +69 -0
- package/src/components/library/forms/index.jsx +5 -0
- package/src/components/library/forms/useFormState.jsx +165 -0
- package/src/components/library/heroui/Accordion.jsx +26 -0
- package/src/components/library/heroui/Alert.jsx +8 -0
- package/src/components/library/heroui/Badge.jsx +8 -0
- package/src/components/library/heroui/Breadcrumbs.jsx +22 -0
- package/src/components/library/heroui/Button.jsx +58 -0
- package/src/components/library/heroui/Card.jsx +8 -0
- package/src/components/library/heroui/Collapsible.jsx +42 -0
- package/src/components/library/heroui/DatePicker.jsx +34 -0
- package/src/components/library/heroui/Dialog.jsx +37 -0
- package/src/components/library/heroui/Drawer.jsx +32 -0
- package/src/components/library/heroui/Dropdown.jsx +28 -0
- package/src/components/library/heroui/Field.jsx +51 -0
- package/src/components/library/heroui/Input.jsx +6 -0
- package/src/components/library/heroui/Kbd.jsx +8 -0
- package/src/components/library/heroui/Meter.jsx +8 -0
- package/src/components/library/heroui/Modal.jsx +32 -0
- package/src/components/library/heroui/Pagination.jsx +8 -0
- package/src/components/library/heroui/Popover.jsx +64 -0
- package/src/components/library/heroui/ProgressBar.jsx +8 -0
- package/src/components/library/heroui/ProgressCircle.jsx +8 -0
- package/src/components/library/heroui/ScrollShadow.jsx +8 -0
- package/src/components/library/heroui/Select.jsx +37 -0
- package/src/components/library/heroui/Separator.jsx +8 -0
- package/src/components/library/heroui/Skeleton.jsx +8 -0
- package/src/components/library/heroui/Tabs.jsx +26 -0
- package/src/components/library/heroui/Toast.jsx +25 -0
- package/src/components/library/heroui/Toggle.jsx +14 -0
- package/src/components/library/heroui/Tooltip.jsx +21 -0
- package/src/components/library/index.jsx +149 -0
- package/src/components/library/layout/PageContainer.jsx +11 -0
- package/src/components/library/skeletons/CardSkeleton.jsx +30 -0
- package/src/components/library/theme/AppThemeProvider.jsx +67 -0
- package/src/components/library/theme/tokens.jsx +72 -0
- package/src/components/library/ui/Alert.jsx +80 -0
- package/src/components/library/ui/Avatar.jsx +44 -0
- package/src/components/library/ui/BreadcrumbExtras.tsx +119 -0
- package/src/components/library/ui/Card.jsx +117 -0
- package/src/components/library/ui/Checkbox.jsx +17 -0
- package/src/components/library/ui/Chip.jsx +38 -0
- package/src/components/library/ui/Collapsible.tsx +31 -0
- package/src/components/library/ui/Container.jsx +56 -0
- package/src/components/library/ui/DatePicker.tsx +34 -0
- package/src/components/library/ui/Dialog.tsx +141 -0
- package/src/components/library/ui/EmptyState.jsx +46 -0
- package/src/components/library/ui/Field.tsx +82 -0
- package/src/components/library/ui/FieldGroup.jsx +17 -0
- package/src/components/library/ui/Label.jsx +22 -0
- package/src/components/library/ui/PaginationExtras.tsx +143 -0
- package/src/components/library/ui/Popover.tsx +39 -0
- package/src/components/library/ui/Select.tsx +113 -0
- package/src/components/library/ui/Spinner.jsx +64 -0
- package/src/components/library/ui/Text.jsx +46 -0
- package/src/components/library/ui/UIButton.jsx +61 -0
- package/src/components/library/ui/UIInput.jsx +21 -0
- package/src/components/workspace/ComponentRegistry.jsx +297 -0
- package/src/templates/pages/Home.tsx.template +6 -5
- package/src/templates/pages/NotFound.tsx.template +2 -2
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import BaseCard from "./BaseCard";
|
|
3
|
+
import UIInput from "../ui/UIInput";
|
|
4
|
+
import UIButton from "../ui/UIButton";
|
|
5
|
+
import UIText from "../ui/Text";
|
|
6
|
+
|
|
7
|
+
function defaultTypeFormat(type, value) {
|
|
8
|
+
if (value == null) return "";
|
|
9
|
+
if (!type) return String(value);
|
|
10
|
+
|
|
11
|
+
if (type === "currency") {
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
if (Number.isFinite(n)) return n.toLocaleString(undefined, { style: "currency", currency: "USD" });
|
|
14
|
+
}
|
|
15
|
+
if (type === "percentage") {
|
|
16
|
+
const n = Number(value);
|
|
17
|
+
if (Number.isFinite(n)) return `${n}%`;
|
|
18
|
+
}
|
|
19
|
+
if (type === "number") {
|
|
20
|
+
const n = Number(value);
|
|
21
|
+
if (Number.isFinite(n)) return n.toLocaleString();
|
|
22
|
+
}
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stableSort(data, compare) {
|
|
27
|
+
return data
|
|
28
|
+
.map((item, idx) => ({ item, idx }))
|
|
29
|
+
.sort((a, b) => {
|
|
30
|
+
const c = compare(a.item, b.item);
|
|
31
|
+
return c !== 0 ? c : a.idx - b.idx;
|
|
32
|
+
})
|
|
33
|
+
.map((x) => x.item);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function TableCard({
|
|
37
|
+
data = [],
|
|
38
|
+
columns = [],
|
|
39
|
+
title,
|
|
40
|
+
subtitle,
|
|
41
|
+
searchable = false,
|
|
42
|
+
sortable = false,
|
|
43
|
+
paginated = false,
|
|
44
|
+
selectable = false,
|
|
45
|
+
pageSize = 10,
|
|
46
|
+
actions,
|
|
47
|
+
rowActions,
|
|
48
|
+
onRowSelect,
|
|
49
|
+
onSort,
|
|
50
|
+
onSearch,
|
|
51
|
+
loading = false,
|
|
52
|
+
error,
|
|
53
|
+
emptyMessage = "No results.",
|
|
54
|
+
simulateInitialLoad = false,
|
|
55
|
+
minInitialDelayMs = 350,
|
|
56
|
+
maxInitialDelayMs = 900,
|
|
57
|
+
...cardProps
|
|
58
|
+
}) {
|
|
59
|
+
const [query, setQuery] = React.useState("");
|
|
60
|
+
const [sortKey, setSortKey] = React.useState(null);
|
|
61
|
+
const [sortDir, setSortDir] = React.useState("asc");
|
|
62
|
+
const [page, setPage] = React.useState(1);
|
|
63
|
+
const [selectedId, setSelectedId] = React.useState(null);
|
|
64
|
+
const [simLoading, setSimLoading] = React.useState(simulateInitialLoad);
|
|
65
|
+
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
if (!simulateInitialLoad) return;
|
|
68
|
+
const delay =
|
|
69
|
+
Math.floor(Math.random() * (maxInitialDelayMs - minInitialDelayMs + 1)) + minInitialDelayMs;
|
|
70
|
+
const t = setTimeout(() => setSimLoading(false), delay);
|
|
71
|
+
return () => clearTimeout(t);
|
|
72
|
+
}, [simulateInitialLoad, minInitialDelayMs, maxInitialDelayMs]);
|
|
73
|
+
|
|
74
|
+
const effectiveLoading = loading || simLoading;
|
|
75
|
+
|
|
76
|
+
const filtered = React.useMemo(() => {
|
|
77
|
+
if (!searchable || !query.trim()) return data;
|
|
78
|
+
const q = query.trim().toLowerCase();
|
|
79
|
+
return data.filter((row) =>
|
|
80
|
+
columns.some((col) => {
|
|
81
|
+
const raw = row?.[col.key];
|
|
82
|
+
if (raw == null) return false;
|
|
83
|
+
return String(raw).toLowerCase().includes(q);
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
}, [data, columns, query, searchable]);
|
|
87
|
+
|
|
88
|
+
const sorted = React.useMemo(() => {
|
|
89
|
+
if (!sortable || !sortKey) return filtered;
|
|
90
|
+
const col = columns.find((c) => c.key === sortKey);
|
|
91
|
+
if (!col) return filtered;
|
|
92
|
+
const dir = sortDir === "desc" ? -1 : 1;
|
|
93
|
+
return stableSort(filtered, (a, b) => {
|
|
94
|
+
const av = a?.[sortKey];
|
|
95
|
+
const bv = b?.[sortKey];
|
|
96
|
+
if (av == null && bv == null) return 0;
|
|
97
|
+
if (av == null) return -1 * dir;
|
|
98
|
+
if (bv == null) return 1 * dir;
|
|
99
|
+
if (typeof av === "number" && typeof bv === "number") return (av - bv) * dir;
|
|
100
|
+
return String(av).localeCompare(String(bv)) * dir;
|
|
101
|
+
});
|
|
102
|
+
}, [filtered, sortable, sortKey, sortDir, columns]);
|
|
103
|
+
|
|
104
|
+
const total = sorted.length;
|
|
105
|
+
const totalPages = paginated ? Math.max(1, Math.ceil(total / pageSize)) : 1;
|
|
106
|
+
const clampedPage = Math.min(page, totalPages);
|
|
107
|
+
|
|
108
|
+
const pageData = React.useMemo(() => {
|
|
109
|
+
if (!paginated) return sorted;
|
|
110
|
+
const start = (clampedPage - 1) * pageSize;
|
|
111
|
+
return sorted.slice(start, start + pageSize);
|
|
112
|
+
}, [sorted, paginated, clampedPage, pageSize]);
|
|
113
|
+
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
if (page !== clampedPage) setPage(clampedPage);
|
|
116
|
+
}, [page, clampedPage]);
|
|
117
|
+
|
|
118
|
+
const header = (
|
|
119
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
|
120
|
+
<div className="min-w-0">
|
|
121
|
+
{title ? (
|
|
122
|
+
<UIText as="div" size="sm" weight="medium">
|
|
123
|
+
{title}
|
|
124
|
+
</UIText>
|
|
125
|
+
) : null}
|
|
126
|
+
{subtitle ? (
|
|
127
|
+
<UIText as="div" size="xs" muted className="mt-1">
|
|
128
|
+
{subtitle}
|
|
129
|
+
</UIText>
|
|
130
|
+
) : null}
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
|
|
133
|
+
{searchable ? (
|
|
134
|
+
<div className="w-full sm:w-64">
|
|
135
|
+
<UIInput
|
|
136
|
+
value={query}
|
|
137
|
+
onChange={(e) => {
|
|
138
|
+
setQuery(e.target.value);
|
|
139
|
+
setPage(1);
|
|
140
|
+
onSearch?.(e.target.value);
|
|
141
|
+
}}
|
|
142
|
+
placeholder="Search…"
|
|
143
|
+
aria-label="Search table"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
) : null}
|
|
147
|
+
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (error) {
|
|
153
|
+
return (
|
|
154
|
+
<BaseCard
|
|
155
|
+
header={header}
|
|
156
|
+
body={
|
|
157
|
+
<div className="mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900 dark:border-rose-900/40 dark:bg-rose-950/30 dark:text-rose-100">
|
|
158
|
+
{String(error)}
|
|
159
|
+
</div>
|
|
160
|
+
}
|
|
161
|
+
{...cardProps}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const canSort = (col) => sortable && (col.sortable ?? true);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<BaseCard
|
|
170
|
+
variant="table"
|
|
171
|
+
header={header}
|
|
172
|
+
body={
|
|
173
|
+
<div className="mt-4 overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
|
|
174
|
+
<div className="w-full overflow-x-auto">
|
|
175
|
+
<table className="min-w-full border-separate border-spacing-0">
|
|
176
|
+
<thead className="bg-slate-50 dark:bg-slate-950/30">
|
|
177
|
+
<tr>
|
|
178
|
+
{columns.map((col) => {
|
|
179
|
+
const active = sortKey === col.key;
|
|
180
|
+
return (
|
|
181
|
+
<th
|
|
182
|
+
key={col.key}
|
|
183
|
+
scope="col"
|
|
184
|
+
className={[
|
|
185
|
+
"whitespace-nowrap border-b border-slate-200 px-4 py-3 text-left text-xs font-semibold text-slate-600 dark:border-slate-800 dark:text-slate-300",
|
|
186
|
+
col.className ?? ""
|
|
187
|
+
]
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.join(" ")}
|
|
190
|
+
>
|
|
191
|
+
{canSort(col) ? (
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
className="inline-flex items-center gap-2 hover:text-slate-900 dark:hover:text-slate-50"
|
|
195
|
+
onClick={() => {
|
|
196
|
+
const nextDir = active && sortDir === "asc" ? "desc" : "asc";
|
|
197
|
+
setSortKey(col.key);
|
|
198
|
+
setSortDir(nextDir);
|
|
199
|
+
onSort?.({ key: col.key, direction: nextDir });
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<span>{col.label}</span>
|
|
203
|
+
<span className="text-[10px] opacity-70">
|
|
204
|
+
{active ? (sortDir === "asc" ? "▲" : "▼") : "↕"}
|
|
205
|
+
</span>
|
|
206
|
+
</button>
|
|
207
|
+
) : (
|
|
208
|
+
col.label
|
|
209
|
+
)}
|
|
210
|
+
</th>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
{rowActions ? (
|
|
214
|
+
<th className="border-b border-slate-200 px-4 py-3 text-right text-xs font-semibold text-slate-600 dark:border-slate-800 dark:text-slate-300">
|
|
215
|
+
Actions
|
|
216
|
+
</th>
|
|
217
|
+
) : null}
|
|
218
|
+
</tr>
|
|
219
|
+
</thead>
|
|
220
|
+
|
|
221
|
+
<tbody className="bg-white dark:bg-slate-900">
|
|
222
|
+
{effectiveLoading ? (
|
|
223
|
+
Array.from({ length: Math.min(pageSize, 6) }).map((_, idx) => (
|
|
224
|
+
<tr key={idx}>
|
|
225
|
+
{columns.map((col) => (
|
|
226
|
+
<td
|
|
227
|
+
key={col.key}
|
|
228
|
+
className="border-b border-slate-200 px-4 py-3 dark:border-slate-800"
|
|
229
|
+
>
|
|
230
|
+
<div className="h-4 w-3/4 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
|
|
231
|
+
</td>
|
|
232
|
+
))}
|
|
233
|
+
{rowActions ? (
|
|
234
|
+
<td className="border-b border-slate-200 px-4 py-3 dark:border-slate-800">
|
|
235
|
+
<div className="h-4 w-10 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
|
|
236
|
+
</td>
|
|
237
|
+
) : null}
|
|
238
|
+
</tr>
|
|
239
|
+
))
|
|
240
|
+
) : pageData.length === 0 ? (
|
|
241
|
+
<tr>
|
|
242
|
+
<td
|
|
243
|
+
colSpan={columns.length + (rowActions ? 1 : 0)}
|
|
244
|
+
className="px-4 py-10 text-center text-sm text-slate-600 dark:text-slate-300"
|
|
245
|
+
>
|
|
246
|
+
{emptyMessage}
|
|
247
|
+
</td>
|
|
248
|
+
</tr>
|
|
249
|
+
) : (
|
|
250
|
+
pageData.map((row, idx) => {
|
|
251
|
+
const rowId = row?.id ?? idx;
|
|
252
|
+
const selected = selectable && selectedId === rowId;
|
|
253
|
+
return (
|
|
254
|
+
<tr
|
|
255
|
+
key={rowId}
|
|
256
|
+
className={[
|
|
257
|
+
"group",
|
|
258
|
+
selectable ? "cursor-pointer" : "",
|
|
259
|
+
selected ? "bg-brand-50 dark:bg-brand-950/30" : ""
|
|
260
|
+
]
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
.join(" ")}
|
|
263
|
+
onClick={() => {
|
|
264
|
+
if (!selectable) return;
|
|
265
|
+
setSelectedId(rowId);
|
|
266
|
+
onRowSelect?.(row);
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
{columns.map((col) => {
|
|
270
|
+
const raw = row?.[col.key];
|
|
271
|
+
const content = col.render ? col.render(raw, row) : defaultTypeFormat(col.type, raw);
|
|
272
|
+
return (
|
|
273
|
+
<td
|
|
274
|
+
key={col.key}
|
|
275
|
+
className={[
|
|
276
|
+
"border-b border-slate-200 px-4 py-3 text-sm text-slate-700 dark:border-slate-800 dark:text-slate-200",
|
|
277
|
+
col.mono ? "font-mono text-[13px]" : "",
|
|
278
|
+
col.className ?? ""
|
|
279
|
+
]
|
|
280
|
+
.filter(Boolean)
|
|
281
|
+
.join(" ")}
|
|
282
|
+
>
|
|
283
|
+
{content}
|
|
284
|
+
</td>
|
|
285
|
+
);
|
|
286
|
+
})}
|
|
287
|
+
{rowActions ? (
|
|
288
|
+
<td className="border-b border-slate-200 px-4 py-3 text-right text-sm dark:border-slate-800">
|
|
289
|
+
<div className="inline-flex items-center justify-end gap-2 opacity-100 sm:opacity-0 sm:group-hover:opacity-100">
|
|
290
|
+
{rowActions(row)}
|
|
291
|
+
</div>
|
|
292
|
+
</td>
|
|
293
|
+
) : null}
|
|
294
|
+
</tr>
|
|
295
|
+
);
|
|
296
|
+
})
|
|
297
|
+
)}
|
|
298
|
+
</tbody>
|
|
299
|
+
</table>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{paginated && !effectiveLoading ? (
|
|
303
|
+
<div className="flex flex-col gap-2 border-t border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/30 sm:flex-row sm:items-center sm:justify-between">
|
|
304
|
+
<div className="text-xs text-slate-600 dark:text-slate-300">
|
|
305
|
+
{total === 0 ? "0 results" : `Showing ${(clampedPage - 1) * pageSize + 1}-${Math.min(clampedPage * pageSize, total)} of ${total}`}
|
|
306
|
+
</div>
|
|
307
|
+
<div className="flex items-center gap-2">
|
|
308
|
+
<UIButton
|
|
309
|
+
variant="outline"
|
|
310
|
+
size="sm"
|
|
311
|
+
disabled={clampedPage <= 1}
|
|
312
|
+
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
313
|
+
>
|
|
314
|
+
Prev
|
|
315
|
+
</UIButton>
|
|
316
|
+
<div className="text-xs text-slate-600 dark:text-slate-300">
|
|
317
|
+
Page {clampedPage} / {totalPages}
|
|
318
|
+
</div>
|
|
319
|
+
<UIButton
|
|
320
|
+
variant="outline"
|
|
321
|
+
size="sm"
|
|
322
|
+
disabled={clampedPage >= totalPages}
|
|
323
|
+
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
|
324
|
+
>
|
|
325
|
+
Next
|
|
326
|
+
</UIButton>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
) : null}
|
|
330
|
+
</div>
|
|
331
|
+
}
|
|
332
|
+
{...cardProps}
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import BaseCard from "./BaseCard";
|
|
3
|
+
import UIText from "../ui/Text";
|
|
4
|
+
|
|
5
|
+
function Section({ title, actions, content, divided }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className={divided ? "border-t border-slate-200 pt-4 dark:border-slate-800" : ""}>
|
|
8
|
+
{(title || actions) ? (
|
|
9
|
+
<div className="mb-3 flex items-start justify-between gap-3">
|
|
10
|
+
{title ? (
|
|
11
|
+
<UIText as="div" size="sm" weight="medium">
|
|
12
|
+
{title}
|
|
13
|
+
</UIText>
|
|
14
|
+
) : (
|
|
15
|
+
<div />
|
|
16
|
+
)}
|
|
17
|
+
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
18
|
+
</div>
|
|
19
|
+
) : null}
|
|
20
|
+
<div>{content}</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function WidgetCard({
|
|
26
|
+
header,
|
|
27
|
+
sections = [],
|
|
28
|
+
footer,
|
|
29
|
+
divided = false,
|
|
30
|
+
collapsible = false,
|
|
31
|
+
defaultExpanded = true,
|
|
32
|
+
loading = false,
|
|
33
|
+
emptyMessage = "No sections.",
|
|
34
|
+
...cardProps
|
|
35
|
+
}) {
|
|
36
|
+
const [expanded, setExpanded] = React.useState(defaultExpanded);
|
|
37
|
+
|
|
38
|
+
const hdr = (
|
|
39
|
+
<div className="flex items-start justify-between gap-3">
|
|
40
|
+
<div className="min-w-0">{header}</div>
|
|
41
|
+
{collapsible ? (
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
className="rounded-lg border border-slate-200 bg-white px-2 py-1 text-xs font-semibold text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
|
|
45
|
+
onClick={() => setExpanded((e) => !e)}
|
|
46
|
+
aria-expanded={expanded}
|
|
47
|
+
aria-label="Toggle widget"
|
|
48
|
+
>
|
|
49
|
+
{expanded ? "Collapse" : "Expand"}
|
|
50
|
+
</button>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const body = (
|
|
56
|
+
<div className="mt-4 space-y-4">
|
|
57
|
+
{loading ? (
|
|
58
|
+
<div className="space-y-3">
|
|
59
|
+
<div className="h-4 w-1/3 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
|
|
60
|
+
<div className="h-4 w-2/3 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
|
|
61
|
+
<div className="h-4 w-1/2 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
|
|
62
|
+
</div>
|
|
63
|
+
) : expanded ? (
|
|
64
|
+
sections.length === 0 ? (
|
|
65
|
+
<div className="rounded-xl border border-dashed border-slate-300 bg-slate-50 p-6 text-center text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-950/30 dark:text-slate-300">
|
|
66
|
+
{emptyMessage}
|
|
67
|
+
</div>
|
|
68
|
+
) : (
|
|
69
|
+
sections.map((s, idx) => (
|
|
70
|
+
<Section
|
|
71
|
+
key={s?.id ?? idx}
|
|
72
|
+
title={s?.title}
|
|
73
|
+
actions={s?.actions}
|
|
74
|
+
content={s?.content}
|
|
75
|
+
divided={divided && idx !== 0}
|
|
76
|
+
/>
|
|
77
|
+
))
|
|
78
|
+
)
|
|
79
|
+
) : (
|
|
80
|
+
<div className="text-sm text-slate-600 dark:text-slate-300">Collapsed.</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{footer ? <div className={divided ? "border-t border-slate-200 pt-4 dark:border-slate-800" : ""}>{footer}</div> : null}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return <BaseCard variant="widget" header={hdr} body={body} {...cardProps} />;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal D3 chart host:
|
|
5
|
+
* - Owns the <svg> element
|
|
6
|
+
* - Computes responsive dimensions from container size
|
|
7
|
+
* - Calls renderChart(svgEl, data, dims, options)
|
|
8
|
+
*/
|
|
9
|
+
export default function D3Chart({
|
|
10
|
+
data,
|
|
11
|
+
renderChart,
|
|
12
|
+
options = {},
|
|
13
|
+
width,
|
|
14
|
+
height = 280,
|
|
15
|
+
responsive = false,
|
|
16
|
+
aspectRatio,
|
|
17
|
+
className = "",
|
|
18
|
+
style,
|
|
19
|
+
containerStyle,
|
|
20
|
+
svgStyle,
|
|
21
|
+
loading = false,
|
|
22
|
+
error,
|
|
23
|
+
ariaLabel = "Chart"
|
|
24
|
+
}) {
|
|
25
|
+
const containerRef = React.useRef(null);
|
|
26
|
+
const svgRef = React.useRef(null);
|
|
27
|
+
const [containerWidth, setContainerWidth] = React.useState(null);
|
|
28
|
+
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
if (!responsive) return;
|
|
31
|
+
const el = containerRef.current;
|
|
32
|
+
if (!el) return;
|
|
33
|
+
|
|
34
|
+
const obs = new ResizeObserver((entries) => {
|
|
35
|
+
const w = entries?.[0]?.contentRect?.width;
|
|
36
|
+
if (typeof w === "number" && Number.isFinite(w)) setContainerWidth(w);
|
|
37
|
+
});
|
|
38
|
+
obs.observe(el);
|
|
39
|
+
return () => obs.disconnect();
|
|
40
|
+
}, [responsive]);
|
|
41
|
+
|
|
42
|
+
const computedWidth = responsive ? containerWidth : width;
|
|
43
|
+
const computedHeight = React.useMemo(() => {
|
|
44
|
+
if (!responsive) return height;
|
|
45
|
+
if (!containerWidth) return height;
|
|
46
|
+
if (aspectRatio && Number.isFinite(aspectRatio) && aspectRatio > 0) return containerWidth / aspectRatio;
|
|
47
|
+
return height;
|
|
48
|
+
}, [responsive, containerWidth, height, aspectRatio]);
|
|
49
|
+
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (loading || error) return;
|
|
52
|
+
if (!renderChart) return;
|
|
53
|
+
const svgEl = svgRef.current;
|
|
54
|
+
if (!svgEl) return;
|
|
55
|
+
|
|
56
|
+
const dims = {
|
|
57
|
+
width: computedWidth ?? 0,
|
|
58
|
+
height: computedHeight ?? 0
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Avoid calling renderChart before we have a measurable width in responsive mode
|
|
62
|
+
if (responsive && (!dims.width || dims.width < 10)) return;
|
|
63
|
+
if (!dims.height || dims.height < 10) return;
|
|
64
|
+
|
|
65
|
+
renderChart(svgEl, data, dims, options);
|
|
66
|
+
}, [data, renderChart, options, computedWidth, computedHeight, responsive, loading, error]);
|
|
67
|
+
|
|
68
|
+
if (error) {
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
ref={containerRef}
|
|
72
|
+
className={["w-full rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900 dark:border-rose-900/40 dark:bg-rose-950/30 dark:text-rose-100", className]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(" ")}
|
|
75
|
+
style={containerStyle}
|
|
76
|
+
>
|
|
77
|
+
{String(error)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
ref={containerRef}
|
|
85
|
+
className={["w-full", className].filter(Boolean).join(" ")}
|
|
86
|
+
style={{ ...containerStyle, position: "relative" }}
|
|
87
|
+
>
|
|
88
|
+
{loading ? (
|
|
89
|
+
<div className="h-full w-full rounded-xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
|
90
|
+
<div className="space-y-3">
|
|
91
|
+
<div className="h-4 w-1/3 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
|
|
92
|
+
<div className="h-44 w-full animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
<svg
|
|
97
|
+
ref={svgRef}
|
|
98
|
+
role="img"
|
|
99
|
+
aria-label={ariaLabel}
|
|
100
|
+
width={computedWidth ?? width ?? "100%"}
|
|
101
|
+
height={computedHeight}
|
|
102
|
+
style={{ ...svgStyle, ...style, display: "block" }}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as d3 from "d3";
|
|
2
|
+
|
|
3
|
+
function clear(svg) {
|
|
4
|
+
d3.select(svg).selectAll("*").remove();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const D3ChartTemplates = {
|
|
8
|
+
lineChart(svg, data, dims, opts = {}) {
|
|
9
|
+
const {
|
|
10
|
+
xKey = "x",
|
|
11
|
+
yKey = "y",
|
|
12
|
+
margin = { top: 12, right: 16, bottom: 28, left: 40 },
|
|
13
|
+
stroke = "#6366F1",
|
|
14
|
+
strokeWidth = 2,
|
|
15
|
+
showAxes = true,
|
|
16
|
+
showGrid = true
|
|
17
|
+
} = opts;
|
|
18
|
+
|
|
19
|
+
clear(svg);
|
|
20
|
+
|
|
21
|
+
const width = dims.width;
|
|
22
|
+
const height = dims.height;
|
|
23
|
+
const innerW = Math.max(0, width - margin.left - margin.right);
|
|
24
|
+
const innerH = Math.max(0, height - margin.top - margin.bottom);
|
|
25
|
+
|
|
26
|
+
const g = d3.select(svg).attr("viewBox", `0 0 ${width} ${height}`).append("g").attr("transform", `translate(${margin.left},${margin.top})`);
|
|
27
|
+
|
|
28
|
+
const xs = data.map((d) => d?.[xKey]).filter((v) => v != null);
|
|
29
|
+
const ys = data.map((d) => d?.[yKey]).filter((v) => v != null);
|
|
30
|
+
|
|
31
|
+
const xDomain = d3.extent(xs);
|
|
32
|
+
const yDomain = d3.extent(ys);
|
|
33
|
+
|
|
34
|
+
const x = d3.scaleLinear().domain(xDomain).nice().range([0, innerW]);
|
|
35
|
+
const y = d3.scaleLinear().domain(yDomain).nice().range([innerH, 0]);
|
|
36
|
+
|
|
37
|
+
if (showGrid) {
|
|
38
|
+
g.append("g")
|
|
39
|
+
.attr("class", "grid")
|
|
40
|
+
.call(d3.axisLeft(y).ticks(5).tickSize(-innerW).tickFormat(""))
|
|
41
|
+
.call((grid) => grid.selectAll("line").attr("stroke", "currentColor").attr("opacity", 0.12))
|
|
42
|
+
.call((grid) => grid.selectAll("path").attr("stroke", "none"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const line = d3
|
|
46
|
+
.line()
|
|
47
|
+
.x((d) => x(d[xKey]))
|
|
48
|
+
.y((d) => y(d[yKey]))
|
|
49
|
+
.defined((d) => d?.[xKey] != null && d?.[yKey] != null);
|
|
50
|
+
|
|
51
|
+
g.append("path")
|
|
52
|
+
.datum(data)
|
|
53
|
+
.attr("fill", "none")
|
|
54
|
+
.attr("stroke", stroke)
|
|
55
|
+
.attr("stroke-width", strokeWidth)
|
|
56
|
+
.attr("d", line);
|
|
57
|
+
|
|
58
|
+
if (showAxes) {
|
|
59
|
+
g.append("g")
|
|
60
|
+
.attr("transform", `translate(0,${innerH})`)
|
|
61
|
+
.call(d3.axisBottom(x).ticks(6))
|
|
62
|
+
.call((ax) => ax.selectAll("text").attr("font-size", 10));
|
|
63
|
+
|
|
64
|
+
g.append("g")
|
|
65
|
+
.call(d3.axisLeft(y).ticks(5))
|
|
66
|
+
.call((ax) => ax.selectAll("text").attr("font-size", 10));
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
groupedBarChart(svg, data, dims, opts = {}) {
|
|
71
|
+
const {
|
|
72
|
+
groups = [],
|
|
73
|
+
margin = { top: 20, right: 20, bottom: 40, left: 55 },
|
|
74
|
+
xKey = "x",
|
|
75
|
+
colors = ["#6366F1", "#CBD5E1"],
|
|
76
|
+
barRadius = 4,
|
|
77
|
+
yFormat = ",.0f",
|
|
78
|
+
showGrid = true,
|
|
79
|
+
} = opts;
|
|
80
|
+
|
|
81
|
+
clear(svg);
|
|
82
|
+
|
|
83
|
+
const width = dims.width;
|
|
84
|
+
const height = dims.height;
|
|
85
|
+
const innerW = Math.max(0, width - margin.left - margin.right);
|
|
86
|
+
const innerH = Math.max(0, height - margin.top - margin.bottom);
|
|
87
|
+
|
|
88
|
+
const sel = d3.select(svg).attr("viewBox", `0 0 ${width} ${height}`);
|
|
89
|
+
const g = sel.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
|
|
90
|
+
|
|
91
|
+
const groupKeys = groups.length ? groups : Object.keys(data[0] || {}).filter((k) => k !== xKey);
|
|
92
|
+
|
|
93
|
+
const x0 = d3.scaleBand().domain(data.map((d) => d[xKey])).range([0, innerW]).padding(0.3);
|
|
94
|
+
const x1 = d3.scaleBand().domain(groupKeys).range([0, x0.bandwidth()]).padding(0.05);
|
|
95
|
+
const yMax = d3.max(data, (d) => d3.max(groupKeys, (k) => d[k])) * 1.15;
|
|
96
|
+
const y = d3.scaleLinear().domain([0, yMax]).range([innerH, 0]);
|
|
97
|
+
|
|
98
|
+
g.append("g")
|
|
99
|
+
.attr("transform", `translate(0,${innerH})`)
|
|
100
|
+
.call(d3.axisBottom(x0).tickSize(0))
|
|
101
|
+
.call((ax) => ax.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
|
|
102
|
+
.call((ax) => ax.select(".domain").remove());
|
|
103
|
+
|
|
104
|
+
g.append("g")
|
|
105
|
+
.call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(yFormat)).tickSize(showGrid ? -innerW : 0))
|
|
106
|
+
.call((ax) => ax.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
|
|
107
|
+
.call((ax) => ax.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
|
|
108
|
+
.call((ax) => ax.select(".domain").remove());
|
|
109
|
+
|
|
110
|
+
const rows = g.selectAll(".bar-group").data(data).join("g")
|
|
111
|
+
.attr("class", "bar-group")
|
|
112
|
+
.attr("transform", (d) => `translate(${x0(d[xKey])},0)`);
|
|
113
|
+
|
|
114
|
+
groupKeys.forEach((key, i) => {
|
|
115
|
+
rows.append("rect")
|
|
116
|
+
.attr("x", x1(key))
|
|
117
|
+
.attr("y", (d) => y(d[key]))
|
|
118
|
+
.attr("width", x1.bandwidth())
|
|
119
|
+
.attr("height", (d) => innerH - y(d[key]))
|
|
120
|
+
.attr("rx", barRadius)
|
|
121
|
+
.attr("fill", colors[i % colors.length]);
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
|