@schandlergarcia/sf-web-components 1.9.48 → 1.9.52

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