@schandlergarcia/sf-web-components 1.0.0 → 1.0.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/.a4drules/features/command-center-dashboard-rule.md +46 -0
- package/.a4drules/skills/command-center-builder/SKILL.md +637 -0
- package/.a4drules/skills/command-center-project/SKILL.md +103 -0
- package/.a4drules/skills/component-library/SKILL.md +1025 -0
- package/.a4drules/skills/outer-app/SKILL.md +64 -0
- package/README.md +7 -7
- package/package.json +1 -1
- package/.a4drules/skills/building-data-visualization/SKILL.md +0 -72
- package/.a4drules/skills/building-data-visualization/implementation/bar-line-chart.md +0 -316
- package/.a4drules/skills/building-data-visualization/implementation/dashboard-layout.md +0 -189
- package/.a4drules/skills/building-data-visualization/implementation/donut-chart.md +0 -181
- package/.a4drules/skills/building-data-visualization/implementation/stat-card.md +0 -150
- package/.a4drules/skills/building-react-components/SKILL.md +0 -96
- package/.a4drules/skills/building-react-components/implementation/component.md +0 -78
- package/.a4drules/skills/building-react-components/implementation/header-footer.md +0 -132
- package/.a4drules/skills/building-react-components/implementation/page.md +0 -93
- package/.a4drules/skills/configuring-csp-trusted-sites/SKILL.md +0 -90
- package/.a4drules/skills/configuring-csp-trusted-sites/implementation/metadata-format.md +0 -281
- package/.a4drules/skills/configuring-webapp-metadata/SKILL.md +0 -158
- package/.a4drules/skills/creating-webapp/SKILL.md +0 -140
- package/.a4drules/skills/deploying-to-salesforce/SKILL.md +0 -226
- package/.a4drules/skills/implementing-file-upload/SKILL.md +0 -396
- package/.a4drules/skills/installing-webapp-features/SKILL.md +0 -210
- package/.a4drules/skills/managing-agentforce-conversation-client/SKILL.md +0 -186
- package/.a4drules/skills/managing-agentforce-conversation-client/references/constraints.md +0 -134
- package/.a4drules/skills/managing-agentforce-conversation-client/references/examples.md +0 -132
- package/.a4drules/skills/managing-agentforce-conversation-client/references/style-tokens.md +0 -101
- package/.a4drules/skills/managing-agentforce-conversation-client/references/troubleshooting.md +0 -57
- package/.a4drules/skills/using-salesforce-data/SKILL.md +0 -363
- package/.a4drules/skills/using-salesforce-data/graphql-search.sh +0 -139
- package/.a4drules/webapp-data.md +0 -353
- package/.a4drules/webapp-ui.md +0 -16
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: component-library
|
|
3
|
+
description: >-
|
|
4
|
+
Complete reference for the Command Center component library. Every component,
|
|
5
|
+
its exact props, data shapes, and when to use it. This is the authoritative
|
|
6
|
+
source — do not guess prop names or data formats.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Component Library
|
|
10
|
+
|
|
11
|
+
Location: `src/components/library/`. Barrel export: `@/components/library`.
|
|
12
|
+
|
|
13
|
+
Vendored from command-center-starter. Do not modify library files.
|
|
14
|
+
|
|
15
|
+
## Import Pattern
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
// Named imports from the barrel (preferred)
|
|
19
|
+
import { MetricCard, ChartCard, TableCard, ListCard } from "@/components/library";
|
|
20
|
+
|
|
21
|
+
// Default exports that need direct import
|
|
22
|
+
import useDataSource from "@/components/library/data/useDataSource";
|
|
23
|
+
import { useThemeMode } from "@/components/library/theme/AppThemeProvider";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## When to Use Each Component
|
|
29
|
+
|
|
30
|
+
| I need to show… | Use this | NOT this |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| A single KPI number | `MetricCard` | Custom div with big number |
|
|
33
|
+
| A row of 2–4 KPIs | Grid of `MetricCard` | `MetricsStrip` (for compact inline) |
|
|
34
|
+
| A data table | `TableCard` | `<table>` HTML |
|
|
35
|
+
| A scrollable item list | `ListCard` | Custom divs with `.map()` |
|
|
36
|
+
| An activity log with status icons | `ActivityCard` | ListCard (no status icons) |
|
|
37
|
+
| A chart (line, bar) | `ChartCard` + `D3Chart` | Raw `<svg>` |
|
|
38
|
+
| A donut/pie chart | `ChartCard` + `D3Chart` + custom renderChart | Any chart library |
|
|
39
|
+
| A world map with markers | `GeoMap` (direct, no ChartCard wrapper) | react-simple-maps |
|
|
40
|
+
| A callout/alert banner | `CalloutCard` | Custom colored div |
|
|
41
|
+
| A section heading | `SectionCard` | `<h2>` with custom styling |
|
|
42
|
+
| A multi-section panel | `WidgetCard` | Nested divs |
|
|
43
|
+
| Custom interactive content (expand/collapse, inline details) | `WidgetCard` (single section with arbitrary JSX) | Hand-rolled `<div className="bg-white border rounded...">` |
|
|
44
|
+
| System health status | `StatusCard` | Custom status badges |
|
|
45
|
+
| A sidebar feed | `FeedPanel` | Custom scrollable div |
|
|
46
|
+
| A command-palette AI chat | `ChatBar` | ChatPanel (that's for full-page) |
|
|
47
|
+
| A full-page AI chat | `ChatPanel` | ChatBar |
|
|
48
|
+
| Action buttons | `ActionList` or `UIButton` | Raw `<button>` |
|
|
49
|
+
| Loading placeholder | `CardSkeleton` | Custom skeleton divs |
|
|
50
|
+
| Empty state | `EmptyState` | Custom centered text |
|
|
51
|
+
| A form dialog | `FormModal` | Raw `Modal` + custom form |
|
|
52
|
+
| Inline form | `FormRenderer` + `useFormState` | Manual state + raw inputs |
|
|
53
|
+
| Filter bar with search/select/toggle | `FilterBar` | Custom filter UI |
|
|
54
|
+
| Progress indicator (linear) | `ProgressBar` | Custom div with width% |
|
|
55
|
+
| Progress indicator (circular) | `ProgressCircle` | Custom SVG circle |
|
|
56
|
+
| Collapsible sections | `Accordion` | Custom show/hide divs |
|
|
57
|
+
| Side panel / drawer | `Drawer` | Custom fixed-position div |
|
|
58
|
+
| Dropdown menu | `Dropdown` | Custom popover |
|
|
59
|
+
| Tooltip | `Tooltip` | Custom hover div |
|
|
60
|
+
| Toast notification | `toast()` function | Custom notification div |
|
|
61
|
+
| Keyboard shortcut display | `Kbd` | Custom `<kbd>` element |
|
|
62
|
+
| Scrollable container with fade | `ScrollShadow` | Custom overflow div |
|
|
63
|
+
| Tab switching inside a card | `Tabs` | Custom tab implementation |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Card Components
|
|
68
|
+
|
|
69
|
+
### MetricCard
|
|
70
|
+
**When:** Display a single KPI value (revenue, count, percentage).
|
|
71
|
+
|
|
72
|
+
```jsx
|
|
73
|
+
<MetricCard
|
|
74
|
+
title="Active Travelers" // label
|
|
75
|
+
value={6} // main display value (any)
|
|
76
|
+
subtitle="Currently on trips" // secondary text
|
|
77
|
+
change="+5.2%" // change amount string
|
|
78
|
+
changeType="positive" // "positive" | "negative" | "neutral"
|
|
79
|
+
icon={<UsersIcon className="h-5 w-5 text-engine-teal" />}
|
|
80
|
+
color="default" // "default" | "primary" | "success" | "warning" | "danger"
|
|
81
|
+
trend="vs last month" // trend label text
|
|
82
|
+
layout="default" // "default" | "compact"
|
|
83
|
+
footer={<span>...</span>} // bottom content
|
|
84
|
+
actions={<button>...</button>} // top-right actions
|
|
85
|
+
loading={false}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
**Mistakes:** ❌ `changeValue` → use `change`. ❌ `trend="positive"` → `trend` is a label, use `changeType`.
|
|
89
|
+
|
|
90
|
+
### ChartCard
|
|
91
|
+
**When:** Wrap a D3Chart visualization in a titled card.
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
<ChartCard
|
|
95
|
+
title="Spend Trend"
|
|
96
|
+
subtitle="Last 30 days"
|
|
97
|
+
height={280} // container height px (default 280)
|
|
98
|
+
chart={<D3Chart ... />} // ReactNode — the chart (REQUIRED, via prop not children)
|
|
99
|
+
legend={<div>...</div>} // below chart
|
|
100
|
+
chartType="Line Chart" // chip label
|
|
101
|
+
filters={<SelectFilter ... />} // header filters
|
|
102
|
+
timeRange={{ current: "30d", options: ["7d","30d","90d"], onChange: fn }}
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
**Critical:** Pass chart via `chart={}` prop. Height sets CSS on container — match with D3Chart `height`.
|
|
106
|
+
**Do NOT use for GeoMap** — GeoMap renders directly without a card wrapper.
|
|
107
|
+
**Do NOT pass Recharts/Chart.js components** — only `D3Chart` is allowed.
|
|
108
|
+
|
|
109
|
+
### TableCard
|
|
110
|
+
**When:** Display tabular data with sorting, search, pagination.
|
|
111
|
+
|
|
112
|
+
```jsx
|
|
113
|
+
<TableCard
|
|
114
|
+
title="Travelers"
|
|
115
|
+
subtitle="6 active"
|
|
116
|
+
data={[{ id: "1", name: "Maya", cost: 2840, status: "Active" }]}
|
|
117
|
+
columns={[
|
|
118
|
+
{ key: "name", label: "Name" },
|
|
119
|
+
{ key: "cost", label: "Cost", type: "currency" },
|
|
120
|
+
{ key: "status", label: "Status", render: (value, row) => <span>{value}</span> },
|
|
121
|
+
]}
|
|
122
|
+
searchable // shows search input
|
|
123
|
+
sortable // clickable column headers
|
|
124
|
+
paginated // pagination controls
|
|
125
|
+
pageSize={10}
|
|
126
|
+
rowActions={(row) => <button>View</button>}
|
|
127
|
+
onRowSelect={(row) => console.log(row)}
|
|
128
|
+
emptyMessage="No travelers found."
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
131
|
+
**Column types:** `"currency"` | `"percentage"` | `"number"` — auto-formatted, use before custom `render`.
|
|
132
|
+
**`render` receives `(value, row)`** — NOT `(row)`. `value` = `row[column.key]`.
|
|
133
|
+
|
|
134
|
+
### ListCard
|
|
135
|
+
**When:** Display a scrollable list of items with avatars, status badges, timestamps.
|
|
136
|
+
|
|
137
|
+
```jsx
|
|
138
|
+
<ListCard
|
|
139
|
+
title="Active Trips"
|
|
140
|
+
items={[{
|
|
141
|
+
id: "1",
|
|
142
|
+
title: "Hotel Zephyr", // or `name`
|
|
143
|
+
description: "San Francisco · Mar 23–26",
|
|
144
|
+
status: "Checked In", // shown as badge
|
|
145
|
+
timestamp: "2 min ago", // string or Date
|
|
146
|
+
avatar: undefined, // omit to auto-generate initials from title/name
|
|
147
|
+
// avatar: "https://example.com/photo.jpg", // URL string → renders <img>
|
|
148
|
+
// avatar: <Avatar initials="MR" />, // ReactNode → renders inside circle
|
|
149
|
+
value: "$2,840", // right-aligned
|
|
150
|
+
unit: "USD", // after value
|
|
151
|
+
}]}
|
|
152
|
+
maxBodyHeight={300} // enables scroll (string or number)
|
|
153
|
+
showAvatars={true}
|
|
154
|
+
showStatus={true}
|
|
155
|
+
showTimestamp={true}
|
|
156
|
+
dense={false}
|
|
157
|
+
divided={true}
|
|
158
|
+
onItemClick={(item, index) => {}}
|
|
159
|
+
itemActions={(item, index) => <button>View</button>}
|
|
160
|
+
emptyMessage="No items."
|
|
161
|
+
/>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### ActivityCard
|
|
165
|
+
**When:** Show an activity log with animated status icons (working, pending, complete, error).
|
|
166
|
+
|
|
167
|
+
```jsx
|
|
168
|
+
<ActivityCard
|
|
169
|
+
title="Today's Activity"
|
|
170
|
+
actions={[ // ⚠️ prop is "actions" NOT "items"
|
|
171
|
+
{
|
|
172
|
+
id: "1", // REQUIRED
|
|
173
|
+
status: "complete", // REQUIRED: "working" | "pending" | "complete" | "error"
|
|
174
|
+
title: "Maya checked in", // or `action` as fallback
|
|
175
|
+
subtitle: "Hotel Zephyr · SF",
|
|
176
|
+
timestamp: "2 min ago", // or `startedAt`
|
|
177
|
+
},
|
|
178
|
+
]}
|
|
179
|
+
/>
|
|
180
|
+
```
|
|
181
|
+
**Returns `null`** if actions is empty. **No `maxBodyHeight`** — for scrollable feeds use `ListCard` instead.
|
|
182
|
+
Status icons: `"complete"` → green check, `"working"` → spinning (indigo), `"pending"` → clock, `"error"` → red exclamation.
|
|
183
|
+
|
|
184
|
+
### CalloutCard
|
|
185
|
+
**When:** Highlight an alert, warning, or important notice.
|
|
186
|
+
|
|
187
|
+
```jsx
|
|
188
|
+
<CalloutCard
|
|
189
|
+
title="Late booking penalty"
|
|
190
|
+
tone="warning" // "neutral" | "success" | "warning" | "danger" | "info"
|
|
191
|
+
icon={<ExclamationTriangleIcon className="h-5 w-5" />}
|
|
192
|
+
message={ // ⚠️ use `message` NOT children
|
|
193
|
+
<div>
|
|
194
|
+
<p>Priya Nair — Booking 2 days before departure</p>
|
|
195
|
+
<button onClick={handleApprove}>Approve</button>
|
|
196
|
+
</div>
|
|
197
|
+
}
|
|
198
|
+
/>
|
|
199
|
+
```
|
|
200
|
+
**Mistakes:** ❌ `variant` → use `tone`. ❌ Children → use `message` prop.
|
|
201
|
+
|
|
202
|
+
### SectionCard
|
|
203
|
+
**When:** Display a prominent section heading or divider.
|
|
204
|
+
|
|
205
|
+
```jsx
|
|
206
|
+
<SectionCard
|
|
207
|
+
title="Eva · Agentforce Agent"
|
|
208
|
+
description="Powered by Agentforce"
|
|
209
|
+
label="LIVE" // small pill badge
|
|
210
|
+
variant="default" // "default" | "primary" | "secondary" | "accent"
|
|
211
|
+
size="md" // "sm" | "md" | "lg" | "xl"
|
|
212
|
+
isDark={false}
|
|
213
|
+
/>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### WidgetCard
|
|
217
|
+
**When:** Display multiple content sections in one card, OR wrap arbitrary custom interactive content (expand/collapse lists, inline detail panels, custom layouts) that doesn't fit other card types.
|
|
218
|
+
|
|
219
|
+
**This is the go-to card when you need custom JSX inside a card container.** Each section's `content` accepts arbitrary React nodes — use this instead of hand-rolling `<div className="bg-white border rounded ...">`.
|
|
220
|
+
|
|
221
|
+
**CRITICAL: WidgetCard does NOT accept children.** All content MUST go through the `sections` prop. If you write `<WidgetCard><div>...</div></WidgetCard>`, the children are silently ignored and the card renders "No sections." instead.
|
|
222
|
+
|
|
223
|
+
**Padding:** BaseCard already applies `p-4` around all content (header + body). Do NOT add extra `p-5`, `p-6`, etc. to your `header` or section `content` — it will cause double padding. If you need edge-to-edge content inside a section (like a divided list), use negative margins or set `padding="none"` on the card.
|
|
224
|
+
|
|
225
|
+
```jsx
|
|
226
|
+
// ❌ WRONG — children are ignored, shows "No sections."
|
|
227
|
+
<WidgetCard>
|
|
228
|
+
<div>This content will NOT render</div>
|
|
229
|
+
</WidgetCard>
|
|
230
|
+
|
|
231
|
+
// ✅ CORRECT — content goes in sections prop
|
|
232
|
+
<WidgetCard
|
|
233
|
+
sections={[{ id: "main", content: <div>This content WILL render</div> }]}
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
// Multi-section panel
|
|
237
|
+
<WidgetCard
|
|
238
|
+
header={<div>Custom header</div>}
|
|
239
|
+
sections={[
|
|
240
|
+
{ id: "1", title: "Section A", content: <div>...</div>, actions: <button>...</button> },
|
|
241
|
+
{ id: "2", title: "Section B", content: <div>...</div> },
|
|
242
|
+
]}
|
|
243
|
+
footer={<div>Footer</div>}
|
|
244
|
+
divided={true} // border between sections
|
|
245
|
+
collapsible={false}
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
// Single-section wrapper for custom interactive content (e.g., expandable list)
|
|
249
|
+
// NOTE: BaseCard already applies p-4 padding — do NOT add extra p-* to header/section content
|
|
250
|
+
<WidgetCard
|
|
251
|
+
header={<h2 className="text-lg font-semibold text-engine-text">Active Travelers</h2>}
|
|
252
|
+
sections={[{
|
|
253
|
+
id: "travelers",
|
|
254
|
+
content: (
|
|
255
|
+
<div className="divide-y divide-engine-border">
|
|
256
|
+
{travelers.map(t => (
|
|
257
|
+
<div key={t.id} onClick={() => toggle(t.id)}>
|
|
258
|
+
{/* collapsed row + expanded details — any JSX works here */}
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
),
|
|
263
|
+
}]}
|
|
264
|
+
/>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### StatusCard
|
|
268
|
+
**When:** Show system health or service status with colored indicators.
|
|
269
|
+
|
|
270
|
+
```jsx
|
|
271
|
+
<StatusCard
|
|
272
|
+
title="System Health"
|
|
273
|
+
status="operational" // "operational" | "degraded" | "outage" | "maintenance"
|
|
274
|
+
items={[
|
|
275
|
+
{ id: "1", title: "API", status: "operational" },
|
|
276
|
+
{ id: "2", title: "Database", status: "degraded", description: "High latency" },
|
|
277
|
+
]}
|
|
278
|
+
layout="list" // "list" | "grid" | "timeline"
|
|
279
|
+
showProgress={true} // shows % operational bar
|
|
280
|
+
/>
|
|
281
|
+
```
|
|
282
|
+
Status aliases: `"ok"/"healthy"/"up"` → `"operational"`, `"warn"` → `"degraded"`, `"down"/"critical"` → `"outage"`.
|
|
283
|
+
|
|
284
|
+
### FeedPanel
|
|
285
|
+
**When:** Sidebar-style scrollable feed with fixed header.
|
|
286
|
+
|
|
287
|
+
```jsx
|
|
288
|
+
<FeedPanel title="Notifications" subtitle="3 new" width={320}>
|
|
289
|
+
{children}
|
|
290
|
+
</FeedPanel>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### MetricsStrip
|
|
294
|
+
**When:** Compact horizontal row of metrics (inline, not cards).
|
|
295
|
+
|
|
296
|
+
```jsx
|
|
297
|
+
<MetricsStrip
|
|
298
|
+
title="Overview"
|
|
299
|
+
metrics={[
|
|
300
|
+
{ label: "Users", value: "1,234", trend: "+5%" },
|
|
301
|
+
{ label: "Revenue", value: "$45K", trend: "-2%" },
|
|
302
|
+
]}
|
|
303
|
+
collapsible={false}
|
|
304
|
+
/>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### ActionList
|
|
308
|
+
**When:** Row of action buttons.
|
|
309
|
+
|
|
310
|
+
```jsx
|
|
311
|
+
<ActionList
|
|
312
|
+
title="Quick Actions"
|
|
313
|
+
actions={[{ label: "Approve" }, { label: "Deny" }, { label: "Defer" }]}
|
|
314
|
+
onAction={(action) => console.log(action.label)}
|
|
315
|
+
/>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Chart Components
|
|
321
|
+
|
|
322
|
+
### D3Chart
|
|
323
|
+
**When:** Render any D3 visualization inside a ChartCard.
|
|
324
|
+
|
|
325
|
+
```jsx
|
|
326
|
+
<D3Chart
|
|
327
|
+
data={dataArray}
|
|
328
|
+
renderChart={D3ChartTemplates.lineChart} // function — NOT template prop
|
|
329
|
+
options={{ xKey: "x", yKey: "y", stroke: "#5BC8C8" }}
|
|
330
|
+
responsive={true}
|
|
331
|
+
height={280}
|
|
332
|
+
/>
|
|
333
|
+
```
|
|
334
|
+
**`renderChart` signature:** `(svgEl, data, { width, height }, options) => void`
|
|
335
|
+
|
|
336
|
+
### D3ChartTemplates
|
|
337
|
+
|
|
338
|
+
Only **two** built-in templates:
|
|
339
|
+
|
|
340
|
+
**`lineChart`** — `{ xKey, yKey, stroke, strokeWidth, margin, showAxes, showGrid }`
|
|
341
|
+
- Data: `[{ x: 1, y: 100 }]` — **numeric x required**
|
|
342
|
+
|
|
343
|
+
**`groupedBarChart`** — `{ xKey, groups, colors, barRadius, margin, yFormat, showGrid }`
|
|
344
|
+
- Data: `[{ x: "Jan", Hotels: 12000, Flights: 8000 }]` — **categorical x**
|
|
345
|
+
|
|
346
|
+
For donut, horizontal bar, area chart, etc. → write a custom `renderChart(svgEl, data, dims)` function using d3.
|
|
347
|
+
|
|
348
|
+
### GeoMap
|
|
349
|
+
**When:** Show a world map with markers, flight arcs, and overlays.
|
|
350
|
+
|
|
351
|
+
```jsx
|
|
352
|
+
<GeoMap
|
|
353
|
+
markers={[{ id: "sf", lon: -122.4, lat: 37.8, active: true, label: "SF" }]}
|
|
354
|
+
arcs={[{ id: "a1", from: [-122.4, 37.8], to: [-74.0, 40.7], progress: 0.65 }]}
|
|
355
|
+
overlays={[{ id: "wx1", center: [-97.7, 30.3], radius: 3 }]}
|
|
356
|
+
initialBounds={{ sw: [-130, 24], ne: [-65, 50], padding: 30 }}
|
|
357
|
+
theme="dark"
|
|
358
|
+
width={960} height={480}
|
|
359
|
+
zoomable
|
|
360
|
+
className="h-full w-full"
|
|
361
|
+
/>
|
|
362
|
+
```
|
|
363
|
+
**Do NOT wrap in ChartCard.** Render directly in `<div className="relative overflow-hidden rounded-xl h-[300px]">`.
|
|
364
|
+
Arc `danger: true` renders in red. Arc `progress` (0–1) shows animated dot along route.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## UI Primitives
|
|
369
|
+
|
|
370
|
+
### UIButton
|
|
371
|
+
```jsx
|
|
372
|
+
<UIButton variant="primary" size="md" onClick={fn} disabled={false} fullWidth={false}>Label</UIButton>
|
|
373
|
+
```
|
|
374
|
+
Variants: `"primary"` | `"secondary"` | `"outline"` | `"ghost"`. Sizes: `"sm"` | `"md"` | `"lg"`.
|
|
375
|
+
|
|
376
|
+
### UIText
|
|
377
|
+
```jsx
|
|
378
|
+
<UIText as="p" size="md" weight="medium" muted={false}>Text content</UIText>
|
|
379
|
+
```
|
|
380
|
+
Sizes: `"xs"` | `"sm"` | `"md"` | `"lg"` | `"xl"` | `"xxl"`. Weights: `"regular"` | `"medium"` | `"bold"`.
|
|
381
|
+
|
|
382
|
+
### UIChip
|
|
383
|
+
```jsx
|
|
384
|
+
<UIChip tone="success" size="xs">Active</UIChip>
|
|
385
|
+
```
|
|
386
|
+
Tones: `"neutral"` | `"primary"` | `"success"` | `"warning"` | `"danger"`. Sizes: `"xs"` | `"sm"`.
|
|
387
|
+
|
|
388
|
+
### Avatar
|
|
389
|
+
```jsx
|
|
390
|
+
<Avatar src="url" name="Maya R" size="md" tone="slate" />
|
|
391
|
+
<Avatar initials="MR" size="sm" />
|
|
392
|
+
<Avatar icon={<UsersIcon />} size="lg" tone="brand" />
|
|
393
|
+
```
|
|
394
|
+
Sizes: `"xs"` | `"sm"` | `"md"` | `"lg"`. Auto-generates initials from `name` if no `src`/`initials`/`icon`.
|
|
395
|
+
|
|
396
|
+
### Spinner
|
|
397
|
+
```jsx
|
|
398
|
+
<Spinner size="md" tone="brand" label="Loading" />
|
|
399
|
+
```
|
|
400
|
+
Tones: `"brand"` | `"white"` | `"muted"` | `"current"`.
|
|
401
|
+
|
|
402
|
+
### EmptyState
|
|
403
|
+
```jsx
|
|
404
|
+
<EmptyState icon={<RocketIcon />} heading="Nothing here" body="Add some data" action={<UIButton>Add</UIButton>} size="md" />
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### UICard
|
|
408
|
+
```jsx
|
|
409
|
+
<UICard padding="p-5">{children}</UICard>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### UIContainer
|
|
413
|
+
```jsx
|
|
414
|
+
<UIContainer title="Section" subtitle="Description" actions={<button>Add</button>} empty={false} emptyText="Nothing yet">{children}</UIContainer>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### UIInput
|
|
418
|
+
```jsx
|
|
419
|
+
<UIInput placeholder="Enter text..." value={val} onChange={fn} type="text" />
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### UIToggle
|
|
423
|
+
```jsx
|
|
424
|
+
<UIToggle label="Dark mode" />
|
|
425
|
+
```
|
|
426
|
+
Integrates with AppThemeProvider automatically.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Filters
|
|
431
|
+
|
|
432
|
+
### FilterBar
|
|
433
|
+
```jsx
|
|
434
|
+
<FilterBar
|
|
435
|
+
filters={[
|
|
436
|
+
{ id: "search", type: "search", placeholder: "Search..." },
|
|
437
|
+
{ id: "status", type: "select", label: "Status", options: ["All", "Active"] },
|
|
438
|
+
{ id: "flagged", type: "toggle", label: "Flagged only" },
|
|
439
|
+
]}
|
|
440
|
+
values={{ search: "", status: "All", flagged: false }}
|
|
441
|
+
onChange={(filterId, value) => {}} // ⚠️ receives (filterId, value) NOT full state
|
|
442
|
+
onReset={() => {}}
|
|
443
|
+
layout="inline" // "inline" | "stacked"
|
|
444
|
+
/>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Chat
|
|
450
|
+
|
|
451
|
+
### ChatBar (command palette — use on dashboards)
|
|
452
|
+
```jsx
|
|
453
|
+
<ChatBar
|
|
454
|
+
title="Eva · Travel Assistant"
|
|
455
|
+
placeholder="Ask Eva..."
|
|
456
|
+
suggestions={["Show spend", "Who has flags?", "Active trips"]}
|
|
457
|
+
onSend={async (userMsg, allMessages, helpers) => {
|
|
458
|
+
// Return message object to add to chat:
|
|
459
|
+
return { role: "assistant", content: "Here's what I found..." };
|
|
460
|
+
// Or stream: helpers.addMessage, helpers.appendChunk, helpers.setStreaming
|
|
461
|
+
}}
|
|
462
|
+
/>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### ChatPanel (full-page chat)
|
|
466
|
+
```jsx
|
|
467
|
+
<ChatPanel
|
|
468
|
+
title="AI Assistant"
|
|
469
|
+
onSend={async (userMsg, allMessages, helpers) => {
|
|
470
|
+
return { role: "assistant", content: "Reply" };
|
|
471
|
+
}}
|
|
472
|
+
suggestions={["Help me with...", "Show me..."]}
|
|
473
|
+
welcomeTitle="How can I help?"
|
|
474
|
+
welcomeSubtitle="Ask anything about your data."
|
|
475
|
+
showHeader={true}
|
|
476
|
+
/>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### useChatState (standalone hook)
|
|
480
|
+
```jsx
|
|
481
|
+
import { useChatState } from "@/components/library";
|
|
482
|
+
const chat = useChatState({ onSend: async (msg, history, helpers) => ({ role: "assistant", content: "Reply" }) });
|
|
483
|
+
// chat.messages, chat.sendMessage(text), chat.isLoading, chat.clearMessages, chat.retryLast
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Data
|
|
489
|
+
|
|
490
|
+
### useDataSource (DEFAULT export)
|
|
491
|
+
```jsx
|
|
492
|
+
import useDataSource from "@/components/library/data/useDataSource";
|
|
493
|
+
const data = useDataSource({ sample: SAMPLE_DATA, live: fetchLiveFn });
|
|
494
|
+
```
|
|
495
|
+
⚠️ **Default export** — `import { useDataSource }` will fail silently.
|
|
496
|
+
|
|
497
|
+
### DataModeProvider
|
|
498
|
+
Wraps app. Provides `useDataMode()` → `{ mode, isSample, isLive, toggle, setMode }`.
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Layout & Skeletons
|
|
503
|
+
|
|
504
|
+
### PageContainer
|
|
505
|
+
```jsx
|
|
506
|
+
<PageContainer>{children}</PageContainer>
|
|
507
|
+
```
|
|
508
|
+
Centered `max-w-6xl` wrapper with padding.
|
|
509
|
+
|
|
510
|
+
### CardSkeleton
|
|
511
|
+
```jsx
|
|
512
|
+
<CardSkeleton lines={3} />
|
|
513
|
+
```
|
|
514
|
+
Animated placeholder card for loading states.
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Forms
|
|
519
|
+
|
|
520
|
+
### FormRenderer
|
|
521
|
+
```jsx
|
|
522
|
+
<FormRenderer
|
|
523
|
+
sections={[{ id: "s1", title: "Details", fields: [
|
|
524
|
+
{ id: "name", type: "text", label: "Name", required: true },
|
|
525
|
+
{ id: "dept", type: "select", label: "Department", options: ["Sales", "Eng"] },
|
|
526
|
+
]}]}
|
|
527
|
+
values={{ name: "", dept: "" }}
|
|
528
|
+
errors={{}}
|
|
529
|
+
touched={{}}
|
|
530
|
+
onFieldChange={(fieldId, value) => {}}
|
|
531
|
+
onFieldBlur={(fieldId) => {}}
|
|
532
|
+
/>
|
|
533
|
+
```
|
|
534
|
+
Field types: `text`, `email`, `url`, `number`, `date`, `textarea`, `select`, `radio`, `checkbox`, `checkboxGroup`, `toggle`.
|
|
535
|
+
|
|
536
|
+
### FormModal
|
|
537
|
+
**When:** Open a modal dialog with a schema-driven form (create or edit records).
|
|
538
|
+
|
|
539
|
+
```jsx
|
|
540
|
+
<FormModal
|
|
541
|
+
isOpen={showModal}
|
|
542
|
+
onClose={() => setShowModal(false)}
|
|
543
|
+
title="New Traveler"
|
|
544
|
+
subtitle="Add a traveler to the system"
|
|
545
|
+
sections={formSections} // same schema as FormRenderer
|
|
546
|
+
initialValues={{ name: "Maya" }} // prefill for edit mode; omit for create
|
|
547
|
+
onSubmit={async (values) => {}} // async — modal auto-closes on success
|
|
548
|
+
submitLabel="Save" // default "Save"
|
|
549
|
+
cancelLabel="Cancel" // default "Cancel"
|
|
550
|
+
size="lg" // "sm" | "md" | "lg" | "xl"
|
|
551
|
+
destructive={false} // red submit button for deletions
|
|
552
|
+
minSubmitMs={4000} // min spinner duration ms
|
|
553
|
+
/>
|
|
554
|
+
```
|
|
555
|
+
Renders via `createPortal` over a backdrop. Escape key dismisses. Wraps FormRenderer + useFormState internally.
|
|
556
|
+
|
|
557
|
+
### FormSection & FormField
|
|
558
|
+
Lower-level building blocks used by FormRenderer. Rarely needed directly.
|
|
559
|
+
|
|
560
|
+
```jsx
|
|
561
|
+
// FormSection — renders a titled group of fields in 2-col grid
|
|
562
|
+
<FormSection section={sectionSchema} values={vals} errors={errs} touched={touched}
|
|
563
|
+
onFieldChange={(id, val) => {}} onFieldBlur={(id) => {}} />
|
|
564
|
+
|
|
565
|
+
// FormField — renders a single labeled input
|
|
566
|
+
<FormField field={fieldDef} value={val} error={errMsg} touched={true}
|
|
567
|
+
onChange={(val) => {}} onBlur={() => {}} />
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### useFormState
|
|
571
|
+
**When:** Manage form state without FormModal (inline forms, custom layouts).
|
|
572
|
+
|
|
573
|
+
```jsx
|
|
574
|
+
const form = useFormState({
|
|
575
|
+
initialValues: { name: "", dept: "" },
|
|
576
|
+
sections: formSections, // schema — used for defaults + validation
|
|
577
|
+
onSubmit: async (values) => {},
|
|
578
|
+
minSubmitMs: 4000, // default 4000
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// form.values, form.errors, form.touched
|
|
582
|
+
// form.isDirty, form.isValid, form.isSubmitting
|
|
583
|
+
// form.setValue(id, value), form.setValues(obj)
|
|
584
|
+
// form.setTouched(id), form.validate(), form.reset()
|
|
585
|
+
// form.handleSubmit(e) — async, validates first, enforces minSubmitMs
|
|
586
|
+
```
|
|
587
|
+
Builds defaults from schema: `checkboxGroup` → `[]`, `toggle`/`checkbox` → `false`, others → `""`. Supports custom validators via `field.validate(value, allValues)`.
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## Individual Filter Components
|
|
592
|
+
|
|
593
|
+
FilterBar composes these internally, but they can also be used standalone:
|
|
594
|
+
|
|
595
|
+
### SearchFilter
|
|
596
|
+
```jsx
|
|
597
|
+
<SearchFilter
|
|
598
|
+
value={searchText}
|
|
599
|
+
onChange={(text) => setSearchText(text)}
|
|
600
|
+
placeholder="Search travelers..."
|
|
601
|
+
className="w-64"
|
|
602
|
+
/>
|
|
603
|
+
```
|
|
604
|
+
Text input with magnifying glass icon and clear button.
|
|
605
|
+
|
|
606
|
+
### SelectFilter
|
|
607
|
+
```jsx
|
|
608
|
+
<SelectFilter
|
|
609
|
+
value={status}
|
|
610
|
+
onChange={(val) => setStatus(val)}
|
|
611
|
+
options={["All", "Active", "Inactive"]} // or [{ value: "active", label: "Active" }]
|
|
612
|
+
label="Status"
|
|
613
|
+
placeholder="Select..."
|
|
614
|
+
/>
|
|
615
|
+
```
|
|
616
|
+
Dropdown select. Value `"all"` skips filtering.
|
|
617
|
+
|
|
618
|
+
### ToggleFilter
|
|
619
|
+
```jsx
|
|
620
|
+
<ToggleFilter
|
|
621
|
+
value={flaggedOnly}
|
|
622
|
+
onChange={(isActive) => setFlaggedOnly(isActive)}
|
|
623
|
+
label="Flagged only"
|
|
624
|
+
/>
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Data Utilities
|
|
630
|
+
|
|
631
|
+
### DataModeToggle
|
|
632
|
+
**When:** Place in the app header to let users switch between sample and live data.
|
|
633
|
+
|
|
634
|
+
```jsx
|
|
635
|
+
<DataModeToggle className="ml-4" />
|
|
636
|
+
```
|
|
637
|
+
Pill button switching between "Sample" (beaker icon, amber) and "Live" (signal icon, emerald). Reads/writes to `DataModeProvider` context.
|
|
638
|
+
|
|
639
|
+
### usePageFilters
|
|
640
|
+
**When:** Complete filtering + sorting state for a data page.
|
|
641
|
+
|
|
642
|
+
```jsx
|
|
643
|
+
const {
|
|
644
|
+
values, // current filter values
|
|
645
|
+
setFilter, // (filterId, value) => void
|
|
646
|
+
resetFilters, // () => void
|
|
647
|
+
sort, // { key, direction } | null
|
|
648
|
+
setSort, // (key, direction) => void
|
|
649
|
+
toggleSort, // (key) => void — cycles asc → desc → null
|
|
650
|
+
filteredData, // data after filters
|
|
651
|
+
sortedData, // data after filters + sort (pass this to components)
|
|
652
|
+
activeFilterCount, // number of non-default filters
|
|
653
|
+
} = usePageFilters({
|
|
654
|
+
data: rawData,
|
|
655
|
+
filters: [
|
|
656
|
+
{ id: "search", type: "search", keys: ["name", "city"] },
|
|
657
|
+
{ id: "status", type: "select", key: "status", defaultValue: "all" },
|
|
658
|
+
{ id: "flagged", type: "toggle", key: "isFlagged", matchValue: true },
|
|
659
|
+
{ id: "dates", type: "dateRange", key: "createdAt" },
|
|
660
|
+
],
|
|
661
|
+
defaultSort: { key: "name", direction: "asc" },
|
|
662
|
+
});
|
|
663
|
+
```
|
|
664
|
+
Pass `sortedData` (not `filteredData`) to components. Disable `TableCard.searchable` when FilterBar provides search.
|
|
665
|
+
|
|
666
|
+
### filterUtils (pure functions)
|
|
667
|
+
```jsx
|
|
668
|
+
import { filterBySearch, filterByValue, filterByToggle, filterByDateRange, sortByKey, applyFilters } from "@/components/library";
|
|
669
|
+
|
|
670
|
+
filterBySearch(data, "maya", ["name", "city"]) // case-insensitive multi-key
|
|
671
|
+
filterByValue(data, "status", "Active") // exact match; skips "all"/""/ null
|
|
672
|
+
filterByToggle(data, "isFlagged", true, true) // when active, match key to value
|
|
673
|
+
filterByDateRange(data, "createdAt", { start, end }) // date range inclusion
|
|
674
|
+
sortByKey(data, "name", "asc") // "asc" | "desc"; handles null, numbers, strings
|
|
675
|
+
applyFilters(data, filterDefs, filterValues) // apply all filters declaratively
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## Chat Sub-Components
|
|
681
|
+
|
|
682
|
+
ChatBar and ChatPanel compose these internally, but they can be used standalone for custom chat layouts:
|
|
683
|
+
|
|
684
|
+
### ChatMessageList
|
|
685
|
+
```jsx
|
|
686
|
+
<ChatMessageList
|
|
687
|
+
messages={messages}
|
|
688
|
+
isLoading={true} // shows "Thinking" indicator
|
|
689
|
+
isStreaming={false} // shows "Generating" indicator
|
|
690
|
+
suggestions={["Show spend", "Top travelers"]}
|
|
691
|
+
onSuggestion={(text) => sendMessage(text)}
|
|
692
|
+
renderAvatar={(msg) => <Avatar ... />} // optional per-message avatar
|
|
693
|
+
/>
|
|
694
|
+
```
|
|
695
|
+
Scrollable message area with auto-scroll to latest message.
|
|
696
|
+
|
|
697
|
+
### ChatMessage
|
|
698
|
+
```jsx
|
|
699
|
+
<ChatMessage
|
|
700
|
+
message={{ id: "1", role: "assistant", content: "Here's what I found...",
|
|
701
|
+
components: [<MetricCard ... />], // inline rendered components
|
|
702
|
+
toolCalls: [{ name: "search", status: "complete", result: "..." }],
|
|
703
|
+
isStreaming: false, timestamp: "2:30 PM" }}
|
|
704
|
+
avatar={<Avatar icon={<CpuChipIcon />} />}
|
|
705
|
+
/>
|
|
706
|
+
```
|
|
707
|
+
Renders a single message bubble. Assistant messages format markdown (code blocks, bold, italic, inline code). System messages render as centered alerts.
|
|
708
|
+
|
|
709
|
+
### ChatInput
|
|
710
|
+
```jsx
|
|
711
|
+
<ChatInput
|
|
712
|
+
onSend={(content) => handleSend(content)}
|
|
713
|
+
disabled={false}
|
|
714
|
+
isLoading={isProcessing} // shows stop button instead of send
|
|
715
|
+
onStop={() => cancelRequest()}
|
|
716
|
+
placeholder="Type a message..."
|
|
717
|
+
maxRows={6} // max visible rows before scroll
|
|
718
|
+
/>
|
|
719
|
+
```
|
|
720
|
+
Auto-resizing textarea. Enter sends, Shift+Enter = newline.
|
|
721
|
+
|
|
722
|
+
### ChatTypingIndicator
|
|
723
|
+
```jsx
|
|
724
|
+
<ChatTypingIndicator label="Thinking" />
|
|
725
|
+
```
|
|
726
|
+
Animated three-dot bouncing indicator with AI icon.
|
|
727
|
+
|
|
728
|
+
### ChatSuggestions
|
|
729
|
+
```jsx
|
|
730
|
+
<ChatSuggestions
|
|
731
|
+
suggestions={["Show spend", "Active trips"]}
|
|
732
|
+
onSelect={(text) => sendMessage(text)}
|
|
733
|
+
/>
|
|
734
|
+
```
|
|
735
|
+
Row of sparkle-icon pill buttons for quick prompts. Returns null if empty.
|
|
736
|
+
|
|
737
|
+
### ChatToolCall
|
|
738
|
+
```jsx
|
|
739
|
+
<ChatToolCall
|
|
740
|
+
toolCall={{ id: "tc1", name: "queryRecords", args: { object: "Trip__c" },
|
|
741
|
+
status: "complete", result: "Found 6 records" }}
|
|
742
|
+
/>
|
|
743
|
+
```
|
|
744
|
+
Collapsible tool execution step. Status icons: spinner (running), check (complete), X (error). Color-coded by status.
|
|
745
|
+
|
|
746
|
+
### ChatWelcome
|
|
747
|
+
```jsx
|
|
748
|
+
<ChatWelcome
|
|
749
|
+
title="How can I help?"
|
|
750
|
+
subtitle="Ask me anything about your travel data."
|
|
751
|
+
suggestions={["Show spend", "Active trips"]}
|
|
752
|
+
onSuggestion={(text) => sendMessage(text)}
|
|
753
|
+
icon={<CpuChipIcon className="h-8 w-8" />}
|
|
754
|
+
/>
|
|
755
|
+
```
|
|
756
|
+
Full-screen welcome state shown before first message.
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## BaseCard
|
|
761
|
+
|
|
762
|
+
Foundation primitive that all card components extend. Rarely used directly — prefer MetricCard, ListCard, etc.
|
|
763
|
+
|
|
764
|
+
```jsx
|
|
765
|
+
<BaseCard
|
|
766
|
+
header={<div>Title area</div>}
|
|
767
|
+
body={<div>Content</div>} // or use children
|
|
768
|
+
footer={<div>Footer</div>}
|
|
769
|
+
variant="default" // "default" | "metric" | "chart" | "table" | "widget" | "status"
|
|
770
|
+
size="md" // min-height: "xs" | "sm" | "md" | "lg" | "xl" | "full"
|
|
771
|
+
padding="default" // "none" | "xs" | "sm" | "default" | "lg" | "xl"
|
|
772
|
+
shadow={true}
|
|
773
|
+
radius="2xl"
|
|
774
|
+
border={true}
|
|
775
|
+
isHoverable={false} // lift on hover
|
|
776
|
+
isPressable={false} // interactive button (requires onPress)
|
|
777
|
+
isLoading={false} // animated skeleton pulse
|
|
778
|
+
isDisabled={false}
|
|
779
|
+
isSelected={false} // ring-2 brand highlight
|
|
780
|
+
onPress={() => {}}
|
|
781
|
+
className=""
|
|
782
|
+
headerClassName=""
|
|
783
|
+
bodyClassName=""
|
|
784
|
+
footerClassName=""
|
|
785
|
+
/>
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## HeroUI Wrapper Components
|
|
791
|
+
|
|
792
|
+
Thin wrappers around `@heroui/react` components, pre-scoped for the command center's theme. Import from `@/components/library`. Use compound sub-component pattern with dot notation.
|
|
793
|
+
|
|
794
|
+
### Navigation & Layout
|
|
795
|
+
|
|
796
|
+
#### Tabs
|
|
797
|
+
```jsx
|
|
798
|
+
import { Tabs } from "@/components/library";
|
|
799
|
+
|
|
800
|
+
<Tabs aria-label="Options" variant="underlined" color="primary">
|
|
801
|
+
<Tabs.List>
|
|
802
|
+
<Tabs.Tab id="overview">Overview</Tabs.Tab>
|
|
803
|
+
<Tabs.Tab id="details">Details</Tabs.Tab>
|
|
804
|
+
</Tabs.List>
|
|
805
|
+
<Tabs.Panel id="overview">
|
|
806
|
+
<div>Overview content</div>
|
|
807
|
+
</Tabs.Panel>
|
|
808
|
+
<Tabs.Panel id="details">
|
|
809
|
+
<div>Details content</div>
|
|
810
|
+
</Tabs.Panel>
|
|
811
|
+
</Tabs>
|
|
812
|
+
```
|
|
813
|
+
**Critical:** `Tabs.Tab` MUST be inside `Tabs.List` — rendering it directly inside `Tabs` causes a "cannot be rendered outside a collection" error. Content goes in `Tabs.Panel`, not as children of `Tabs.Tab`.
|
|
814
|
+
|
|
815
|
+
Use **inside cards** for sub-section switching — never as page-level navigation.
|
|
816
|
+
|
|
817
|
+
#### Accordion
|
|
818
|
+
```jsx
|
|
819
|
+
import { Accordion } from "@/components/library";
|
|
820
|
+
|
|
821
|
+
<Accordion selectionMode="multiple" variant="bordered">
|
|
822
|
+
<Accordion.Item key="1" title="Section 1" subtitle="Optional subtitle">
|
|
823
|
+
<div>Expanded content</div>
|
|
824
|
+
</Accordion.Item>
|
|
825
|
+
</Accordion>
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
#### Breadcrumbs
|
|
829
|
+
```jsx
|
|
830
|
+
import { Breadcrumbs } from "@/components/library";
|
|
831
|
+
|
|
832
|
+
<Breadcrumbs>
|
|
833
|
+
<Breadcrumbs.Item>Home</Breadcrumbs.Item>
|
|
834
|
+
<Breadcrumbs.Item>Dashboard</Breadcrumbs.Item>
|
|
835
|
+
<Breadcrumbs.Item>Current</Breadcrumbs.Item>
|
|
836
|
+
</Breadcrumbs>
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
#### Separator
|
|
840
|
+
```jsx
|
|
841
|
+
import { Separator } from "@/components/library";
|
|
842
|
+
|
|
843
|
+
<Separator orientation="horizontal" className="my-4" />
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
#### Pagination
|
|
847
|
+
```jsx
|
|
848
|
+
import { Pagination } from "@/components/library";
|
|
849
|
+
|
|
850
|
+
<Pagination total={10} page={currentPage} onChange={setCurrentPage}
|
|
851
|
+
showControls boundaries={1} siblings={1} />
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Overlays
|
|
855
|
+
|
|
856
|
+
#### Drawer
|
|
857
|
+
```jsx
|
|
858
|
+
import { Drawer } from "@/components/library";
|
|
859
|
+
|
|
860
|
+
<Drawer isOpen={open} onOpenChange={setOpen} placement="right" size="md">
|
|
861
|
+
<Drawer.Content>
|
|
862
|
+
<Drawer.Header>Title</Drawer.Header>
|
|
863
|
+
<Drawer.Body>Content</Drawer.Body>
|
|
864
|
+
<Drawer.Footer>
|
|
865
|
+
<UIButton onClick={() => setOpen(false)}>Close</UIButton>
|
|
866
|
+
</Drawer.Footer>
|
|
867
|
+
</Drawer.Content>
|
|
868
|
+
</Drawer>
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
#### Modal
|
|
872
|
+
```jsx
|
|
873
|
+
import { Modal } from "@/components/library";
|
|
874
|
+
|
|
875
|
+
<Modal isOpen={open} onOpenChange={setOpen} size="lg">
|
|
876
|
+
<Modal.Container>
|
|
877
|
+
<Modal.Header>Title</Modal.Header>
|
|
878
|
+
<Modal.Body>Content</Modal.Body>
|
|
879
|
+
<Modal.Footer>Actions</Modal.Footer>
|
|
880
|
+
</Modal.Container>
|
|
881
|
+
</Modal>
|
|
882
|
+
```
|
|
883
|
+
For schema-driven forms, prefer `FormModal` over raw `Modal`.
|
|
884
|
+
|
|
885
|
+
#### Dropdown
|
|
886
|
+
```jsx
|
|
887
|
+
import { Dropdown } from "@/components/library";
|
|
888
|
+
|
|
889
|
+
<Dropdown>
|
|
890
|
+
<Dropdown.Trigger>
|
|
891
|
+
<UIButton>Options</UIButton>
|
|
892
|
+
</Dropdown.Trigger>
|
|
893
|
+
<Dropdown.Menu onAction={(key) => console.log(key)}>
|
|
894
|
+
<Dropdown.Item key="edit">Edit</Dropdown.Item>
|
|
895
|
+
<Dropdown.Item key="delete" className="text-danger">Delete</Dropdown.Item>
|
|
896
|
+
</Dropdown.Menu>
|
|
897
|
+
</Dropdown>
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
#### Tooltip
|
|
901
|
+
```jsx
|
|
902
|
+
import { Tooltip } from "@/components/library";
|
|
903
|
+
|
|
904
|
+
<Tooltip content="More info" placement="top">
|
|
905
|
+
<span>Hover me</span>
|
|
906
|
+
</Tooltip>
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
#### Toast
|
|
910
|
+
```jsx
|
|
911
|
+
import { toast } from "@/components/library";
|
|
912
|
+
|
|
913
|
+
// Trigger from anywhere (Toast.Provider must be in CommandCenter.tsx — already set up)
|
|
914
|
+
toast.success("Record saved");
|
|
915
|
+
toast.error("Something went wrong");
|
|
916
|
+
toast("Neutral message");
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
### Feedback
|
|
920
|
+
|
|
921
|
+
#### Alert
|
|
922
|
+
```jsx
|
|
923
|
+
import { Alert } from "@/components/library";
|
|
924
|
+
|
|
925
|
+
<Alert color="warning" title="Heads up" description="Check this out" />
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
#### Badge
|
|
929
|
+
```jsx
|
|
930
|
+
import { Badge } from "@/components/library";
|
|
931
|
+
|
|
932
|
+
<Badge content="3" color="danger">
|
|
933
|
+
<BellIcon className="h-5 w-5" />
|
|
934
|
+
</Badge>
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
#### ProgressBar
|
|
938
|
+
```jsx
|
|
939
|
+
import { ProgressBar } from "@/components/library";
|
|
940
|
+
|
|
941
|
+
<ProgressBar label="Upload" value={75} maxValue={100} color="primary" showValueLabel />
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
#### ProgressCircle
|
|
945
|
+
```jsx
|
|
946
|
+
import { ProgressCircle } from "@/components/library";
|
|
947
|
+
|
|
948
|
+
<ProgressCircle value={60} maxValue={100} color="success" showValueLabel />
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
#### Meter
|
|
952
|
+
```jsx
|
|
953
|
+
import { Meter } from "@/components/library";
|
|
954
|
+
|
|
955
|
+
<Meter label="CPU Usage" value={82} maxValue={100} color="warning" />
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
#### Skeleton
|
|
959
|
+
```jsx
|
|
960
|
+
import { Skeleton } from "@/components/library";
|
|
961
|
+
|
|
962
|
+
<Skeleton className="h-4 w-3/4 rounded-lg" />
|
|
963
|
+
```
|
|
964
|
+
For card-shaped loading states, prefer `CardSkeleton` over raw `Skeleton`.
|
|
965
|
+
|
|
966
|
+
### Pickers & Forms
|
|
967
|
+
|
|
968
|
+
#### Select
|
|
969
|
+
```jsx
|
|
970
|
+
import { Select } from "@/components/library";
|
|
971
|
+
|
|
972
|
+
<Select label="Department" placeholder="Choose..." selectedKeys={[dept]}
|
|
973
|
+
onSelectionChange={(keys) => setDept([...keys][0])}>
|
|
974
|
+
<Select.Item key="sales">Sales</Select.Item>
|
|
975
|
+
<Select.Item key="eng">Engineering</Select.Item>
|
|
976
|
+
</Select>
|
|
977
|
+
```
|
|
978
|
+
For filter dropdowns, prefer `SelectFilter` or `FilterBar` over raw `Select`.
|
|
979
|
+
|
|
980
|
+
### Utilities
|
|
981
|
+
|
|
982
|
+
#### Kbd
|
|
983
|
+
```jsx
|
|
984
|
+
import { Kbd } from "@/components/library";
|
|
985
|
+
|
|
986
|
+
<Kbd keys={["command"]}>K</Kbd> // renders ⌘K
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
#### ScrollShadow
|
|
990
|
+
```jsx
|
|
991
|
+
import { ScrollShadow } from "@/components/library";
|
|
992
|
+
|
|
993
|
+
<ScrollShadow className="h-[300px]">
|
|
994
|
+
{longContent}
|
|
995
|
+
</ScrollShadow>
|
|
996
|
+
```
|
|
997
|
+
Adds fade shadows at scroll edges to indicate more content.
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
## Do Not
|
|
1002
|
+
|
|
1003
|
+
- Import from individual library files when the barrel exports it
|
|
1004
|
+
- Modify vendored library files
|
|
1005
|
+
- Import shadcn (`@/components/ui/`) in command center code — shadcn components (`Button`, `Card`, `Input`, `Select`, `Alert`, `Skeleton`, etc.) exist for the outer app only. In command center pages, use the equivalent library component (`UIButton`, `BaseCard`/`WidgetCard`, `UIInput`, `Select`, `Alert`, `Skeleton`, etc. from `@/components/library`)
|
|
1006
|
+
- Use Lucide icons (`lucide-react`) in command center code — use Heroicons (`@heroicons/react`) instead
|
|
1007
|
+
- `import { useDataSource }` — it's a **default** export
|
|
1008
|
+
- Pass `items` to ActivityCard — use `actions`
|
|
1009
|
+
- Pass children to CalloutCard — use `message` prop
|
|
1010
|
+
- Pass `(row)` to TableCard render — it receives `(value, row)`
|
|
1011
|
+
- Use `template` prop on D3Chart — use `renderChart` function reference
|
|
1012
|
+
- Wrap GeoMap in ChartCard — render directly in a container div
|
|
1013
|
+
- Use ActivityCard for scrollable feeds — use ListCard with `maxBodyHeight`
|
|
1014
|
+
- Use raw `Modal` for form dialogs — use `FormModal` instead
|
|
1015
|
+
- Use `Tabs` as page-level navigation — only use inside cards for sub-sections
|
|
1016
|
+
- Pass `filteredData` to components — use `sortedData` from usePageFilters
|
|
1017
|
+
- Use `CardSkeleton` and `Skeleton` interchangeably — `CardSkeleton` for card placeholders, `Skeleton` for inline shapes
|
|
1018
|
+
- Recreate form state manually — use `useFormState` hook
|
|
1019
|
+
- Use `ChatPanel` on dashboards — use `ChatBar` (command palette style)
|
|
1020
|
+
- Pass children to `WidgetCard` — it ignores children and shows "No sections." Use the `sections` prop instead: `<WidgetCard sections={[{ id: "main", content: <div>...</div> }]} />`
|
|
1021
|
+
- Add `p-5`, `p-6`, etc. to WidgetCard `header` or section `content` — BaseCard already applies `p-4` padding, so extra padding causes double spacing
|
|
1022
|
+
- Pass initials strings as `avatar` in ListCard items — `avatar: "MR"` is treated as an image URL and breaks. Instead, omit `avatar` to auto-generate initials from `title`/`name`, or pass a ReactNode like `<Avatar initials="MR" />`
|
|
1023
|
+
- Hand-roll a card container (`<div className="bg-white border rounded ...">`) when `WidgetCard` with a single section can wrap your custom content
|
|
1024
|
+
- Use Recharts, Chart.js, or any third-party chart library — only `D3Chart` and `GeoMap` are allowed
|
|
1025
|
+
- Dismiss `validate:dashboard` errors as "justified" — every error has a library component fix
|