@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.
- package/README.md +457 -398
- package/package.json +18 -13
- package/src/components/__tests__/calendar-view-popup.test.tsx +42 -0
- package/src/components/__tests__/chat.test.tsx +129 -0
- package/src/components/__tests__/meetings-list.test.tsx +114 -0
- package/src/components/__tests__/slide-deck-viewer.test.tsx +82 -0
- package/src/components/__tests__/workspace.test.tsx +106 -0
- package/src/components/account/__tests__/account.test.tsx +5 -32
- package/src/components/account/change-password-form.tsx +1 -28
- package/src/components/calendar/calendar-view.tsx +31 -0
- package/src/components/calendar/index.ts +7 -0
- package/src/components/calendar/meetings-list.tsx +202 -0
- package/src/components/calendar/upcoming-meetings.tsx +5 -5
- package/src/components/chat/ChatComposer.tsx +113 -0
- package/src/components/chat/ChatMessage.tsx +81 -0
- package/src/components/chat/ChatThread.tsx +57 -0
- package/src/components/chat/index.ts +12 -0
- package/src/components/chat/types.ts +20 -0
- package/src/components/index.ts +13 -0
- package/src/components/slide-deck/SlideCanvas.tsx +68 -0
- package/src/components/slide-deck/SlideDeckViewer.tsx +144 -0
- package/src/components/slide-deck/SlideFilmstrip.tsx +73 -0
- package/src/components/slide-deck/index.ts +7 -0
- package/src/components/slide-deck/types.ts +18 -0
- package/src/components/team/DomainClaimCard.tsx +170 -0
- package/src/components/team/InviteMemberDialog.tsx +182 -0
- package/src/components/team/LeaveTeamDialog.tsx +130 -0
- package/src/components/team/MembersTable.tsx +138 -0
- package/src/components/team/OrgSwitcher.tsx +68 -0
- package/src/components/team/PendingInvitationCallout.tsx +106 -0
- package/src/components/team/RoleSelector.tsx +68 -0
- package/src/components/team/__tests__/team-components.test.tsx +352 -0
- package/src/components/team/domain-claim-card-default-class-names.ts +45 -0
- package/src/components/team/index.ts +57 -0
- package/src/components/team/invite-member-dialog-default-class-names.ts +41 -0
- package/src/components/team/leave-team-dialog-default-class-names.ts +33 -0
- package/src/components/team/members-table-default-class-names.ts +39 -0
- package/src/components/team/org-switcher-default-class-names.ts +13 -0
- package/src/components/team/pending-invitation-callout-default-class-names.ts +22 -0
- package/src/components/team/role-selector-default-class-names.ts +11 -0
- package/src/components/team/types.ts +97 -0
- package/src/components/workflows/ExecNodeDetails.tsx +83 -0
- package/src/components/workflows/ExecutionTimeline.tsx +146 -0
- package/src/components/workflows/NodeInspector.tsx +257 -0
- package/src/components/workflows/NodePalette.tsx +119 -0
- package/src/components/workflows/WorkflowCanvas.tsx +113 -0
- package/src/components/workflows/WorkflowEdge.tsx +65 -0
- package/src/components/workflows/WorkflowEditor.tsx +130 -0
- package/src/components/workflows/WorkflowNode.tsx +198 -0
- package/src/components/workflows/WorkflowRunViewer.tsx +81 -0
- package/src/components/workflows/__tests__/ExecutionTimeline.test.tsx +99 -0
- package/src/components/workflows/__tests__/NodeInspector.test.tsx +74 -0
- package/src/components/workflows/__tests__/NodePalette.test.tsx +46 -0
- package/src/components/workflows/__tests__/WorkflowCanvas.test.tsx +59 -0
- package/src/components/workflows/__tests__/WorkflowNode.test.tsx +92 -0
- package/src/components/workflows/__tests__/WorkflowRunViewer.test.tsx +138 -0
- package/src/components/workflows/__tests__/auto-layout.test.ts +107 -0
- package/src/components/workflows/__tests__/serialization.test.ts +278 -0
- package/src/components/workflows/exec-status.ts +90 -0
- package/src/components/workflows/hooks/useCanvasGraph.ts +70 -0
- package/src/components/workflows/hooks/useNodeStatusOverlay.ts +47 -0
- package/src/components/workflows/index.ts +78 -0
- package/src/components/workflows/layout/auto-layout.ts +142 -0
- package/src/components/workflows/node-icons.ts +31 -0
- package/src/components/workflows/serialization.ts +171 -0
- package/src/components/workflows/theme/categories.ts +96 -0
- package/src/components/workflows/types.ts +231 -0
- package/src/components/workflows/workflows.css +29 -0
- package/src/components/workspace/DualPaneWorkspace.tsx +187 -0
- package/src/components/workspace/SplitPane.tsx +174 -0
- 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
|
|
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
|
-
|
|
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
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
- **Tailwind CSS Integration**: Pre-configured theme and utilities
|
|
21
|
-
- **TypeScript**: Full type safety
|
|
20
|
+
---
|
|
22
21
|
|
|
23
|
-
##
|
|
22
|
+
## Install / wire-up
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
---
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
93
|
+
You also need this package as a `presets` entry or to merge its theme by hand:
|
|
38
94
|
|
|
39
|
-
|
|
95
|
+
```js
|
|
96
|
+
// (option A) preset
|
|
97
|
+
module.exports = { presets: [require('@startsimpli/ui/tailwind')], content: [/* ... */] }
|
|
98
|
+
```
|
|
40
99
|
|
|
41
|
-
|
|
100
|
+
…and add `'@startsimpli/ui'` to `transpilePackages` in `next.config.{ts,js}` so
|
|
101
|
+
Next compiles its JSX with the app.
|
|
42
102
|
|
|
43
|
-
|
|
103
|
+
---
|
|
44
104
|
|
|
45
|
-
|
|
46
|
-
import { UnifiedTable } from '@startsimpli/ui/table'
|
|
105
|
+
## Theme tokens (CSS-variable contract)
|
|
47
106
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
317
|
+
<UnifiedTable<User>
|
|
318
|
+
tableId="users"
|
|
319
|
+
data={data?.items ?? []}
|
|
72
320
|
columns={columns}
|
|
73
|
-
|
|
74
|
-
|
|
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,
|
|
328
|
+
serverSide: true, // REQUIRED
|
|
81
329
|
onPageChange: setPage,
|
|
82
330
|
}}
|
|
83
331
|
sorting={{
|
|
84
332
|
enabled: true,
|
|
85
|
-
serverSide: true,
|
|
86
|
-
|
|
87
|
-
|
|
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:
|
|
95
|
-
onChange:
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
**ALL tables MUST use server-side operations. This is non-negotiable.**
|
|
348
|
+
### Why server-side, always
|
|
110
349
|
|
|
111
350
|
```tsx
|
|
112
|
-
//
|
|
113
|
-
pagination={{
|
|
114
|
-
|
|
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
|
-
//
|
|
126
|
-
pagination={{
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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:
|
|
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
|
-
|
|
400
|
+
### Inline editing
|
|
219
401
|
|
|
220
402
|
```tsx
|
|
221
403
|
<UnifiedTable
|
|
222
|
-
columns={[
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
},
|
|
411
|
+
optimisticUpdate: true,
|
|
412
|
+
onSave: async (rowId, columnId, value) => updateUser(rowId, { [columnId]: value }),
|
|
240
413
|
}}
|
|
241
414
|
/>
|
|
242
415
|
```
|
|
243
416
|
|
|
244
|
-
|
|
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
|
|
268
|
-
onSaveView: async (
|
|
269
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
471
|
+
---
|
|
486
472
|
|
|
487
|
-
|
|
488
|
-
# Run tests
|
|
489
|
-
npm test
|
|
473
|
+
## QueryProvider
|
|
490
474
|
|
|
491
|
-
|
|
492
|
-
npm run test:watch
|
|
475
|
+
A pre-configured `QueryClientProvider` so every app shares the same defaults:
|
|
493
476
|
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
- Firefox (latest)
|
|
528
|
-
- Safari (latest)
|
|
529
|
-
- Edge (latest)
|
|
585
|
+
---
|
|
530
586
|
|
|
531
|
-
##
|
|
587
|
+
## Shared-first reminder
|
|
532
588
|
|
|
533
|
-
|
|
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
|
|
596
|
+
Proprietary — StartSimpli.
|