hazo_ui 2.11.0 → 2.17.0
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/CHANGE_LOG.md +228 -0
- package/README.md +491 -0
- package/dist/index.cjs +2959 -494
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +557 -1
- package/dist/index.d.ts +557 -1
- package/dist/index.js +2904 -459
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,8 +202,12 @@ The following components support both global config and prop-level color overrid
|
|
|
202
202
|
|
|
203
203
|
- **[HazoUiConfirmDialog](#hazouiconfirmdialog)** - A compact, opinionated confirmation dialog with accent top border, variant system (destructive, warning, info, success), async loading support, and configurable buttons. Perfect for delete confirmations, unsaved changes warnings, and simple acknowledgments.
|
|
204
204
|
|
|
205
|
+
- **[HazoUiTable](#hazouitable--column-config-driven-data-table-v2140)** - A column-config-driven data table built on a shadcn `Table` primitive family. Sortable headers, debounced search, multi-column filter / sort dialogs, pagination, row click (mouse + keyboard), loading / empty / no-results states, and a card-per-row mobile fallback. Optional server-side `onLoad`.
|
|
206
|
+
|
|
205
207
|
- **[Drawer](#drawer)** - A `vaul`-backed bottom sheet primitive for mobile UIs. Pair with `useMediaQuery` to swap between `Dialog` and `Drawer` based on viewport width.
|
|
206
208
|
|
|
209
|
+
- **[Chart Primitives](#chart-primitives-v2170)** (v2.17.0) - Pure-SVG chart components for KPI cards and trend dashboards: `Sparkline`, `InverseSparkline`, `LineChart`, `MultiLineChart`, `StackedBars`, and `DateRangeSelector`. Zero third-party chart deps. Gap-aware paths, hover tooltips, and per-series endpoint markers built in.
|
|
210
|
+
|
|
207
211
|
### State Primitives (v2.10.0)
|
|
208
212
|
|
|
209
213
|
Lightweight, opinionated components for the four ubiquitous async states: **loading**, **empty**, **error**, and **success**.
|
|
@@ -3317,6 +3321,493 @@ Returns `{ error: string | null; setError: (v: unknown) => void; clearError: ()
|
|
|
3317
3321
|
|
|
3318
3322
|
---
|
|
3319
3323
|
|
|
3324
|
+
## HazoUiKanban — Drag-Drop Kanban (v2.13.0+)
|
|
3325
|
+
|
|
3326
|
+
Drag-drop board with mobile tab-strip / desktop column-grid layouts,
|
|
3327
|
+
optimistic updates with revert, theme-able priority borders, and
|
|
3328
|
+
keyboard-driven DnD. Wraps `@dnd-kit` internally.
|
|
3329
|
+
|
|
3330
|
+
### Minimal usage
|
|
3331
|
+
|
|
3332
|
+
```tsx
|
|
3333
|
+
import {
|
|
3334
|
+
HazoUiKanban,
|
|
3335
|
+
HazoUiKanbanFilter,
|
|
3336
|
+
applyKanbanFilter,
|
|
3337
|
+
type KanbanColumn,
|
|
3338
|
+
type KanbanFilterValue,
|
|
3339
|
+
} from 'hazo_ui';
|
|
3340
|
+
|
|
3341
|
+
const COLUMNS: KanbanColumn[] = [
|
|
3342
|
+
{ key: 'todo', title: 'To Do' },
|
|
3343
|
+
{ key: 'in_progress', title: 'In Progress' },
|
|
3344
|
+
{ key: 'blocked', title: 'Blocked' },
|
|
3345
|
+
{ key: 'done', title: 'Done' },
|
|
3346
|
+
];
|
|
3347
|
+
|
|
3348
|
+
function Actions({ initial }) {
|
|
3349
|
+
const [items, setItems] = useState(initial);
|
|
3350
|
+
const [filter, setFilter] = useState<KanbanFilterValue>({
|
|
3351
|
+
search: '', categories: [], priority: null,
|
|
3352
|
+
});
|
|
3353
|
+
const visible = useMemo(() => applyKanbanFilter(items, filter), [items, filter]);
|
|
3354
|
+
|
|
3355
|
+
return (
|
|
3356
|
+
<>
|
|
3357
|
+
<HazoUiKanbanFilter
|
|
3358
|
+
categories={['On-page SEO', 'Technical SEO', 'Content']}
|
|
3359
|
+
priorities={['P0', 'P1', 'P2', 'P3']}
|
|
3360
|
+
value={filter}
|
|
3361
|
+
onChange={setFilter}
|
|
3362
|
+
/>
|
|
3363
|
+
<HazoUiKanban
|
|
3364
|
+
columns={COLUMNS}
|
|
3365
|
+
items={visible}
|
|
3366
|
+
renderCard={(action) => <ActionCard action={action} />}
|
|
3367
|
+
itemLabel={(action) => `action ${action.id}`}
|
|
3368
|
+
onMove={async (event) => {
|
|
3369
|
+
try {
|
|
3370
|
+
const res = await fetch(`/api/v1/actions/${event.itemId}`, {
|
|
3371
|
+
method: 'PATCH',
|
|
3372
|
+
body: JSON.stringify({ status: event.toColumn }),
|
|
3373
|
+
});
|
|
3374
|
+
if (!res.ok) throw new Error('PATCH failed');
|
|
3375
|
+
setItems(prev => prev.map(it =>
|
|
3376
|
+
it.id === event.itemId ? { ...it, columnKey: event.toColumn } : it,
|
|
3377
|
+
));
|
|
3378
|
+
} catch {
|
|
3379
|
+
event.revert();
|
|
3380
|
+
}
|
|
3381
|
+
}}
|
|
3382
|
+
/>
|
|
3383
|
+
</>
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
```
|
|
3387
|
+
|
|
3388
|
+
### Optimistic update + revert
|
|
3389
|
+
|
|
3390
|
+
`onMove` fires after the library has already moved the card visually. The
|
|
3391
|
+
event carries `revert()` — call it when your API request fails and the
|
|
3392
|
+
overlay snaps back to whatever `items` says.
|
|
3393
|
+
|
|
3394
|
+
### Theming (CSS custom properties)
|
|
3395
|
+
|
|
3396
|
+
| Variable | Default | Purpose |
|
|
3397
|
+
|---|---|---|
|
|
3398
|
+
| `--hazo-kanban-priority-p0` | `0 84% 60%` (red) | Left border for `P0` cards |
|
|
3399
|
+
| `--hazo-kanban-priority-p1` | `45 93% 55%` (yellow) | Left border for `P1` cards |
|
|
3400
|
+
| `--hazo-kanban-priority-p2` | `217 91% 60%` (blue) | Left border for `P2` cards |
|
|
3401
|
+
| `--hazo-kanban-priority-p3` | `220 9% 64%` (grey) | Left border for `P3` cards |
|
|
3402
|
+
| `--hazo-kanban-card-bg` | `var(--card)` | Card background |
|
|
3403
|
+
| `--hazo-kanban-card-border` | `var(--border)` | Card border |
|
|
3404
|
+
| `--hazo-kanban-column-gap` | `0.75rem` | Gap between columns |
|
|
3405
|
+
|
|
3406
|
+
Values are HSL channels (no `hsl()` wrapper) to match the shadcn theme
|
|
3407
|
+
convention. Custom priorities like `'critical'` work — define
|
|
3408
|
+
`--hazo-kanban-priority-critical` and pass `priority: 'critical'`.
|
|
3409
|
+
|
|
3410
|
+
### Visual reference
|
|
3411
|
+
|
|
3412
|
+
Run `npm run dev:test-app` and visit `/board` for five demo scenarios
|
|
3413
|
+
(default kanban, controlled / uncontrolled filter, keyboard-only flow,
|
|
3414
|
+
and optimistic + revert).
|
|
3415
|
+
|
|
3416
|
+
### Out-of-the-box card editor
|
|
3417
|
+
|
|
3418
|
+
Each card carries a pencil icon (top-right, opacity-0 idle, opacity-100
|
|
3419
|
+
on hover/focus). Clicking it opens a `HazoUiDialog` editor:
|
|
3420
|
+
|
|
3421
|
+
```tsx
|
|
3422
|
+
<HazoUiKanban
|
|
3423
|
+
columns={COLUMNS}
|
|
3424
|
+
items={items}
|
|
3425
|
+
renderCard={(action) => <ActionCard action={action} />}
|
|
3426
|
+
// Declarative field config — library renders one row per declaration.
|
|
3427
|
+
editorFields={[
|
|
3428
|
+
{ key: 'title', type: 'textarea', required: true },
|
|
3429
|
+
{ key: 'category', type: 'select', options: CATEGORIES },
|
|
3430
|
+
{ key: 'priority', type: 'priority' },
|
|
3431
|
+
]}
|
|
3432
|
+
onCardSave={async (event) => {
|
|
3433
|
+
const res = await fetch(`/api/v1/actions/${event.itemId}`, {
|
|
3434
|
+
method: 'PATCH',
|
|
3435
|
+
body: JSON.stringify(event.next),
|
|
3436
|
+
});
|
|
3437
|
+
if (!res.ok) throw new Error('PATCH failed');
|
|
3438
|
+
setItems(prev => prev.map(it =>
|
|
3439
|
+
it.id === event.itemId ? event.next : it,
|
|
3440
|
+
));
|
|
3441
|
+
}}
|
|
3442
|
+
/>
|
|
3443
|
+
```
|
|
3444
|
+
|
|
3445
|
+
Field types: `text`, `textarea`, `select`, `number`, `checkbox`,
|
|
3446
|
+
`priority` (a select pre-populated with `editorPriorities` —
|
|
3447
|
+
defaults to `["P0","P1","P2","P3"]`).
|
|
3448
|
+
|
|
3449
|
+
If you don't pass `editorFields`, the library auto-detects all
|
|
3450
|
+
string-valued fields on the item (except `id`, `columnKey`, `priority`)
|
|
3451
|
+
and renders each as a text input with a humanized label.
|
|
3452
|
+
|
|
3453
|
+
#### Custom form via renderCardEditor
|
|
3454
|
+
|
|
3455
|
+
When the declarative config isn't enough, replace the dialog body
|
|
3456
|
+
with your own form:
|
|
3457
|
+
|
|
3458
|
+
```tsx
|
|
3459
|
+
<HazoUiKanban
|
|
3460
|
+
// ...
|
|
3461
|
+
renderCardEditor={(item, ctx) => (
|
|
3462
|
+
<MyComplexForm
|
|
3463
|
+
value={ctx.draft}
|
|
3464
|
+
onChange={(next) => ctx.setDraft(next)}
|
|
3465
|
+
saving={ctx.saving}
|
|
3466
|
+
error={ctx.error}
|
|
3467
|
+
/>
|
|
3468
|
+
)}
|
|
3469
|
+
onCardSave={async (event) => { /* ... */ }}
|
|
3470
|
+
/>
|
|
3471
|
+
```
|
|
3472
|
+
|
|
3473
|
+
`ctx` provides `draft`, `setDraft`, `save()`, `close()`, `saving`, `error`,
|
|
3474
|
+
and `isDirty`. The library still renders the dialog shell, title, and
|
|
3475
|
+
the default Save/Cancel footer. To render your own buttons too, pass
|
|
3476
|
+
`hideEditorFooter={true}` and call `ctx.save()`/`ctx.close()` from
|
|
3477
|
+
within your form.
|
|
3478
|
+
|
|
3479
|
+
#### Save lifecycle
|
|
3480
|
+
|
|
3481
|
+
`onCardSave` returns `void | Promise<void>`. A returned Promise gates
|
|
3482
|
+
the dialog close and shows a `Saving…` spinner; a rejected Promise
|
|
3483
|
+
keeps the dialog open with `ctx.error` populated. Consumer is
|
|
3484
|
+
responsible for updating `items` on success — symmetric with `onMove`.
|
|
3485
|
+
|
|
3486
|
+
If `onCardSave` is not provided, the pencil is hidden entirely
|
|
3487
|
+
(read-only kanban). To explicitly hide editing without removing
|
|
3488
|
+
`onCardSave`, pass `disableEdit={true}`.
|
|
3489
|
+
|
|
3490
|
+
---
|
|
3491
|
+
|
|
3492
|
+
## HazoUiTable — Column-config-driven Data Table (v2.14.0+, v2.15 additions)
|
|
3493
|
+
|
|
3494
|
+
A higher-level data table that composes the shadcn `Table` primitive
|
|
3495
|
+
family with the existing `HazoUiMultiSortDialog` /
|
|
3496
|
+
`HazoUiMultiFilterDialog` and an in-memory filter / sort / paginate
|
|
3497
|
+
pipeline.
|
|
3498
|
+
|
|
3499
|
+
```tsx
|
|
3500
|
+
import { HazoUiTable, type TableColumn } from 'hazo_ui';
|
|
3501
|
+
|
|
3502
|
+
interface Run {
|
|
3503
|
+
id: string;
|
|
3504
|
+
date: Date;
|
|
3505
|
+
status: 'ok' | 'fail';
|
|
3506
|
+
count: number;
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
const columns: TableColumn<Run>[] = [
|
|
3510
|
+
{ key: 'date', label: 'Date', sortable: true, formatter: 'date', filterType: 'date' },
|
|
3511
|
+
{
|
|
3512
|
+
key: 'status',
|
|
3513
|
+
label: 'Status',
|
|
3514
|
+
sortable: true,
|
|
3515
|
+
filterType: 'combobox',
|
|
3516
|
+
filterConfig: {
|
|
3517
|
+
comboboxOptions: [
|
|
3518
|
+
{ label: 'OK', value: 'ok' },
|
|
3519
|
+
{ label: 'Fail', value: 'fail' },
|
|
3520
|
+
],
|
|
3521
|
+
},
|
|
3522
|
+
},
|
|
3523
|
+
{ key: 'count', label: 'Count', sortable: true, formatter: 'number', align: 'right' },
|
|
3524
|
+
// v2.15+ — column-level currency override (defaults to USD)
|
|
3525
|
+
{ key: 'revenue', label: 'Revenue', sortable: true, formatter: 'currency', currency: 'EUR', align: 'right' },
|
|
3526
|
+
];
|
|
3527
|
+
|
|
3528
|
+
<HazoUiTable<Run>
|
|
3529
|
+
columns={columns}
|
|
3530
|
+
rows={runs}
|
|
3531
|
+
getRowKey={(r) => r.id}
|
|
3532
|
+
enableSearch
|
|
3533
|
+
enableSortDialog
|
|
3534
|
+
enableFilterDialog
|
|
3535
|
+
pagination={{ pageSize: 25 }}
|
|
3536
|
+
onRowClick={(r) => router.push(`/runs/${r.id}`)}
|
|
3537
|
+
/>
|
|
3538
|
+
```
|
|
3539
|
+
|
|
3540
|
+
Features:
|
|
3541
|
+
|
|
3542
|
+
- Header click cycles asc → desc → none. **Shift-click** a second
|
|
3543
|
+
header to append it as a secondary sort (v2.15). Multi-column sort
|
|
3544
|
+
also available via the optional sort dialog.
|
|
3545
|
+
- Free-text search across string-typed columns (debounced 200 ms).
|
|
3546
|
+
- Per-column filters via the filter dialog — declarable per column.
|
|
3547
|
+
- Loading state via `SkeletonBar`, empty / no-results via `EmptyState`.
|
|
3548
|
+
- Pagination footer with Prev / Next.
|
|
3549
|
+
- Row click with mouse and keyboard (Enter / Space) when `onRowClick`
|
|
3550
|
+
is set.
|
|
3551
|
+
- Mobile card-per-row fallback below `mobileBreakpoint` (default
|
|
3552
|
+
768 px). Opt out with `mobileCardFallback={false}`.
|
|
3553
|
+
- Optional `onLoad({ page, sort, filter })` for server-driven
|
|
3554
|
+
pagination — latest-request-wins.
|
|
3555
|
+
- Structured error UX (v2.15): pass `error={node | (err) => node}`
|
|
3556
|
+
to render a custom failure state when `onLoad` rejects; otherwise
|
|
3557
|
+
the table falls back to an `EmptyState` showing the thrown message.
|
|
3558
|
+
- Per-column `currency` (v2.15) for ISO-4217 codes other than USD.
|
|
3559
|
+
|
|
3560
|
+
The package also re-exports the bare shadcn primitives — `Table`,
|
|
3561
|
+
`TableHeader`, `TableBody`, `TableFooter`, `TableHead`, `TableRow`,
|
|
3562
|
+
`TableCell`, `TableCaption` — for consumers that prefer raw markup.
|
|
3563
|
+
|
|
3564
|
+
---
|
|
3565
|
+
|
|
3566
|
+
## Chart Primitives (v2.17.0)
|
|
3567
|
+
|
|
3568
|
+
A set of dependency-free, pure-SVG chart components for KPI cards,
|
|
3569
|
+
trend visualisations, and time-series dashboards. No `recharts`,
|
|
3570
|
+
no `chart.js`, no canvas — every chart is a flat `<svg>` whose math
|
|
3571
|
+
is computed in React. Hover tooltips and gap-aware paths are built in.
|
|
3572
|
+
|
|
3573
|
+
```tsx
|
|
3574
|
+
import {
|
|
3575
|
+
Sparkline,
|
|
3576
|
+
InverseSparkline,
|
|
3577
|
+
LineChart,
|
|
3578
|
+
MultiLineChart,
|
|
3579
|
+
StackedBars,
|
|
3580
|
+
DateRangeSelector,
|
|
3581
|
+
format_num,
|
|
3582
|
+
pick_x_label_indices,
|
|
3583
|
+
type ChartDataSeries,
|
|
3584
|
+
type MultiSeries,
|
|
3585
|
+
type StackedBar,
|
|
3586
|
+
type DateRangeOption,
|
|
3587
|
+
} from 'hazo_ui';
|
|
3588
|
+
```
|
|
3589
|
+
|
|
3590
|
+
### Sparkline
|
|
3591
|
+
|
|
3592
|
+
Axisless single-series trend line for KPI cards. Stretches edge-to-edge
|
|
3593
|
+
across the parent (`width: 100%`), 12% area fill, 2px endpoint dot at the
|
|
3594
|
+
latest non-null value. By design no hover — the numeric value belongs
|
|
3595
|
+
above the sparkline in the KPI card itself.
|
|
3596
|
+
|
|
3597
|
+
```tsx
|
|
3598
|
+
<Sparkline data={[2, 3, 5, 4, 7, 9, 12]} color="#10b981" height={40} />
|
|
3599
|
+
```
|
|
3600
|
+
|
|
3601
|
+
Props:
|
|
3602
|
+
|
|
3603
|
+
| Prop | Type | Default | Description |
|
|
3604
|
+
|-------------|---------------------|---------|--------------------------------------------|
|
|
3605
|
+
| `data` | `(number \| null)[]`| — | Series. `null` entries are skipped. |
|
|
3606
|
+
| `color` | `string` | — | Stroke, dot, and area-fill color. |
|
|
3607
|
+
| `height` | `number` | `40` | Pixel height. Width is always 100%. |
|
|
3608
|
+
| `className` | `string` | — | Container className passthrough. |
|
|
3609
|
+
|
|
3610
|
+
### InverseSparkline
|
|
3611
|
+
|
|
3612
|
+
Same shape as `Sparkline`, but the Y-axis is **inverted** — lower values
|
|
3613
|
+
plot higher on the screen. Built for GSC average-position trends (rank 1
|
|
3614
|
+
= best). Fixed 62×18 default to fit inline in scrollable query lists.
|
|
3615
|
+
No area fill, no dot — the line direction is the only signal.
|
|
3616
|
+
|
|
3617
|
+
```tsx
|
|
3618
|
+
<InverseSparkline data={[12, 10, 8, 5, 3]} color="#3b82f6" />
|
|
3619
|
+
```
|
|
3620
|
+
|
|
3621
|
+
Props:
|
|
3622
|
+
|
|
3623
|
+
| Prop | Type | Default | Description |
|
|
3624
|
+
|-------------|---------------------|---------|--------------------------------------------|
|
|
3625
|
+
| `data` | `(number \| null)[]`| — | Series. `null` entries are skipped. |
|
|
3626
|
+
| `color` | `string` | — | Stroke color. |
|
|
3627
|
+
| `width` | `number` | `62` | Pixel width. |
|
|
3628
|
+
| `height` | `number` | `18` | Pixel height. |
|
|
3629
|
+
| `className` | `string` | — | Container className passthrough. |
|
|
3630
|
+
|
|
3631
|
+
### LineChart
|
|
3632
|
+
|
|
3633
|
+
Full-anatomy single-series chart: three dashed gridlines at 0/50/100% of
|
|
3634
|
+
plot height, Y-axis ticks (max/mid/min) at the left, three X-axis labels
|
|
3635
|
+
(start/mid/end) below, marker dot at the last data point with a dashed
|
|
3636
|
+
guide-line back to the Y-axis and a value label rendered above-left.
|
|
3637
|
+
|
|
3638
|
+
Hover the plot area to surface a vertical cursor line and a value bubble
|
|
3639
|
+
at the nearest data index. Pass `showTooltip={false}` for static use
|
|
3640
|
+
(print export, embeds).
|
|
3641
|
+
|
|
3642
|
+
```tsx
|
|
3643
|
+
<LineChart
|
|
3644
|
+
data={[12, 14, 18, 20, 25, 30, 36, 42]}
|
|
3645
|
+
dates={['May 1', 'May 2', 'May 3', 'May 4', 'May 5', 'May 6', 'May 7', 'May 8']}
|
|
3646
|
+
color="#10b981"
|
|
3647
|
+
unit="%"
|
|
3648
|
+
/>
|
|
3649
|
+
```
|
|
3650
|
+
|
|
3651
|
+
Props:
|
|
3652
|
+
|
|
3653
|
+
| Prop | Type | Default | Description |
|
|
3654
|
+
|---------------|---------------------|---------|----------------------------------------------------------|
|
|
3655
|
+
| `data` | `(number \| null)[]`| — | Series. `null` entries are gaps in the line. |
|
|
3656
|
+
| `dates` | `string[]` | — | X-axis label per data point (length should match `data`).|
|
|
3657
|
+
| `color` | `string` | — | Stroke / area / marker color. |
|
|
3658
|
+
| `width` | `number` | `360` | viewBox width. |
|
|
3659
|
+
| `height` | `number` | `130` | viewBox height. |
|
|
3660
|
+
| `unit` | `string` | — | Suffix on the current-value label (e.g. `"%"`). |
|
|
3661
|
+
| `showTooltip` | `boolean` | `true` | Disable hover tooltip when `false`. |
|
|
3662
|
+
| `className` | `string` | — | Container className passthrough. |
|
|
3663
|
+
|
|
3664
|
+
### MultiLineChart
|
|
3665
|
+
|
|
3666
|
+
Multiple lines sharing a Y-axis. **No area fill** — multi-series uses
|
|
3667
|
+
lines only so colors don't muddy each other. Per-series endpoint marker +
|
|
3668
|
+
value label keeps the latest reading visible without hovering. Hover
|
|
3669
|
+
surfaces one cursor with a stacked bubble showing every series value at
|
|
3670
|
+
that index. Optional legend below the SVG.
|
|
3671
|
+
|
|
3672
|
+
```tsx
|
|
3673
|
+
<MultiLineChart
|
|
3674
|
+
series={[
|
|
3675
|
+
{ label: 'Indexed', color: '#10b981', data: [1, 2, 5, 12, 26, 48, 62] },
|
|
3676
|
+
{ label: 'Crawled', color: '#f59e0b', data: [3, 4, 8, 15, 22, 28, 30] },
|
|
3677
|
+
]}
|
|
3678
|
+
dates={['May 1', 'May 2', 'May 3', 'May 4', 'May 5', 'May 6', 'May 7']}
|
|
3679
|
+
/>
|
|
3680
|
+
```
|
|
3681
|
+
|
|
3682
|
+
Props:
|
|
3683
|
+
|
|
3684
|
+
| Prop | Type | Default | Description |
|
|
3685
|
+
|---------------|-----------------|---------|-------------------------------------------------|
|
|
3686
|
+
| `series` | `MultiSeries[]` | — | One entry per line (`label`, `color`, `data`). |
|
|
3687
|
+
| `dates` | `string[]` | — | X-axis labels. |
|
|
3688
|
+
| `width` | `number` | `360` | viewBox width. |
|
|
3689
|
+
| `height` | `number` | `140` | viewBox height. |
|
|
3690
|
+
| `showTooltip` | `boolean` | `true` | Disable hover tooltip when `false`. |
|
|
3691
|
+
| `showLegend` | `boolean` | `true` | Render legend below the chart. |
|
|
3692
|
+
| `className` | `string` | — | Container className passthrough. |
|
|
3693
|
+
|
|
3694
|
+
### StackedBars
|
|
3695
|
+
|
|
3696
|
+
Generic stacked-bar over time. One bar per X position; each bar split
|
|
3697
|
+
top-to-bottom into colored bands defined by `segments`. Good for
|
|
3698
|
+
distribution-over-time visuals (GSC position buckets, traffic-source mix,
|
|
3699
|
+
category breakdowns).
|
|
3700
|
+
|
|
3701
|
+
```tsx
|
|
3702
|
+
<StackedBars
|
|
3703
|
+
bars={[
|
|
3704
|
+
{ label: 'May 1', segments: [
|
|
3705
|
+
{ value: 4, color: '#10b981' },
|
|
3706
|
+
{ value: 6, color: '#3b82f6' },
|
|
3707
|
+
{ value: 2, color: '#f59e0b' },
|
|
3708
|
+
]},
|
|
3709
|
+
// ...one entry per bar
|
|
3710
|
+
]}
|
|
3711
|
+
/>
|
|
3712
|
+
```
|
|
3713
|
+
|
|
3714
|
+
Props:
|
|
3715
|
+
|
|
3716
|
+
| Prop | Type | Default | Description |
|
|
3717
|
+
|--------------|----------------|---------|-----------------------------------------------------|
|
|
3718
|
+
| `bars` | `StackedBar[]` | — | One entry per column; `segments` are top-to-bottom. |
|
|
3719
|
+
| `width` | `number` | `360` | viewBox width. |
|
|
3720
|
+
| `height` | `number` | `140` | viewBox height. |
|
|
3721
|
+
| `showYAxis` | `boolean` | `true` | Render Y-axis ticks (max / mid / 0). |
|
|
3722
|
+
| `className` | `string` | — | Container className passthrough. |
|
|
3723
|
+
|
|
3724
|
+
### DateRangeSelector
|
|
3725
|
+
|
|
3726
|
+
Framework-agnostic segmented control (`value` + `onChange`) for picking
|
|
3727
|
+
a chart's time window. The active pill is tinted with the
|
|
3728
|
+
`--primary` CSS variable; inactive pills use muted foreground colors.
|
|
3729
|
+
No router coupling — wire the value into URL state, local state, or any
|
|
3730
|
+
store yourself.
|
|
3731
|
+
|
|
3732
|
+
```tsx
|
|
3733
|
+
const [range, setRange] = React.useState('30d');
|
|
3734
|
+
|
|
3735
|
+
<DateRangeSelector
|
|
3736
|
+
value={range}
|
|
3737
|
+
onChange={setRange}
|
|
3738
|
+
options={[
|
|
3739
|
+
{ value: '7d', label: '7d' },
|
|
3740
|
+
{ value: '30d', label: '30d' },
|
|
3741
|
+
{ value: '90d', label: '90d' },
|
|
3742
|
+
]}
|
|
3743
|
+
/>
|
|
3744
|
+
```
|
|
3745
|
+
|
|
3746
|
+
Props:
|
|
3747
|
+
|
|
3748
|
+
| Prop | Type | Default | Description |
|
|
3749
|
+
|-------------|-------------------------------|----------------|--------------------------------------|
|
|
3750
|
+
| `value` | `string` | — | Currently selected option value. |
|
|
3751
|
+
| `onChange` | `(value: string) => void` | — | Fires when the user picks an option. |
|
|
3752
|
+
| `options` | `DateRangeOption[]` | — | `{ value, label }` pairs. |
|
|
3753
|
+
| `ariaLabel` | `string` | `"Date range"` | Aria label for the button group. |
|
|
3754
|
+
| `className` | `string` | — | Container className passthrough. |
|
|
3755
|
+
|
|
3756
|
+
### Helpers
|
|
3757
|
+
|
|
3758
|
+
Two pure helpers are exported for callers that want to format values or
|
|
3759
|
+
pick label positions consistently with the charts:
|
|
3760
|
+
|
|
3761
|
+
```tsx
|
|
3762
|
+
format_num(1234); // "1.2k"
|
|
3763
|
+
format_num(1_500_000); // "1.5M"
|
|
3764
|
+
format_num(5.73); // "5.7"
|
|
3765
|
+
format_num(null); // ""
|
|
3766
|
+
|
|
3767
|
+
pick_x_label_indices(14); // [0, 7, 13] — start / mid / end
|
|
3768
|
+
```
|
|
3769
|
+
|
|
3770
|
+
### Types
|
|
3771
|
+
|
|
3772
|
+
```ts
|
|
3773
|
+
type ChartDataPoint = number | null;
|
|
3774
|
+
type ChartDataSeries = ChartDataPoint[];
|
|
3775
|
+
|
|
3776
|
+
interface MultiSeries {
|
|
3777
|
+
label: string;
|
|
3778
|
+
color: string;
|
|
3779
|
+
data: ChartDataSeries;
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
interface StackedBar {
|
|
3783
|
+
label: string;
|
|
3784
|
+
segments: { value: number; color: string }[];
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
interface DateRangeOption {
|
|
3788
|
+
value: string;
|
|
3789
|
+
label: string;
|
|
3790
|
+
}
|
|
3791
|
+
```
|
|
3792
|
+
|
|
3793
|
+
### Notes
|
|
3794
|
+
|
|
3795
|
+
- **Null handling.** A `null` entry in any series is a *gap* — the line
|
|
3796
|
+
breaks rather than connecting around the missing point. The Y range is
|
|
3797
|
+
computed from non-null values only, so one missing day won't deform the
|
|
3798
|
+
chart.
|
|
3799
|
+
- **Axis label color is pinned.** Gridlines (`#2a3441`) and axis labels
|
|
3800
|
+
(`#8b949e`) are hardcoded for dark-themed dashboards. Theming via CSS
|
|
3801
|
+
variables is a follow-up if a second consumer needs it.
|
|
3802
|
+
- **All charts are Client Components.** The library bundle is
|
|
3803
|
+
`"use client"`-stamped at build time, so charts work in any Next.js app
|
|
3804
|
+
but don't get RSC-rendered.
|
|
3805
|
+
- **Hover assumes `width: 100%`.** `LineChart` / `MultiLineChart` map
|
|
3806
|
+
`clientX` to a viewBox X via `getBoundingClientRect()`. CSS transforms
|
|
3807
|
+
on the wrapper (`scale(...)`) will desync the mapping.
|
|
3808
|
+
|
|
3809
|
+
---
|
|
3810
|
+
|
|
3320
3811
|
## Troubleshooting
|
|
3321
3812
|
|
|
3322
3813
|
### Styles not applying (Tailwind v4)
|