@startsimpli/ui 0.4.14 → 0.4.15

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.
Files changed (71) hide show
  1. package/README.md +457 -398
  2. package/package.json +18 -13
  3. package/src/components/__tests__/calendar-view-popup.test.tsx +42 -0
  4. package/src/components/__tests__/chat.test.tsx +129 -0
  5. package/src/components/__tests__/meetings-list.test.tsx +114 -0
  6. package/src/components/__tests__/slide-deck-viewer.test.tsx +82 -0
  7. package/src/components/__tests__/workspace.test.tsx +106 -0
  8. package/src/components/account/__tests__/account.test.tsx +5 -32
  9. package/src/components/account/change-password-form.tsx +1 -28
  10. package/src/components/calendar/calendar-view.tsx +31 -0
  11. package/src/components/calendar/index.ts +7 -0
  12. package/src/components/calendar/meetings-list.tsx +202 -0
  13. package/src/components/calendar/upcoming-meetings.tsx +5 -5
  14. package/src/components/chat/ChatComposer.tsx +113 -0
  15. package/src/components/chat/ChatMessage.tsx +81 -0
  16. package/src/components/chat/ChatThread.tsx +57 -0
  17. package/src/components/chat/index.ts +12 -0
  18. package/src/components/chat/types.ts +20 -0
  19. package/src/components/index.ts +13 -0
  20. package/src/components/slide-deck/SlideCanvas.tsx +68 -0
  21. package/src/components/slide-deck/SlideDeckViewer.tsx +144 -0
  22. package/src/components/slide-deck/SlideFilmstrip.tsx +73 -0
  23. package/src/components/slide-deck/index.ts +7 -0
  24. package/src/components/slide-deck/types.ts +18 -0
  25. package/src/components/team/DomainClaimCard.tsx +170 -0
  26. package/src/components/team/InviteMemberDialog.tsx +182 -0
  27. package/src/components/team/LeaveTeamDialog.tsx +130 -0
  28. package/src/components/team/MembersTable.tsx +138 -0
  29. package/src/components/team/OrgSwitcher.tsx +68 -0
  30. package/src/components/team/PendingInvitationCallout.tsx +106 -0
  31. package/src/components/team/RoleSelector.tsx +68 -0
  32. package/src/components/team/__tests__/team-components.test.tsx +352 -0
  33. package/src/components/team/domain-claim-card-default-class-names.ts +45 -0
  34. package/src/components/team/index.ts +57 -0
  35. package/src/components/team/invite-member-dialog-default-class-names.ts +41 -0
  36. package/src/components/team/leave-team-dialog-default-class-names.ts +33 -0
  37. package/src/components/team/members-table-default-class-names.ts +39 -0
  38. package/src/components/team/org-switcher-default-class-names.ts +13 -0
  39. package/src/components/team/pending-invitation-callout-default-class-names.ts +22 -0
  40. package/src/components/team/role-selector-default-class-names.ts +11 -0
  41. package/src/components/team/types.ts +97 -0
  42. package/src/components/workflows/ExecNodeDetails.tsx +83 -0
  43. package/src/components/workflows/ExecutionTimeline.tsx +146 -0
  44. package/src/components/workflows/NodeInspector.tsx +257 -0
  45. package/src/components/workflows/NodePalette.tsx +119 -0
  46. package/src/components/workflows/WorkflowCanvas.tsx +113 -0
  47. package/src/components/workflows/WorkflowEdge.tsx +65 -0
  48. package/src/components/workflows/WorkflowEditor.tsx +130 -0
  49. package/src/components/workflows/WorkflowNode.tsx +198 -0
  50. package/src/components/workflows/WorkflowRunViewer.tsx +81 -0
  51. package/src/components/workflows/__tests__/ExecutionTimeline.test.tsx +99 -0
  52. package/src/components/workflows/__tests__/NodeInspector.test.tsx +74 -0
  53. package/src/components/workflows/__tests__/NodePalette.test.tsx +46 -0
  54. package/src/components/workflows/__tests__/WorkflowCanvas.test.tsx +59 -0
  55. package/src/components/workflows/__tests__/WorkflowNode.test.tsx +92 -0
  56. package/src/components/workflows/__tests__/WorkflowRunViewer.test.tsx +138 -0
  57. package/src/components/workflows/__tests__/auto-layout.test.ts +107 -0
  58. package/src/components/workflows/__tests__/serialization.test.ts +278 -0
  59. package/src/components/workflows/exec-status.ts +90 -0
  60. package/src/components/workflows/hooks/useCanvasGraph.ts +70 -0
  61. package/src/components/workflows/hooks/useNodeStatusOverlay.ts +47 -0
  62. package/src/components/workflows/index.ts +78 -0
  63. package/src/components/workflows/layout/auto-layout.ts +142 -0
  64. package/src/components/workflows/node-icons.ts +31 -0
  65. package/src/components/workflows/serialization.ts +171 -0
  66. package/src/components/workflows/theme/categories.ts +96 -0
  67. package/src/components/workflows/types.ts +231 -0
  68. package/src/components/workflows/workflows.css +29 -0
  69. package/src/components/workspace/DualPaneWorkspace.tsx +187 -0
  70. package/src/components/workspace/SplitPane.tsx +174 -0
  71. package/src/components/workspace/index.ts +4 -0
package/README.md CHANGED
@@ -1,192 +1,381 @@
1
1
  # @startsimpli/ui
2
2
 
3
- Shared UI components package for StartSimpli applications. Provides a comprehensive set of React components including the powerful UnifiedTable component and shadcn/ui primitives.
3
+ Shared React UI primitives for every StartSimpli Next.js app. Buttons, dialogs, the
4
+ production `UnifiedTable`, a Tailwind preset, a CSS-variable design-token contract,
5
+ and all the domain compositions (compose flows, kanban, gantt, email editor, command
6
+ palette, dashboard widgets) that are reused across `raise-simpli`, `market-simpli`,
7
+ `trade-simpli`, `vault-web`, and friends.
4
8
 
5
- ## Features
9
+ - **Package**: `@startsimpli/ui` · v0.4.13
10
+ - **Source**: ~240 `.ts`/`.tsx` files across 30 barrel files
11
+ - **Tests**: 58 test files (~1600 `test`/`it` cases) under `src/components/**/__tests__/`
12
+ - **Peer deps**: `react` 18/19, `react-dom` 18/19, `next` 14/15/16, optional `@tanstack/react-query` ≥5
13
+ - **Status**: the biggest shared package; the inventory below is exhaustive (read from the barrel files, not aspirational)
6
14
 
7
- - **UnifiedTable**: Production-ready data table with advanced features
8
- - Server-side pagination, sorting, and search
9
- - Column visibility, reordering, and resizing
10
- - Bulk actions and row actions
11
- - Inline editing
12
- - Filters with presets
13
- - Export to CSV/Excel
14
- - Saved views
15
- - Mobile-responsive card view
16
- - Keyboard navigation
17
- - URL persistence
15
+ > Shared-first policy (monorepo CLAUDE.md rule 9): any reusable button, dialog,
16
+ > table, hook, util, layout, or domain widget belongs **here**, not in an app's
17
+ > `src/`. Wrapping `@startsimpli/ui` in an app-local provider just to add a field
18
+ > is also forbidden extend the shared component.
18
19
 
19
- - **shadcn/ui Components**: Complete set of accessible UI primitives
20
- - **Tailwind CSS Integration**: Pre-configured theme and utilities
21
- - **TypeScript**: Full type safety
20
+ ---
22
21
 
23
- ## Installation
22
+ ## Install / wire-up
24
23
 
25
- ```bash
26
- npm install @startsimpli/ui
24
+ Every app must do four things to consume the UI package end-to-end:
25
+
26
+ ```ts
27
+ // app/layout.tsx
28
+ import { AuthProvider } from '@startsimpli/auth';
29
+ import { QueryProvider, Toaster } from '@startsimpli/ui';
30
+ import '@startsimpli/ui/theme/contract'; // or copy contract.css into your theme dir
31
+ import './globals.css';
32
+
33
+ export default function RootLayout({ children }) {
34
+ return (
35
+ <html lang="en" suppressHydrationWarning>
36
+ <body suppressHydrationWarning>
37
+ <AuthProvider config={{ apiBaseUrl: process.env.NEXT_PUBLIC_API_URL ?? '' }}>
38
+ <QueryProvider>
39
+ {children}
40
+ <Toaster />
41
+ </QueryProvider>
42
+ </AuthProvider>
43
+ </body>
44
+ </html>
45
+ );
46
+ }
27
47
  ```
28
48
 
29
- ### Peer Dependencies
49
+ 1. **Mount `<QueryProvider>`** — shared `QueryClient` with sane defaults
50
+ (`staleTime: 5min`, `refetchOnWindowFocus: false`). One per app, at the root.
51
+ 2. **Mount `<Toaster>`** — driver for `useToast` / `notify`. Mount once, near the
52
+ root. Fixed bottom-right, accessible region with live-region semantics.
53
+ 3. **Define the theme tokens** — copy `theme/contract.css` (the public contract,
54
+ exported as `@startsimpli/ui/theme/contract`) into your app's globals or import
55
+ it directly. See "Theme tokens" below.
56
+ 4. **Wire Tailwind content paths** — described next. Skipping this is the cause
57
+ of the "classes silently tree-shaken" bug.
30
58
 
31
- This package requires the following peer dependencies:
59
+ ---
32
60
 
33
- ```bash
34
- npm install react react-dom next
61
+ ## Tailwind: content paths are MANDATORY
62
+
63
+ Apps' `tailwind.config.ts` MUST list this package's `src/` (and every other
64
+ `@startsimpli/*` package whose JSX renders Tailwind classes) under `content`.
65
+ Otherwise Tailwind walks only the app source and tree-shakes every class that
66
+ only exists inside the package — the rendered HTML has `className="bg-blue-600
67
+ text-white"` but no matching CSS rule, so featured CTAs render grey-on-grey and
68
+ buttons collapse.
69
+
70
+ This was the root cause of **startsim-ja6** ("Pro CTA grey-on-grey" in vault-web)
71
+ and is now codified in `vault-web/CLAUDE.md` rule 3. Reference config:
72
+
73
+ ```ts
74
+ // vault-web/tailwind.config.ts
75
+ import type { Config } from "tailwindcss";
76
+
77
+ const config: Config = {
78
+ content: [
79
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
80
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
81
+ "../packages/ui/src/**/*.{js,ts,jsx,tsx}",
82
+ "../packages/billing/src/**/*.{js,ts,jsx,tsx}",
83
+ "../packages/auth/src/**/*.{js,ts,jsx,tsx}",
84
+ "../packages/forms/src/**/*.{js,ts,jsx,tsx}",
85
+ "../packages/hooks/src/**/*.{js,ts,jsx,tsx}",
86
+ ],
87
+ theme: { /* ... */ },
88
+ };
89
+
90
+ export default config;
35
91
  ```
36
92
 
37
- ## Usage
93
+ You also need this package as a `presets` entry or to merge its theme by hand:
38
94
 
39
- ### UnifiedTable
95
+ ```js
96
+ // (option A) preset
97
+ module.exports = { presets: [require('@startsimpli/ui/tailwind')], content: [/* ... */] }
98
+ ```
40
99
 
41
- The UnifiedTable component provides a complete data table solution with server-side operations.
100
+ …and add `'@startsimpli/ui'` to `transpilePackages` in `next.config.{ts,js}` so
101
+ Next compiles its JSX with the app.
42
102
 
43
- #### Basic Example
103
+ ---
44
104
 
45
- ```tsx
46
- import { UnifiedTable } from '@startsimpli/ui/table'
105
+ ## Theme tokens (CSS-variable contract)
47
106
 
48
- function MyTable() {
49
- const [data, setData] = useState([])
50
- const [loading, setLoading] = useState(false)
51
- const [page, setPage] = useState(1)
52
- const [totalCount, setTotalCount] = useState(0)
107
+ The design system is HSL-channel CSS variables. Changing one variable reshapes
108
+ every component — radii, shadows, brand colors, typography stacks. The
109
+ **single contract** lives in `theme/contract.css` and is mirrored in
110
+ `src/theme/contract.ts` as a typed list (`THEME_TOKENS`).
53
111
 
54
- const columns = [
55
- {
56
- id: 'name',
57
- header: 'Name',
58
- accessorKey: 'name',
59
- sortable: true,
60
- },
61
- {
62
- id: 'email',
63
- header: 'Email',
64
- accessorKey: 'email',
65
- sortable: true,
66
- },
67
- ]
112
+ ```css
113
+ :root {
114
+ --primary: 221.2 83.2% 53.3%;
115
+ --radius: 0.5rem; /* set 0 = sharp editorial, 1.5rem = pill */
116
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
117
+ --font-heading: var(--font-sans);
118
+ --border-width: 1px;
119
+ /* ...19 colors · 6 radii · 4 shadows · 3 type · 4 motion · 1 border-width */
120
+ }
121
+ ```
122
+
123
+ The Tailwind preset (`src/theme/tailwind.preset.ts`) consumes those variables —
124
+ `bg-primary` resolves to `hsl(var(--primary))`, `rounded-md` resolves to
125
+ `var(--radius-md)`, etc. To swap the look of an entire app, override the
126
+ variables in your globals; no Tailwind config change needed.
127
+
128
+ ```ts
129
+ import { THEME_TOKENS, type ThemeColorTokens } from '@startsimpli/ui';
130
+ // THEME_TOKENS.colors → ['--background', '--foreground', '--primary', ...]
131
+ ```
132
+
133
+ **Cross-link**: `startsim-sw1.8` tracks single-sourcing these tokens between this
134
+ web contract.css and `@startsimpli/ui-native/tokens.ts` (currently mirrored by
135
+ hand — the goal is one source feeding both). When that lands, the canonical token
136
+ file moves up a level; this README's contract path will be updated accordingly.
137
+
138
+ ---
139
+
140
+ ## Public surface
141
+
142
+ 5 root exports plus a handful of subpath exports. The root re-exports almost
143
+ everything by domain; subpaths exist for the heaviest features so apps can
144
+ tree-shake.
145
+
146
+ ### Subpath exports
147
+
148
+ | Subpath | Purpose |
149
+ |---|---|
150
+ | `@startsimpli/ui` | Everything except `email-editor` blocks; primary surface |
151
+ | `@startsimpli/ui/table` | `UnifiedTable` only (lighter import) |
152
+ | `@startsimpli/ui/gantt` | Gantt chart family (`GanttChart`, views, hooks, types) |
153
+ | `@startsimpli/ui/gantt/styles` | Required CSS for the gantt views |
154
+ | `@startsimpli/ui/email-editor` | Block-based email editor (heavy; opt-in) |
155
+ | `@startsimpli/ui/components` | Same as `.` minus theme/utils — historical alias |
156
+ | `@startsimpli/ui/utils` | `cn`, `formatDate`, `formatCurrency`, `getInitials`, ... |
157
+ | `@startsimpli/ui/theme` | `tailwindPreset`, `tailwindConfig`, `THEME_TOKENS` |
158
+ | `@startsimpli/ui/theme/contract` | The CSS variables file (import in globals.css) |
159
+ | `@startsimpli/ui/tailwind` | CommonJS preset (for legacy JS configs) |
160
+
161
+ ### Inventory by domain
162
+
163
+ Drawn directly from the barrel files in `src/components/**/index.ts`. If
164
+ something isn't listed here, it isn't exported.
165
+
166
+ **shadcn/ui primitives** (`src/components/ui/`) — every Radix-backed primitive
167
+ the rest of the package builds on:
168
+
169
+ > Accordion · Alert · Badge · Button · Calendar · Card · Checkbox · Collapsible
170
+ > · Dialog · DropdownMenu · Input · Label · Popover · Progress · ScrollArea ·
171
+ > Select · Separator · Skeleton · Table · Tabs · Textarea · Tooltip ·
172
+ > **FeatureGate** · **ApiErrorBoundary** · **PageLoader** · **QueryProvider**
173
+
174
+ **UnifiedTable** (`src/components/unified-table/`) — the production data table.
175
+
176
+ - `UnifiedTable`, `StandardTableToolbar`, `MobileView`, `MobileCard`, `MobileCardActions`, `MOBILE_BREAKPOINT`
177
+ - Hooks: `useTableState`, `useSelection`, `usePagination`, `useFilters`, `useResponsive`, `useColumnVisibility`, `useColumnReorder`, `useColumnResize`, `useTablePreferences`, `useTableKeyboard`, `useTableURL`
178
+ - All the column / sort / filter / bulk-action / saved-view types are exported from `./types`
179
+ - See "UnifiedTable" section below for the full API
180
+
181
+ **Dialogs** (`src/components/dialog/`):
182
+ - `BaseDialog` (compound API: `BaseDialog.Header`, `.Title`, `.Body`, `.Footer`), `useDialogContext`, `ConfirmDialog`
183
+
184
+ **Toast / Notify** (`src/components/toast/`):
185
+ - `Toaster` (mount once at root)
186
+ - `useToast()`, `toast()`, `clearAllToasts()` — low-level imperative API (shadcn-style)
187
+ - `useNotify()`, `notify()` — opinionated wrapper (success / error / warning / info) used across the apps
188
+
189
+ **State components** (`src/components/states/`):
190
+ - `EmptyState`, `ErrorState` — standardized empty/error UI with optional action
191
+
192
+ **Loading** (`src/components/loading/`):
193
+ - `TableSkeleton`, `DashboardSkeleton`
194
+
195
+ **Badges** (`src/components/badge/`):
196
+ - `StatusBadge`, `StageBadge` (config-driven, brand-aware)
197
+
198
+ **Wizard** (`src/components/wizard/`):
199
+ - `StepIndicator` + `StepConfig`
200
+
201
+ **Account forms** (`src/components/account/`):
202
+ - `ProfileForm`, `ChangePasswordForm` — used by raise-simpli's settings
203
+
204
+ **Compose flow** (`src/components/compose/`) — shared email/draft composer:
205
+ - `useAutoSave`, `SaveStatusIndicator`, `ComposeHeader`, `SubjectInput`, `SendConfirmationDialog`, `ComposeLoading`
206
+
207
+ **Email editor** (`src/components/email-editor/`) — block-based builder with drag/drop, undo/redo, HTML renderer:
208
+ - `EmailEditor`, `BlockRenderer`
209
+ - Block types: `Block`, `TextBlock`, `MetricsBlock`, `DividerBlock`, `CTABlock`, `ImageBlock`, `SpacerBlock`, `SocialBlock`, `HeaderBlock`, `FooterBlock`, `Section`, `Row`, `ColumnLayout`, `GlobalStyles`
210
+ - Helpers: `createBlock`, `createRow`, `createSection`, `serializeSections`, `deserializeSections`, `migrateFromLegacy`, `flattenToLegacy`
211
+ - Renderer: `renderToEmailHtml`, `renderToPreviewHtml`
212
+ - Templates: `createInvestorUpdateTemplate`, `createEmptyTemplate`, `THEME_PRESETS`
213
+
214
+ **Email dialogs** (`src/components/email-dialogs/`):
215
+ - `ScheduleDialog`, `TestSendDialog`, `PreviewDialog`, `TemplatePicker`
216
+ - Merge fields: `MergeFieldsMenu`, `MergeFieldPreview`, `replaceMergeFields`, `DEFAULT_MERGE_FIELDS`, `DEFAULT_CATEGORIES`
217
+
218
+ **Dashboard** (`src/components/dashboard/`):
219
+ - `MetricCard`, `PeriodSelector`, `SparklineTrend`, `DashboardGrid`, `DashboardSection`, `PipelineFunnel`, `TopCampaigns`
220
+
221
+ **Enrichment** (`src/components/enrichment/`):
222
+ - `QualityBadge`, `EnrichButton`, `EnrichmentProgress`, `ApolloEnrichButton`
223
+
224
+ **Integrations** (`src/components/integrations/`):
225
+ - `IntegrationCard`, `ConnectionStatus`
226
+
227
+ **Command palette** (`src/components/command-palette/`):
228
+ - `CommandPalette`, `CommandPaletteProvider`, `useCommandPalette`, `useCommandPaletteSearch`, `CommandGroup`, `CommandResultItem`
229
+
230
+ **Settings layout** (`src/components/settings/`):
231
+ - `SettingsLayout`, `SettingsNav`, `SettingsCard`
232
+
233
+ **Kanban** (`src/components/kanban/`):
234
+ - `KanbanBoard` (drag-drop via `@hello-pangea/dnd`)
235
+
236
+ **Lists** (`src/components/lists/`):
237
+ - `ListCard`, `CreateListDialog`
238
+
239
+ **Pipeline** (`src/components/pipeline/`):
240
+ - `StageTransitionModal`
241
+
242
+ **Calendar / meetings** (`src/components/calendar/`):
243
+ - `CalendarView` (wraps `react-big-calendar`), `UpcomingMeetings`, `MeetingsList`
244
+
245
+ **Gantt** (`src/components/gantt/`, also via `@startsimpli/ui/gantt`):
246
+ - `GanttChart`, `GanttTimelineView`, `GanttBoardView`, `GanttListView`, `GanttFilterBar`, `useGanttState`
247
+ - Pure helpers: `calculateExpectedProgress`, `calculateHealthStatus`, `getHealthColor`, `parseDateRangeFromTitle`, `getHierarchyLevel`
248
+
249
+ **Activity** (root `src/components/`):
250
+ - `ActivityTimeline`, `QuickLogButtons`, `LogActivityDialog`
251
+
252
+ **Navigation** (`src/components/navigation/sidebar.tsx`):
253
+ - `Sidebar`, `SidebarLayout`, types `SidebarLink`, `SidebarSection`
254
+
255
+ **Safe HTML**:
256
+ - `SafeHtml` — dompurify-backed; `raise-simpli/.eslintrc.json` forbids
257
+ `dangerouslySetInnerHTML` in favor of this.
258
+
259
+ ### Top headline components (by reach across the monorepo)
260
+
261
+ | Component | Used by | Why |
262
+ |---|---|---|
263
+ | `UnifiedTable` | market-simpli (CRM lists, campaigns, contacts), raise-simpli | Every server-paginated table |
264
+ | `QueryProvider` | vault-web, market-simpli, trade-simpli, raise-simpli | Single shared `QueryClient` per app |
265
+ | `Toaster` | vault-web, market-simpli, trade-simpli, raise-simpli | Notification region |
266
+ | `notify` / `useToast` | All apps | Imperative success/error/warn |
267
+ | `Button` | All apps | shadcn primitive, gradient variants |
268
+ | `Dialog` + `BaseDialog` + `ConfirmDialog` | market-simpli, raise-simpli | Modal flows |
269
+ | `EmptyState` / `ErrorState` | market-simpli, raise-simpli | Empty/list/page fallbacks |
270
+ | `Card` (+ Header/Title/Content) | trade-simpli (dashboards), market-simpli | Container primitive |
271
+ | `Badge` / `StatusBadge` | trade-simpli, market-simpli | Pipeline + status chips |
272
+ | `SettingsLayout` / `SettingsCard` / `SettingsNav` | market-simpli | Whole settings shell |
273
+ | `ProfileForm` + `ChangePasswordForm` | raise-simpli | Account settings |
274
+ | `MeetingsList` | raise-simpli | Upcoming meetings widget |
275
+ | `DashboardGrid` + `MetricCard` + `PeriodSelector` | trade-simpli, market-simpli | Dashboard composition |
276
+ | `IntegrationCard` | market-simpli | Integrations grid |
277
+ | `GanttChart` (via `@startsimpli/ui/gantt`) | raise-simpli | Roadmaps / timelines |
278
+ | `EmailEditor` + email-dialogs (`ScheduleDialog`, `TestSendDialog`, `TemplatePicker`) | market-simpli (campaign composer), raise-simpli (investor updates) | Block-based email builder |
279
+ | `ApolloEnrichButton` / `EnrichmentProgress` | raise-simpli (Apollo enrichment) | Enrichment lifecycle UI |
280
+ | `SafeHtml` | raise-simpli (ESLint-enforced) | XSS-safe HTML rendering |
281
+ | `FeatureGate` | raise-simpli | Plan-gated UI |
282
+ | `StepIndicator` | market-simpli (onboarding wizard) | Multi-step UX |
283
+
284
+ ---
285
+
286
+ ## UnifiedTable
287
+
288
+ Production-ready data table. **Every table in every app uses server-side
289
+ pagination, sorting, and search — this is non-negotiable** (CLAUDE.md rule 8).
290
+
291
+ Features: pagination, sorting, search, column visibility / reordering / resizing,
292
+ bulk + row actions, inline editing with validation, filter sections with chip /
293
+ range / select / boolean inputs, CSV + Excel export, saved views, URL state
294
+ persistence, mobile card view, keyboard navigation.
295
+
296
+ ### Basic example
297
+
298
+ ```tsx
299
+ import { UnifiedTable } from '@startsimpli/ui/table';
300
+ import type { ColumnConfig } from '@startsimpli/ui';
301
+
302
+ function UsersTable() {
303
+ const [page, setPage] = useState(1);
304
+ const [search, setSearch] = useState('');
305
+ const [sort, setSort] = useState({ sortBy: 'name', sortDirection: 'asc' as const });
306
+ const { data, isLoading } = useQuery({
307
+ queryKey: ['users', page, search, sort],
308
+ queryFn: () => api.users.list({ page, search, ...sort }),
309
+ });
310
+
311
+ const columns: ColumnConfig<User>[] = [
312
+ { id: 'name', header: 'Name', accessorKey: 'name', sortable: true },
313
+ { id: 'email', header: 'Email', accessorKey: 'email', sortable: true },
314
+ ];
68
315
 
69
316
  return (
70
- <UnifiedTable
71
- data={data}
317
+ <UnifiedTable<User>
318
+ tableId="users"
319
+ data={data?.items ?? []}
72
320
  columns={columns}
73
- tableId="users-table"
74
- getRowId={(row) => row.id}
321
+ getRowId={(u) => u.id}
322
+ loading={isLoading}
75
323
  pagination={{
76
324
  enabled: true,
77
325
  pageSize: 25,
78
- totalCount,
326
+ totalCount: data?.total ?? 0,
79
327
  currentPage: page,
80
- serverSide: true, // CRITICAL: Always use server-side pagination
328
+ serverSide: true, // REQUIRED
81
329
  onPageChange: setPage,
82
330
  }}
83
331
  sorting={{
84
332
  enabled: true,
85
- serverSide: true, // CRITICAL: Always use server-side sorting
86
- onChange: (sort) => {
87
- // Pass sort params to API
88
- fetchData({ page, sortBy: sort.sortBy, sortDirection: sort.sortDirection })
89
- },
333
+ serverSide: true, // REQUIRED
334
+ value: sort,
335
+ onChange: setSort,
90
336
  }}
91
337
  search={{
92
338
  enabled: true,
93
- placeholder: 'Search users...',
94
- value: searchTerm,
95
- onChange: (value) => {
96
- // Pass search query to API
97
- setSearchTerm(value)
98
- fetchData({ page: 1, search: value })
99
- },
339
+ placeholder: 'Search users',
340
+ value: search,
341
+ onChange: setSearch,
100
342
  }}
101
- loading={loading}
102
343
  />
103
- )
344
+ );
104
345
  }
105
346
  ```
106
347
 
107
- #### Server-Side Operations (CRITICAL)
108
-
109
- **ALL tables MUST use server-side operations. This is non-negotiable.**
348
+ ### Why server-side, always
110
349
 
111
350
  ```tsx
112
- // CORRECT - Server-side pagination
113
- pagination={{
114
- enabled: true,
115
- pageSize: 25,
116
- totalCount: 1000, // Total from API
117
- currentPage: page,
118
- serverSide: true, // Fetch pages from API
119
- onPageChange: (newPage) => {
120
- // Call API with ?page=newPage&pageSize=25
121
- fetchData({ page: newPage, pageSize: 25 })
122
- },
123
- }}
351
+ // CORRECT
352
+ pagination={{ serverSide: true, totalCount, currentPage: page, onPageChange }}
353
+ sorting ={{ serverSide: true, onChange: (s) => fetchData({ ...s }) }}
124
354
 
125
- // WRONG - Client-side pagination
126
- pagination={{
127
- enabled: true,
128
- serverSide: false, // Loads all data into memory
129
- }}
355
+ // WRONG loads everything into memory, doesn't scale
356
+ pagination={{ serverSide: false }}
357
+ sorting ={{ serverSide: false }}
130
358
  ```
131
359
 
132
- ```tsx
133
- // ✅ CORRECT - Server-side sorting
134
- sorting={{
135
- enabled: true,
136
- serverSide: true, // Pass sort params to API
137
- onChange: (sort) => {
138
- // Call API with ?sortField=name&sortDirection=asc
139
- fetchData({ sortField: sort.sortBy, sortDirection: sort.sortDirection })
140
- },
141
- }}
142
-
143
- // ❌ WRONG - Client-side sorting
144
- sorting={{
145
- enabled: true,
146
- serverSide: false, // Sorts in memory
147
- }}
148
- ```
149
-
150
- ```tsx
151
- // ✅ CORRECT - Server-side search
152
- search={{
153
- enabled: true,
154
- value: searchTerm,
155
- onChange: (value) => {
156
- // Call API with ?search=value
157
- fetchData({ search: value })
158
- },
159
- }}
160
-
161
- // ❌ WRONG - Client-side search
162
- // (No client-side search mode - always passes to API)
163
- ```
164
-
165
- #### Selection and Bulk Actions
360
+ ### Bulk actions
166
361
 
167
362
  ```tsx
168
363
  <UnifiedTable
169
- selection={{
170
- enabled: true,
171
- selectedIds: selectedIds,
172
- onSelectionChange: setSelectedIds,
173
- }}
364
+ selection={{ enabled: true, selectedIds, onSelectionChange: setSelectedIds }}
174
365
  bulkActions={[
175
366
  {
176
367
  id: 'delete',
177
368
  label: 'Delete',
178
369
  icon: Trash2,
179
370
  variant: 'gradient-purple',
180
- onClick: async (ids) => {
181
- await deleteUsers(Array.from(ids))
182
- },
371
+ onClick: async (ids) => deleteUsers(Array.from(ids)),
183
372
  confirmMessage: 'Delete {count} users?',
184
373
  },
185
374
  ]}
186
375
  />
187
376
  ```
188
377
 
189
- #### Filters
378
+ ### Filters
190
379
 
191
380
  ```tsx
192
381
  <UnifiedTable
@@ -195,273 +384,65 @@ search={{
195
384
  position: 'top',
196
385
  collapsible: true,
197
386
  config: {
198
- sections: [
199
- {
200
- id: 'status',
201
- type: 'chips',
202
- label: 'Status',
203
- filters: [
204
- { id: 'status', label: 'Status', type: 'chips', options: ['active', 'inactive'] },
205
- ],
206
- },
207
- ],
387
+ sections: [{
388
+ id: 'status',
389
+ type: 'chips',
390
+ label: 'Status',
391
+ filters: [{ id: 'status', label: 'Status', type: 'chips', options: ['active', 'inactive'] }],
392
+ }],
208
393
  },
209
394
  value: filters,
210
- onChange: (newFilters) => {
211
- setFilters(newFilters)
212
- // Pass to API: ?status=active
213
- },
395
+ onChange: setFilters, // forward to your API query
214
396
  }}
215
397
  />
216
398
  ```
217
399
 
218
- #### Inline Editing
400
+ ### Inline editing
219
401
 
220
402
  ```tsx
221
403
  <UnifiedTable
222
- columns={[
223
- {
224
- id: 'name',
225
- header: 'Name',
226
- accessorKey: 'name',
227
- editable: true,
228
- editType: 'text',
229
- validate: (value) => {
230
- if (!value) return 'Name is required'
231
- return null
232
- },
233
- },
234
- ]}
404
+ columns={[{
405
+ id: 'name', header: 'Name', accessorKey: 'name',
406
+ editable: true, editType: 'text',
407
+ validate: (v) => (!v ? 'Name is required' : null),
408
+ }]}
235
409
  inlineEdit={{
236
410
  enabled: true,
237
- onSave: async (rowId, columnId, value, row) => {
238
- await updateUser(rowId, { [columnId]: value })
239
- },
411
+ optimisticUpdate: true,
412
+ onSave: async (rowId, columnId, value) => updateUser(rowId, { [columnId]: value }),
240
413
  }}
241
414
  />
242
415
  ```
243
416
 
244
- #### Export
245
-
246
- ```tsx
247
- <UnifiedTable
248
- export={{
249
- enabled: true,
250
- baseFilename: 'users',
251
- formats: ['csv', 'excel'],
252
- showProgress: true,
253
- onExportComplete: (format, scope, rowCount) => {
254
- console.log(`Exported ${rowCount} rows as ${format}`)
255
- },
256
- }}
257
- />
258
- ```
259
-
260
- #### Saved Views
417
+ ### Export, saved views, mobile
261
418
 
262
419
  ```tsx
263
420
  <UnifiedTable
421
+ export={{ enabled: true, baseFilename: 'users', formats: ['csv', 'excel'], showProgress: true }}
264
422
  savedViews={{
265
423
  enabled: true,
266
424
  views: savedViews,
267
- currentViewId: currentViewId,
268
- onSaveView: async (view) => {
269
- const newView = await saveView(view)
270
- return newView
271
- },
272
- onLoadView: (viewId) => {
273
- const view = savedViews.find(v => v.id === viewId)
274
- if (view) {
275
- // Apply view settings
276
- setFilters(view.filters || {})
277
- setColumnVisibility(view.columnVisibility || {})
278
- }
279
- },
425
+ currentViewId,
426
+ onSaveView: async (v) => saveView(v),
427
+ onLoadView: (id) => applyView(savedViews.find(v => v.id === id)!),
280
428
  }}
281
- />
282
- ```
283
-
284
- #### Mobile Support
285
-
286
- ```tsx
287
- <UnifiedTable
288
429
  mobileConfig={{
289
430
  titleKey: 'name',
290
431
  subtitleKey: 'email',
291
432
  primaryFields: ['name', 'email'],
292
433
  secondaryFields: ['status', 'createdAt'],
293
434
  }}
435
+ urlPersistence={{ enabled: true, debounceMs: 300 }}
294
436
  />
295
437
  ```
296
438
 
297
- ### UI Components
298
-
299
- Import shadcn/ui components:
300
-
301
- ```tsx
302
- import { Button } from '@startsimpli/ui/components'
303
- import { Dialog, DialogContent, DialogHeader } from '@startsimpli/ui/components'
304
- import { Input, Label } from '@startsimpli/ui/components'
305
- ```
306
-
307
- ### Utilities
308
-
309
- ```tsx
310
- import { cn } from '@startsimpli/ui/utils'
311
-
312
- // Merge Tailwind classes
313
- const className = cn('base-class', condition && 'conditional-class')
314
- ```
315
-
316
- ### Tailwind Configuration
317
-
318
- Extend your Tailwind config with the shared theme:
319
-
320
- ```js
321
- // tailwind.config.js
322
- const baseConfig = require('@startsimpli/ui/theme/tailwind.config')
323
-
324
- module.exports = {
325
- ...baseConfig,
326
- content: [
327
- './src/**/*.{ts,tsx}',
328
- './node_modules/@startsimpli/ui/src/**/*.{ts,tsx}',
329
- ],
330
- theme: {
331
- ...baseConfig.theme,
332
- extend: {
333
- ...baseConfig.theme.extend,
334
- // Your custom extensions
335
- },
336
- },
337
- }
338
- ```
339
-
340
- ## UnifiedTable API Reference
341
-
342
- ### Props
343
-
344
- #### Core Props
345
-
346
- - `data: TData[]` - Array of data to display
347
- - `columns: ColumnConfig<TData>[]` - Column definitions
348
- - `tableId: string` - Unique identifier for the table
349
- - `getRowId: (row: TData) => string` - Function to get unique row ID
350
-
351
- #### Pagination
352
-
353
- - `pagination.enabled: boolean` - Enable pagination
354
- - `pagination.pageSize: number` - Rows per page
355
- - `pagination.totalCount: number` - Total number of rows (from API)
356
- - `pagination.currentPage: number` - Current page number (1-indexed)
357
- - `pagination.serverSide: boolean` - **MUST be true** for production
358
- - `pagination.onPageChange: (page: number) => void` - Page change callback
359
-
360
- #### Sorting
361
-
362
- - `sorting.enabled: boolean` - Enable sorting
363
- - `sorting.serverSide: boolean` - **MUST be true** for production
364
- - `sorting.value: SortState` - Current sort state (controlled)
365
- - `sorting.onChange: (sort: SortState) => void` - Sort change callback
366
-
367
- #### Search
368
-
369
- - `search.enabled: boolean` - Enable search
370
- - `search.placeholder: string` - Search input placeholder
371
- - `search.value: string` - Current search value (controlled)
372
- - `search.onChange: (value: string) => void` - Search change callback
373
-
374
- #### Selection
375
-
376
- - `selection.enabled: boolean` - Enable row selection
377
- - `selection.selectedIds: Set<string>` - Selected row IDs (controlled)
378
- - `selection.onSelectionChange: (ids: Set<string>) => void` - Selection change callback
379
- - `selection.selectAllPages: boolean` - Whether all pages are selected
380
-
381
- #### Bulk Actions
382
-
383
- - `bulkActions: BulkAction[]` - Array of bulk action definitions
384
-
385
- #### Row Actions
386
-
387
- - `rowActions: RowAction<TData>[]` - Array of row-level actions
388
-
389
- #### Filters
439
+ ### Column config
390
440
 
391
- - `filters.enabled: boolean` - Enable filters
392
- - `filters.config: FilterConfig` - Filter configuration
393
- - `filters.value: FilterState` - Current filter values (controlled)
394
- - `filters.onChange: (filters: FilterState) => void` - Filter change callback
395
-
396
- #### Column Visibility
397
-
398
- - `columnVisibility.enabled: boolean` - Enable column visibility controls
399
- - `columnVisibility.defaultVisible: string[]` - Initially visible columns
400
- - `columnVisibility.alwaysVisible: string[]` - Columns that cannot be hidden
401
- - `columnVisibility.persistKey: string` - localStorage key for persistence
402
-
403
- #### Column Reordering
404
-
405
- - `columnReorder.enabled: boolean` - Enable drag-and-drop column reordering
406
- - `columnReorder.initialOrder: string[]` - Initial column order
407
- - `columnReorder.onOrderChange: (order: string[]) => void` - Order change callback
408
-
409
- #### Column Resizing
410
-
411
- - `columnResize.enabled: boolean` - Enable column resizing
412
- - `columnResize.initialWidths: Record<string, number>` - Initial column widths
413
- - `columnResize.minWidth: number` - Minimum column width (default: 50)
414
- - `columnResize.onWidthChange: (widths: Record<string, number>) => void` - Width change callback
415
-
416
- #### Inline Editing
417
-
418
- - `inlineEdit.enabled: boolean` - Enable inline editing
419
- - `inlineEdit.onSave: (rowId, columnId, value, row) => Promise<void>` - Save callback
420
- - `inlineEdit.optimisticUpdate: boolean` - Apply changes immediately (default: true)
421
-
422
- #### Export
423
-
424
- - `export.enabled: boolean` - Enable export functionality
425
- - `export.baseFilename: string` - Base filename for exports
426
- - `export.formats: ('csv' | 'excel')[]` - Available export formats
427
- - `export.showProgress: boolean` - Show progress during export
428
-
429
- #### Saved Views
430
-
431
- - `savedViews.enabled: boolean` - Enable saved views
432
- - `savedViews.views: SavedView[]` - Array of saved views
433
- - `savedViews.currentViewId: string` - Currently active view ID
434
- - `savedViews.onSaveView: (view) => Promise<SavedView>` - Save view callback
435
- - `savedViews.onLoadView: (viewId) => void` - Load view callback
436
-
437
- #### URL Persistence
438
-
439
- - `urlPersistence.enabled: boolean` - Enable URL state persistence
440
- - `urlPersistence.debounceMs: number` - Debounce delay (default: 300ms)
441
-
442
- #### Mobile
443
-
444
- - `mobileConfig.titleKey: string` - Key for card title
445
- - `mobileConfig.subtitleKey: string` - Key for card subtitle
446
- - `mobileConfig.primaryFields: string[]` - Primary fields to display
447
- - `mobileConfig.secondaryFields: string[]` - Secondary fields to display
448
-
449
- #### Other
450
-
451
- - `loading: boolean` - Show loading state
452
- - `loadingRows: Set<string>` - Row IDs that are loading
453
- - `className: string` - Additional CSS classes
454
- - `emptyState: ReactNode` - Custom empty state component
455
- - `errorState: ReactNode` - Custom error state component
456
- - `onRowClick: (row: TData) => void` - Row click handler
457
-
458
- ### Column Configuration
459
-
460
- ```tsx
441
+ ```ts
461
442
  interface ColumnConfig<TData> {
462
443
  id: string
463
444
  header: string | ((props: any) => ReactNode)
464
- accessorKey?: string // Dot notation supported: 'user.name'
445
+ accessorKey?: string // dot notation OK: 'user.name'
465
446
  accessorFn?: (row: TData) => any
466
447
  cell?: (row: TData) => ReactNode
467
448
  sortable?: boolean
@@ -480,58 +461,136 @@ interface ColumnConfig<TData> {
480
461
  }
481
462
  ```
482
463
 
483
- ## Testing
464
+ The full prop reference (`pagination`, `sorting`, `search`, `selection`,
465
+ `bulkActions`, `rowActions`, `filters`, `columnVisibility`, `columnReorder`,
466
+ `columnResize`, `inlineEdit`, `export`, `savedViews`, `urlPersistence`,
467
+ `mobileConfig`, `loading`, `loadingRows`, `emptyState`, `errorState`,
468
+ `onRowClick`) lives in `src/components/unified-table/types.ts` — that is the
469
+ source of truth.
484
470
 
485
- The package includes comprehensive test coverage for all components.
471
+ ---
486
472
 
487
- ```bash
488
- # Run tests
489
- npm test
473
+ ## QueryProvider
490
474
 
491
- # Watch mode
492
- npm run test:watch
475
+ A pre-configured `QueryClientProvider` so every app shares the same defaults:
493
476
 
494
- # Coverage
495
- npm run test:coverage
477
+ ```ts
478
+ // src/components/ui/query-provider.tsx
479
+ new QueryClient({
480
+ defaultOptions: {
481
+ queries: {
482
+ staleTime: 5 * 60 * 1000, // 5 minutes — avoids hot refetch storms
483
+ refetchOnWindowFocus: false, // explicit control over refetches
484
+ },
485
+ },
486
+ });
496
487
  ```
497
488
 
498
- ## Type Safety
489
+ Mount it once at the root (see "Install / wire-up"). Apps that need different
490
+ defaults should override per-query, not re-instantiate.
491
+
492
+ `@tanstack/react-query` is an **optional** peer dep — the package compiles
493
+ without it, but `QueryProvider` requires it at runtime.
494
+
495
+ ---
499
496
 
500
- All components are fully typed with TypeScript. The UnifiedTable component uses generics for type-safe data handling:
497
+ ## Toaster + notify
498
+
499
+ `<Toaster />` is the renderer; mount once at the root. Variants: `default`,
500
+ `destructive`, `success`, `warning`, `info`. The container is a polite live
501
+ region (`role="region"`, `aria-live="polite"`), fixed bottom-right, with
502
+ slide-in animation via `tailwindcss-animate`.
503
+
504
+ Two APIs are exposed:
501
505
 
502
506
  ```tsx
503
- interface User {
504
- id: string
505
- name: string
506
- email: string
507
- }
507
+ // Low-level (shadcn shape)
508
+ import { useToast, toast, clearAllToasts } from '@startsimpli/ui';
509
+ toast({ title: 'Saved', description: 'Draft saved', variant: 'success' });
510
+
511
+ // Opinionated wrapper (most apps use this)
512
+ import { notify } from '@startsimpli/ui';
513
+ notify.success('Saved');
514
+ notify.error('Could not save', { description: err.message });
515
+ notify.info('Heads up');
516
+ notify.warning('Almost out of credits');
517
+ ```
508
518
 
509
- <UnifiedTable<User>
510
- data={users}
511
- columns={columns}
512
- getRowId={(user) => user.id} // Type-safe
513
- />
519
+ ---
520
+
521
+ ## Utils
522
+
523
+ ```ts
524
+ import { cn, generateId, sleep, formatDate, formatRelativeTime, formatRelativeDate, formatCurrency, getInitials } from '@startsimpli/ui';
514
525
  ```
515
526
 
516
- ## Performance Considerations
527
+ - `cn(...classes)` — `clsx` + `tailwind-merge` (the standard merge)
528
+ - `formatDate(d)` → `"May 28, 2026"`
529
+ - `formatRelativeTime(d)` → `"Today" | "Yesterday" | "3 days ago" | "May 5, 2026"`
530
+ - `formatRelativeDate(d)` → fine-grained `"just now" | "5m ago" | "2h ago" | "3d ago"`
531
+ - `formatCurrency(n)` → `"$1,000,000"` (USD, no cents)
532
+ - `getInitials(name)` → up to 2-char uppercase initials
533
+ - `generateId(prefix?)`, `sleep(ms)`
534
+
535
+ ---
517
536
 
518
- 1. **Always use server-side operations** - Never load entire datasets into memory
519
- 2. **Debounce search inputs** - Use the built-in debouncing (default: 300ms)
520
- 3. **Virtualization for large lists** - Consider implementing virtual scrolling for 1000+ rows
521
- 4. **Optimize column renderers** - Use `useMemo` for complex cell renderers
522
- 5. **Minimize re-renders** - Use controlled state carefully
537
+ ## Real consumer examples
538
+
539
+ **vault-web** (`vault-web/src/app/layout.tsx`) root wiring is the canonical
540
+ example: `<AuthProvider>` `<QueryProvider>` `<Toaster />`. Vault's billing
541
+ page composes `SubscriptionManager` from `@startsimpli/billing`, which itself
542
+ sits on the design tokens defined here.
543
+
544
+ **market-simpli** — heaviest consumer (~64 import sites). Uses `UnifiedTable`
545
+ extensively for CRM lists, `SettingsLayout`/`SettingsNav`/`SettingsCard` for the
546
+ settings shell, `IntegrationCard` for the integrations grid, `EmailEditor` +
547
+ email-dialogs for campaign compose, `StepIndicator` for onboarding,
548
+ `PeriodSelector`/`DashboardGrid` for dashboards.
549
+
550
+ **raise-simpli** — settings pages use `useToast`; `ProfileForm` +
551
+ `ChangePasswordForm` mounted under account settings; `SafeHtml` enforced via
552
+ ESLint over `dangerouslySetInnerHTML`; `FeatureGate` for plan-gated UI;
553
+ `ApolloEnrichButton` + `EnrichmentProgress` drive Apollo enrichment;
554
+ `GanttChart` via `@startsimpli/ui/gantt` for roadmap views; `MeetingsList` for
555
+ upcoming meetings; consumes the Tailwind preset directly
556
+ (`presets: [require('@startsimpli/ui/tailwind')]`).
557
+
558
+ **trade-simpli** — dashboards: `Card`, `Badge`, `StatusBadge`, `DashboardGrid`,
559
+ plus `<QueryProvider>` + `<Toaster />` at the root.
560
+
561
+ `recipesimpli` and `crochet-patterns` are standalone Next apps with their own
562
+ DB and have not yet adopted `@startsimpli/ui`.
563
+
564
+ ---
565
+
566
+ ## Testing
567
+
568
+ ```bash
569
+ pnpm --filter @startsimpli/ui test # all tests
570
+ pnpm --filter @startsimpli/ui test:watch
571
+ pnpm --filter @startsimpli/ui test:coverage
572
+ pnpm --filter @startsimpli/ui type-check
573
+ ```
523
574
 
524
- ## Browser Support
575
+ - **58 test files** under `src/components/**/__tests__/` (~1600 `test`/`it`
576
+ cases), covering UnifiedTable (17 files), email editor (8 files), and every
577
+ domain barrel (dashboard, dialogs, kanban, gantt, settings, compose,
578
+ command-palette, navigation, lists, pipeline, enrichment, integrations,
579
+ account, calendar, badge, loading, states, wizard, toast, safe-html).
580
+ - Jest + `@swc/jest` + `jest-environment-jsdom`, with `next/navigation` and
581
+ `next/link` mocked in `src/__mocks__/next/`.
582
+ - Coverage thresholds enforced at 70% (branches / functions / lines /
583
+ statements) via `jest.config.js`.
525
584
 
526
- - Chrome (latest)
527
- - Firefox (latest)
528
- - Safari (latest)
529
- - Edge (latest)
585
+ ---
530
586
 
531
- ## Contributing
587
+ ## Shared-first reminder
532
588
 
533
- This package is part of the StartSimpli monorepo. Follow the monorepo contribution guidelines.
589
+ Before adding any reusable button / dialog / hook / formatter / layout / domain
590
+ widget to an app's `src/`, ask whether another app could plausibly want it.
591
+ If yes — or if you're unsure — it goes here. See the monorepo
592
+ `CLAUDE.md` rule 9 and `.claude/docs/conventions.md#shared-packages-policy`.
534
593
 
535
594
  ## License
536
595
 
537
- Proprietary - StartSimpli
596
+ Proprietary StartSimpli.