hazo_ui 2.11.0 → 2.16.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 CHANGED
@@ -5,6 +5,187 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.16.0] - 2026-05-17
9
+
10
+ ### Changed
11
+ - **Restyled `HazoUiMultiSortDialog` and `HazoUiMultiFilterDialog`** for a
12
+ modern, Linear/Notion-style appearance:
13
+ - Tighter dialog chrome: bordered header, padded body, bordered footer
14
+ bar with subtle muted background — replaces the flat, dated look.
15
+ - **Sort dialog**: each row now shows a numbered priority badge, a
16
+ segmented Asc/Desc pill (replaces the ambiguous switch), and a muted
17
+ delete affordance that reddens on hover.
18
+ - **Filter dialog**: rows have compact mathematical operator symbols
19
+ (`=`, `≠`, `>`, `<`, `≥`, `≤`) in a narrow select; inputs are 32px
20
+ tall for a denser layout; rows match the sort dialog visually.
21
+ - **Footer hierarchy fixed**: `Clear all` is a low-emphasis ghost
22
+ button on the left; `Cancel` (outline) and `Apply` (primary) are
23
+ grouped on the right — `Apply` is the rightmost / final action.
24
+ - Trigger buttons now show an active-count badge next to the icon.
25
+ - Empty state uses a dashed-border placeholder card; the "Add field"
26
+ affordance matches and disables itself when all fields are added.
27
+ - **Dialog animation**: removed `slide-in-from-left-1/2` /
28
+ `slide-out-to-left-1/2` classes from `DialogContent`. Modals now
29
+ fade + zoom in place from center — no more horizontal "drift" that
30
+ caught the dialog mid-animation looking off-screen.
31
+
32
+ ### API
33
+ - All component props (including the color-override props on both
34
+ dialogs) are unchanged; this is a visual refresh, not a contract
35
+ change. Existing consumers do not need to update call sites.
36
+
37
+ ## [2.15.1] - 2026-05-17
38
+
39
+ ### Fixed
40
+ - **SSR/CSR hydration mismatch in HazoUiTable number/currency/percent
41
+ formatters.** `Intl.NumberFormat(undefined, …)` resolved to the
42
+ runtime's host locale, which differs between Node (server) and the
43
+ browser (client) — e.g. server `$4,812,500.00` vs. client
44
+ `US$4,812,500.00` when the browser default is `en-GB`. The library
45
+ now pins to `"en-US"` so both environments produce the same string.
46
+
47
+ ### Added
48
+ - New `locale?: string` on `TableColumn` (BCP-47 tag, e.g. `"de-DE"`,
49
+ `"ja-JP"`) — opt-out for the new pinned default. Applies to `number`,
50
+ `currency`, and `percent` formatters. Date formatting was unaffected
51
+ and is unchanged (it uses `date-fns`, not `Intl`).
52
+
53
+ ## [2.15.0] - 2026-05-17
54
+
55
+ ### Added
56
+ - **Shift-click multi-sort on `HazoUiTable` headers.** Plain header
57
+ click still cycles asc → desc → none (single column). Shift-click
58
+ a header to append it as a secondary sort, cycling that column
59
+ asc → desc → removed without collapsing prior columns. Discoverable
60
+ via a `title=` tooltip on the sort button. `useTableState.cycleHeaderSort`
61
+ gains an `append?: boolean` parameter; new `cycleAppendColumn` helper
62
+ alongside the existing `cycleSingleColumn`.
63
+ - **Column-level `currency` override on `TableColumn`.** When `formatter`
64
+ is `"currency"`, the formatter reads `column.currency` (any ISO-4217
65
+ code, e.g. `"EUR"`, `"GBP"`) instead of the previously hard-coded
66
+ `"USD"`. Defaults to `"USD"` when omitted — fully backwards-compatible.
67
+ - **Structured `onLoad` error UX.** `HazoUiTableProps` gains
68
+ `error?: React.ReactNode | ((err: unknown) => React.ReactNode)`.
69
+ `useTableState` exposes `serverError` (always `null` outside server
70
+ mode). On `onLoad` rejection, the table renders the consumer's
71
+ `error` prop in place of rows; if none is provided, a default
72
+ `EmptyState` shows the thrown message. `console.warn` continues to
73
+ log every rejection. Branch added to both the desktop `TableBody`
74
+ and the mobile card view.
75
+ - Dev-app `/table` §9 demos the error state with a rejecting loader.
76
+ §2 copy mentions the new shift-click gesture.
77
+
78
+ ### Internal
79
+ - Renamed `use_table_state` → `useTableState` (function only — the
80
+ filename stays `use_table_state.ts`). Aligns with the package's
81
+ other hooks (`useMediaQuery`, `useLoadingState`, `useErrorDisplay`)
82
+ and unblocks the `react-hooks/rules-of-hooks` lint rule.
83
+ - New ESLint v9 flat config (`eslint.config.js`) so `npm run lint`
84
+ works again. Scoped to `src/**/*.{ts,tsx}` with
85
+ `@typescript-eslint` and `react-hooks` rules.
86
+
87
+ ### Notes
88
+ - The 200 ms search debounce default is unchanged — the spec §10
89
+ follow-up about tuning needs empirical data from the sister app.
90
+ Consumers can already override per-table via `searchDebounceMs`.
91
+
92
+ ## [2.14.0] - 2026-05-17
93
+
94
+ ### Added
95
+ - **`Table` shadcn primitive family.** Eight components (`Table`,
96
+ `TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`,
97
+ `TableCell`, `TableCaption`) re-exported under bare shadcn names.
98
+ Matches the canonical shadcn shape — drop-in for consumers that want
99
+ raw markup.
100
+ - **`HazoUiTable` composite.** Column-config-driven data table that
101
+ composes the primitive with an in-memory filter/sort/paginate
102
+ pipeline:
103
+ - `TableColumn` is the single source of truth for display, sort,
104
+ filter, and search metadata.
105
+ - Sortable headers cycle asc → desc → none and replace any
106
+ multi-column sort with single-column on click. Multi-column sort
107
+ via the optional `HazoUiMultiSortDialog` integration; indicators
108
+ show order numbers.
109
+ - Optional toolbar with debounced free-text search (200 ms),
110
+ `HazoUiMultiFilterDialog`, and `HazoUiMultiSortDialog`. Available
111
+ fields are derived from columns — declared once.
112
+ - Loading state via `SkeletonBar`; empty / no-results via
113
+ `EmptyState`.
114
+ - Optional pagination footer with "Showing X–Y of Z" + Prev/Next.
115
+ - Optional row click with full mouse + keyboard support (Enter,
116
+ Space) when `onRowClick` is set.
117
+ - Mobile card-per-row fallback below a configurable breakpoint
118
+ (default 768 px). Opt out with `mobileCardFallback={false}`.
119
+ - Optional `onLoad({ page, sort, filter })` for server-side data;
120
+ latest-request-wins.
121
+ - Dev-app `/table` route with eight demo sections covering every
122
+ acceptance scenario.
123
+
124
+ ### Dependencies
125
+ - No new dependencies. Composes existing `Skeleton`, `EmptyState`,
126
+ `Card`, `Input`, `Button`, `HazoUiMultiSortDialog`,
127
+ `HazoUiMultiFilterDialog`, `useMediaQuery` from this package, plus
128
+ `date-fns` and `lucide-react` (already deps).
129
+
130
+ ## [2.13.1] - 2026-05-16
131
+
132
+ ### Changed
133
+ - **`KanbanEditor` is now built on `HazoUiDialog`** (was raw shadcn `Dialog` primitives). Picks up the standardized header (with `title` + `description` so Radix's missing-`Description` warning is gone naturally), the standardized Save/Cancel footer with built-in `actionButtonLoading` spinner + `actionButtonDisabled`, and a priority-tinted full-width header bar via `headerBar` + `headerBarColor` — reads `hsl(var(--hazo-kanban-priority-{p0..p3}))` for visual continuity with the card left borders. The Save button gets a `Check` icon for clarity.
134
+ - Auto-detect default editor now appends a **Status** select at the end (sourced from the kanban's `columns`), so users can move the card between columns from the editor out of the box without configuring `editorFields`.
135
+ - Field labels rendered slightly smaller and muted (`text-xs font-medium text-muted-foreground`) for a more compact SaaS density.
136
+
137
+ ### Added
138
+ - New `'status'` value on `KanbanEditorFieldType`. Renders a `Select` pre-populated from the kanban's `columns` prop (label = `column.title`, value = `column.key`). Bound to `columnKey` by convention; saving with a different status moves the card to a new column through the consumer's `onCardSave` handler — same code path as `onMove`.
139
+ - Dev-app §7 editor declares `{ key: 'columnKey', type: 'status', label: 'Status' }` so the demo exercises the new field type.
140
+
141
+ ### Removed
142
+ - The explicit `aria-describedby={undefined}` workaround on `DialogContent` from v2.13.0 — `HazoUiDialog` exposes a proper `description` prop that satisfies Radix without the escape hatch.
143
+
144
+ ## [2.13.0] - 2026-05-16
145
+
146
+ ### Changed (breaking, but pre-publish)
147
+ - **Renamed `HazoUiBoard` family to `HazoUiKanban`.** v2.12.0 was committed to git but never `npm publish`'d, so this is a hard rename with no deprecation shim. Affected exports: `HazoUiBoard` → `HazoUiKanban`, `HazoUiBoardFilter` → `HazoUiKanbanFilter`, `applyBoardFilter` → `applyKanbanFilter`, plus the corresponding type aliases (`BoardItem` → `KanbanItem`, etc.). CSS class prefixes `cls_hazo_board_*` → `cls_hazo_kanban_*`; CSS variables `--hazo-board-*` → `--hazo-kanban-*`. Dev-app route `/board` → `/kanban`.
148
+
149
+ ### Added
150
+ - **Out-of-the-box card editor.** Each card now carries a pencil icon (top-right, opacity-0 idle, opacity-100 on hover/focus). Clicking opens a `HazoUiDialog`-shell editor. Three rendering modes:
151
+ 1. `renderCardEditor` render-prop — full control over the body.
152
+ 2. `editorFields` declarative config — library renders one row per field declaration. Six built-in types: `text`, `textarea`, `select`, `number`, `checkbox`, `priority`. Required-field gating disables Save when text/textarea/number fields are empty.
153
+ 3. Neither — library auto-detects string-typed fields on the item (skipping `id`, `columnKey`, `priority`).
154
+ - **Async save lifecycle.** `onCardSave` returns `void | Promise<void>`. A returned Promise gates dialog close and shows a "Saving…" spinner; a rejected Promise keeps the dialog open with `ctx.error` populated. Consumer is responsible for updating `items` on success (symmetric with `onMove`).
155
+ - Seven new props on `HazoUiKanbanProps<T>`: `editorFields`, `editorPriorities`, `editorTitle`, `renderCardEditor`, `hideEditorFooter`, `onCardSave`, `disableEdit`.
156
+ - Four new public types: `KanbanEditorField`, `KanbanEditorFieldType`, `KanbanEditorCtx<T>`, `KanbanSaveEvent<T>`.
157
+ - Dev-app demo at `/kanban` extended with three new sections (§6 default editor, §7 declarative editor, §8 renderCardEditor override).
158
+
159
+ ### Fixed
160
+ - Suppressed Radix `DialogContent` missing-`Description` warning in `KanbanEditor` by setting `aria-describedby={undefined}` explicitly. Caught during the v2.13.0 smoke test — every editor open emitted a console warning under React strict mode.
161
+
162
+ ### Notes
163
+ - No new CSS variables — the editor uses existing shadcn Dialog tokens and input/select/checkbox/textarea/button variables.
164
+ - The pencil icon is hidden when `disableEdit={true}` OR `onCardSave` is not provided (no point letting the user edit if nothing happens on save).
165
+ - The pencil's `pointerdown`/`mousedown`/`keydown` events stop propagation so dnd-kit's PointerSensor doesn't interpret the click as the start of a drag.
166
+ - One shared `<KanbanEditor>` instance mounts at the orchestrator level, not one Dialog per card.
167
+ - No `SETUP_CHECKLIST.md` changes (no new setup steps for consumers).
168
+
169
+ ## [2.12.0] - 2026-05-16
170
+
171
+ ### Added
172
+ - **`HazoUiBoard`** — drag-drop kanban board primitive with mobile tab-strip / desktop column-grid layout switch via pure CSS. Wraps `@dnd-kit/*` internally; consumers see only the `HazoUi*` API. Optimistic-overlay state model: `items` is consumer-controlled; failed moves snap back via `handle.revert()` carried on the `BoardMoveEvent`.
173
+ - **`HazoUiBoardFilter`** — controlled-or-uncontrolled filter bar with free-text search, multi-select category chips (`ToggleGroup type="multiple"`), and single-select priority chips. Decoupled from `HazoUiBoard` — consumer applies the filter to `items` before passing in.
174
+ - **`applyBoardFilter`** — pure helper that filters `items` by search / categories / priority. Convenience export; consumers can swap in their own filter function.
175
+ - Keyboard navigation per `@dnd-kit/accessibility` patterns (Tab focus → Space pick up → Arrow keys → Space drop → Esc cancel).
176
+ - Custom screen-reader announcements that name the action and column; override-able via the `announcements` prop.
177
+ - New CSS custom properties in both `src/styles/globals.css` (dev-app input) and `src/styles/hazo-ui.css` (shipped to `dist/styles.css`): `--hazo-board-priority-p0..p3` (HSL channels), `--hazo-board-card-bg`, `--hazo-board-card-border`, `--hazo-board-column-gap`. Apps can re-theme by overriding any variable in their cascade.
178
+ - Dev-app demo at `dev-app/app/board/page.tsx` (route `/board`) with 5 sections: default kanban, controlled filter, uncontrolled filter, keyboard-only walkthrough, and optimistic-update + revert.
179
+
180
+ ### Fixed (during the v2.12.0 cycle)
181
+ - **dev-app `next build` failure on pre-existing UI components.** `dev-app/package.json` pinned `@types/react` to `18.3.28` (workspace-root version) so type-check no longer fails in `src/components/ui/button-group.tsx` and `toggle-group.tsx` with "Type 'bigint' is not assignable to type 'ReactNode'". Pre-existing infrastructure issue surfaced during board demo wiring.
182
+
183
+ ### Notes
184
+ - Sub-components `BoardColumn` and `BoardCard` are intentionally internal — the data-driven `HazoUiBoard` API (`columns`, `items`, `renderCard`) is the only entry point.
185
+ - `HazoUiBoard` passes a `React.useId()`-derived value as `DndContext`'s `id` prop so multiple boards on one page produce stable, SSR-deterministic `aria-describedby` IDs (no hydration warnings).
186
+ - No new dependencies (`@dnd-kit/*` was already in `dependencies` from prior versions).
187
+ - No `SETUP_CHECKLIST.md` changes (no new setup steps for consumers).
188
+
8
189
  ## [2.11.0] - 2026-05-16
9
190
 
10
191
  ### Fixed
package/README.md CHANGED
@@ -202,6 +202,8 @@ 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
 
207
209
  ### State Primitives (v2.10.0)
@@ -3317,6 +3319,248 @@ Returns `{ error: string | null; setError: (v: unknown) => void; clearError: ()
3317
3319
 
3318
3320
  ---
3319
3321
 
3322
+ ## HazoUiKanban — Drag-Drop Kanban (v2.13.0+)
3323
+
3324
+ Drag-drop board with mobile tab-strip / desktop column-grid layouts,
3325
+ optimistic updates with revert, theme-able priority borders, and
3326
+ keyboard-driven DnD. Wraps `@dnd-kit` internally.
3327
+
3328
+ ### Minimal usage
3329
+
3330
+ ```tsx
3331
+ import {
3332
+ HazoUiKanban,
3333
+ HazoUiKanbanFilter,
3334
+ applyKanbanFilter,
3335
+ type KanbanColumn,
3336
+ type KanbanFilterValue,
3337
+ } from 'hazo_ui';
3338
+
3339
+ const COLUMNS: KanbanColumn[] = [
3340
+ { key: 'todo', title: 'To Do' },
3341
+ { key: 'in_progress', title: 'In Progress' },
3342
+ { key: 'blocked', title: 'Blocked' },
3343
+ { key: 'done', title: 'Done' },
3344
+ ];
3345
+
3346
+ function Actions({ initial }) {
3347
+ const [items, setItems] = useState(initial);
3348
+ const [filter, setFilter] = useState<KanbanFilterValue>({
3349
+ search: '', categories: [], priority: null,
3350
+ });
3351
+ const visible = useMemo(() => applyKanbanFilter(items, filter), [items, filter]);
3352
+
3353
+ return (
3354
+ <>
3355
+ <HazoUiKanbanFilter
3356
+ categories={['On-page SEO', 'Technical SEO', 'Content']}
3357
+ priorities={['P0', 'P1', 'P2', 'P3']}
3358
+ value={filter}
3359
+ onChange={setFilter}
3360
+ />
3361
+ <HazoUiKanban
3362
+ columns={COLUMNS}
3363
+ items={visible}
3364
+ renderCard={(action) => <ActionCard action={action} />}
3365
+ itemLabel={(action) => `action ${action.id}`}
3366
+ onMove={async (event) => {
3367
+ try {
3368
+ const res = await fetch(`/api/v1/actions/${event.itemId}`, {
3369
+ method: 'PATCH',
3370
+ body: JSON.stringify({ status: event.toColumn }),
3371
+ });
3372
+ if (!res.ok) throw new Error('PATCH failed');
3373
+ setItems(prev => prev.map(it =>
3374
+ it.id === event.itemId ? { ...it, columnKey: event.toColumn } : it,
3375
+ ));
3376
+ } catch {
3377
+ event.revert();
3378
+ }
3379
+ }}
3380
+ />
3381
+ </>
3382
+ );
3383
+ }
3384
+ ```
3385
+
3386
+ ### Optimistic update + revert
3387
+
3388
+ `onMove` fires after the library has already moved the card visually. The
3389
+ event carries `revert()` — call it when your API request fails and the
3390
+ overlay snaps back to whatever `items` says.
3391
+
3392
+ ### Theming (CSS custom properties)
3393
+
3394
+ | Variable | Default | Purpose |
3395
+ |---|---|---|
3396
+ | `--hazo-kanban-priority-p0` | `0 84% 60%` (red) | Left border for `P0` cards |
3397
+ | `--hazo-kanban-priority-p1` | `45 93% 55%` (yellow) | Left border for `P1` cards |
3398
+ | `--hazo-kanban-priority-p2` | `217 91% 60%` (blue) | Left border for `P2` cards |
3399
+ | `--hazo-kanban-priority-p3` | `220 9% 64%` (grey) | Left border for `P3` cards |
3400
+ | `--hazo-kanban-card-bg` | `var(--card)` | Card background |
3401
+ | `--hazo-kanban-card-border` | `var(--border)` | Card border |
3402
+ | `--hazo-kanban-column-gap` | `0.75rem` | Gap between columns |
3403
+
3404
+ Values are HSL channels (no `hsl()` wrapper) to match the shadcn theme
3405
+ convention. Custom priorities like `'critical'` work — define
3406
+ `--hazo-kanban-priority-critical` and pass `priority: 'critical'`.
3407
+
3408
+ ### Visual reference
3409
+
3410
+ Run `npm run dev:test-app` and visit `/board` for five demo scenarios
3411
+ (default kanban, controlled / uncontrolled filter, keyboard-only flow,
3412
+ and optimistic + revert).
3413
+
3414
+ ### Out-of-the-box card editor
3415
+
3416
+ Each card carries a pencil icon (top-right, opacity-0 idle, opacity-100
3417
+ on hover/focus). Clicking it opens a `HazoUiDialog` editor:
3418
+
3419
+ ```tsx
3420
+ <HazoUiKanban
3421
+ columns={COLUMNS}
3422
+ items={items}
3423
+ renderCard={(action) => <ActionCard action={action} />}
3424
+ // Declarative field config — library renders one row per declaration.
3425
+ editorFields={[
3426
+ { key: 'title', type: 'textarea', required: true },
3427
+ { key: 'category', type: 'select', options: CATEGORIES },
3428
+ { key: 'priority', type: 'priority' },
3429
+ ]}
3430
+ onCardSave={async (event) => {
3431
+ const res = await fetch(`/api/v1/actions/${event.itemId}`, {
3432
+ method: 'PATCH',
3433
+ body: JSON.stringify(event.next),
3434
+ });
3435
+ if (!res.ok) throw new Error('PATCH failed');
3436
+ setItems(prev => prev.map(it =>
3437
+ it.id === event.itemId ? event.next : it,
3438
+ ));
3439
+ }}
3440
+ />
3441
+ ```
3442
+
3443
+ Field types: `text`, `textarea`, `select`, `number`, `checkbox`,
3444
+ `priority` (a select pre-populated with `editorPriorities` —
3445
+ defaults to `["P0","P1","P2","P3"]`).
3446
+
3447
+ If you don't pass `editorFields`, the library auto-detects all
3448
+ string-valued fields on the item (except `id`, `columnKey`, `priority`)
3449
+ and renders each as a text input with a humanized label.
3450
+
3451
+ #### Custom form via renderCardEditor
3452
+
3453
+ When the declarative config isn't enough, replace the dialog body
3454
+ with your own form:
3455
+
3456
+ ```tsx
3457
+ <HazoUiKanban
3458
+ // ...
3459
+ renderCardEditor={(item, ctx) => (
3460
+ <MyComplexForm
3461
+ value={ctx.draft}
3462
+ onChange={(next) => ctx.setDraft(next)}
3463
+ saving={ctx.saving}
3464
+ error={ctx.error}
3465
+ />
3466
+ )}
3467
+ onCardSave={async (event) => { /* ... */ }}
3468
+ />
3469
+ ```
3470
+
3471
+ `ctx` provides `draft`, `setDraft`, `save()`, `close()`, `saving`, `error`,
3472
+ and `isDirty`. The library still renders the dialog shell, title, and
3473
+ the default Save/Cancel footer. To render your own buttons too, pass
3474
+ `hideEditorFooter={true}` and call `ctx.save()`/`ctx.close()` from
3475
+ within your form.
3476
+
3477
+ #### Save lifecycle
3478
+
3479
+ `onCardSave` returns `void | Promise<void>`. A returned Promise gates
3480
+ the dialog close and shows a `Saving…` spinner; a rejected Promise
3481
+ keeps the dialog open with `ctx.error` populated. Consumer is
3482
+ responsible for updating `items` on success — symmetric with `onMove`.
3483
+
3484
+ If `onCardSave` is not provided, the pencil is hidden entirely
3485
+ (read-only kanban). To explicitly hide editing without removing
3486
+ `onCardSave`, pass `disableEdit={true}`.
3487
+
3488
+ ---
3489
+
3490
+ ## HazoUiTable — Column-config-driven Data Table (v2.14.0+, v2.15 additions)
3491
+
3492
+ A higher-level data table that composes the shadcn `Table` primitive
3493
+ family with the existing `HazoUiMultiSortDialog` /
3494
+ `HazoUiMultiFilterDialog` and an in-memory filter / sort / paginate
3495
+ pipeline.
3496
+
3497
+ ```tsx
3498
+ import { HazoUiTable, type TableColumn } from 'hazo_ui';
3499
+
3500
+ interface Run {
3501
+ id: string;
3502
+ date: Date;
3503
+ status: 'ok' | 'fail';
3504
+ count: number;
3505
+ }
3506
+
3507
+ const columns: TableColumn<Run>[] = [
3508
+ { key: 'date', label: 'Date', sortable: true, formatter: 'date', filterType: 'date' },
3509
+ {
3510
+ key: 'status',
3511
+ label: 'Status',
3512
+ sortable: true,
3513
+ filterType: 'combobox',
3514
+ filterConfig: {
3515
+ comboboxOptions: [
3516
+ { label: 'OK', value: 'ok' },
3517
+ { label: 'Fail', value: 'fail' },
3518
+ ],
3519
+ },
3520
+ },
3521
+ { key: 'count', label: 'Count', sortable: true, formatter: 'number', align: 'right' },
3522
+ // v2.15+ — column-level currency override (defaults to USD)
3523
+ { key: 'revenue', label: 'Revenue', sortable: true, formatter: 'currency', currency: 'EUR', align: 'right' },
3524
+ ];
3525
+
3526
+ <HazoUiTable<Run>
3527
+ columns={columns}
3528
+ rows={runs}
3529
+ getRowKey={(r) => r.id}
3530
+ enableSearch
3531
+ enableSortDialog
3532
+ enableFilterDialog
3533
+ pagination={{ pageSize: 25 }}
3534
+ onRowClick={(r) => router.push(`/runs/${r.id}`)}
3535
+ />
3536
+ ```
3537
+
3538
+ Features:
3539
+
3540
+ - Header click cycles asc → desc → none. **Shift-click** a second
3541
+ header to append it as a secondary sort (v2.15). Multi-column sort
3542
+ also available via the optional sort dialog.
3543
+ - Free-text search across string-typed columns (debounced 200 ms).
3544
+ - Per-column filters via the filter dialog — declarable per column.
3545
+ - Loading state via `SkeletonBar`, empty / no-results via `EmptyState`.
3546
+ - Pagination footer with Prev / Next.
3547
+ - Row click with mouse and keyboard (Enter / Space) when `onRowClick`
3548
+ is set.
3549
+ - Mobile card-per-row fallback below `mobileBreakpoint` (default
3550
+ 768 px). Opt out with `mobileCardFallback={false}`.
3551
+ - Optional `onLoad({ page, sort, filter })` for server-driven
3552
+ pagination — latest-request-wins.
3553
+ - Structured error UX (v2.15): pass `error={node | (err) => node}`
3554
+ to render a custom failure state when `onLoad` rejects; otherwise
3555
+ the table falls back to an `EmptyState` showing the thrown message.
3556
+ - Per-column `currency` (v2.15) for ISO-4217 codes other than USD.
3557
+
3558
+ The package also re-exports the bare shadcn primitives — `Table`,
3559
+ `TableHeader`, `TableBody`, `TableFooter`, `TableHead`, `TableRow`,
3560
+ `TableCell`, `TableCaption` — for consumers that prefer raw markup.
3561
+
3562
+ ---
3563
+
3320
3564
  ## Troubleshooting
3321
3565
 
3322
3566
  ### Styles not applying (Tailwind v4)