hs-uix 2.1.1 → 2.2.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/README.md +3 -1
- package/common-components.d.ts +319 -68
- package/dist/calendar.js +355 -57
- package/dist/calendar.mjs +356 -57
- package/dist/common-components.js +3546 -88
- package/dist/common-components.mjs +3530 -84
- package/dist/datatable.js +108 -18
- package/dist/datatable.mjs +108 -18
- package/dist/experimental.js +2876 -0
- package/dist/experimental.mjs +2883 -0
- package/dist/feed.js +267 -38
- package/dist/feed.mjs +260 -37
- package/dist/filter.js +1379 -0
- package/dist/filter.mjs +1334 -0
- package/dist/form.js +222 -26
- package/dist/form.mjs +227 -27
- package/dist/index.js +3208 -287
- package/dist/index.mjs +3156 -283
- package/dist/kanban.js +282 -62
- package/dist/kanban.mjs +273 -61
- package/dist/safe.js +9207 -0
- package/dist/safe.mjs +9298 -0
- package/dist/utils.js +491 -75
- package/dist/utils.mjs +491 -75
- package/experimental.d.ts +1 -0
- package/filter.d.ts +1 -0
- package/index.d.ts +45 -3
- package/package.json +19 -1
- package/safe.d.ts +1 -0
- package/src/calendar/README.md +74 -5
- package/src/calendar/index.d.ts +95 -1
- package/src/common-components/README.md +140 -1
- package/src/datatable/README.md +0 -2
- package/src/experimental/README.md +126 -0
- package/src/experimental/index.d.ts +346 -0
- package/src/feed/README.md +69 -0
- package/src/feed/index.d.ts +103 -0
- package/src/filter/README.md +148 -0
- package/src/filter/index.d.ts +221 -0
- package/src/form/README.md +132 -4
- package/src/form/index.d.ts +82 -1
- package/src/kanban/README.md +119 -6
- package/src/kanban/index.d.ts +153 -2
- package/src/safe/README.md +108 -0
- package/src/safe/index.d.ts +158 -0
- package/src/utils/README.md +39 -0
- package/src/wizard/README.md +158 -0
- package/src/wizard/index.d.ts +138 -0
- package/utils.d.ts +17 -0
package/src/kanban/index.d.ts
CHANGED
|
@@ -86,6 +86,13 @@ export interface KanbanStage<Row = Record<string, unknown>> {
|
|
|
86
86
|
icon?: string;
|
|
87
87
|
terminal?: boolean;
|
|
88
88
|
order?: number;
|
|
89
|
+
/**
|
|
90
|
+
* WIP limit for this stage. The header shows "count / limit" and an
|
|
91
|
+
* "Over WIP" StatusTag when exceeded. 0 is valid ("stay empty"). The
|
|
92
|
+
* top-level `wipLimits` prop overrides this per stage. Advisory only —
|
|
93
|
+
* transitions that exceed the limit still complete.
|
|
94
|
+
*/
|
|
95
|
+
wipLimit?: number;
|
|
89
96
|
footer?: (rows: Row[]) => ReactNode;
|
|
90
97
|
canEnter?: (row: Row) => boolean;
|
|
91
98
|
onEnterRequired?: {
|
|
@@ -93,6 +100,46 @@ export interface KanbanStage<Row = Record<string, unknown>> {
|
|
|
93
100
|
};
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// WIP limits
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export interface KanbanWipStatus {
|
|
108
|
+
/** Cards counted against the limit (stageMeta.totalCount when present, else the loaded bucket size). */
|
|
109
|
+
count: number;
|
|
110
|
+
/** Effective limit (top-level override beats stage.wipLimit), or null when the stage has none. */
|
|
111
|
+
limit: number | null;
|
|
112
|
+
/** True only when count is STRICTLY greater than limit. */
|
|
113
|
+
exceeded: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type KanbanWipEvaluation = Record<string, KanbanWipStatus>;
|
|
117
|
+
|
|
118
|
+
export interface KanbanWipExceededEvent {
|
|
119
|
+
stageId: string;
|
|
120
|
+
count: number;
|
|
121
|
+
limit: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Swimlanes
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
export type KanbanSwimlaneBy<Row = Record<string, unknown>> =
|
|
129
|
+
| string
|
|
130
|
+
| ((row: Row) => string | number | boolean | null | undefined);
|
|
131
|
+
|
|
132
|
+
export type KanbanSwimlaneLabels<Row = Record<string, unknown>> =
|
|
133
|
+
| Record<string, string>
|
|
134
|
+
| ((laneKey: string, rows: Row[]) => ReactNode);
|
|
135
|
+
|
|
136
|
+
export interface KanbanLanePartition<Row = Record<string, unknown>> {
|
|
137
|
+
/** Lane keys in render order (swimlaneOrder first — including empty explicit lanes — then first-seen). */
|
|
138
|
+
laneKeys: string[];
|
|
139
|
+
/** Rows per lane in original data order. Every key in laneKeys is present. */
|
|
140
|
+
rowsByLane: Record<string, Row[]>;
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
// ---------------------------------------------------------------------------
|
|
97
144
|
// Pagination
|
|
98
145
|
// ---------------------------------------------------------------------------
|
|
@@ -147,6 +194,7 @@ export interface KanbanParams {
|
|
|
147
194
|
filters: Record<string, unknown>;
|
|
148
195
|
sort: string | null;
|
|
149
196
|
collapsedStages: string[];
|
|
197
|
+
collapsedLanes: string[];
|
|
150
198
|
}
|
|
151
199
|
|
|
152
200
|
export interface KanbanEmptyStateRenderContext {
|
|
@@ -183,6 +231,13 @@ export interface KanbanLabels {
|
|
|
183
231
|
errorTitle?: string;
|
|
184
232
|
errorMessage?: string;
|
|
185
233
|
cardCount?: (n: number) => string;
|
|
234
|
+
/** Stage-header count when a WIP limit is set. Default "{count} / {limit}". */
|
|
235
|
+
wipCount?: (count: number, limit: number) => string;
|
|
236
|
+
/** Warning StatusTag text on over-limit stage headers. Default "Over WIP". */
|
|
237
|
+
overWip?: string;
|
|
238
|
+
laneCount?: (n: number) => string;
|
|
239
|
+
/** Lane header label for rows whose swimlane value is null/undefined/"". Default "Unassigned". */
|
|
240
|
+
unassignedLane?: string;
|
|
186
241
|
moveTo?: string;
|
|
187
242
|
clearAll?: string;
|
|
188
243
|
selectAll?: string | ((count: number, label: string) => string);
|
|
@@ -278,6 +333,37 @@ export interface KanbanProps<Row = Record<string, unknown>, Id = string | number
|
|
|
278
333
|
stageMeta?: Record<string, KanbanStageMeta>;
|
|
279
334
|
onLoadMore?: (stage: string) => void;
|
|
280
335
|
|
|
336
|
+
// WIP limits
|
|
337
|
+
/** Top-level per-stage limit overrides: `{ [stageId]: n }`. Beats `stage.wipLimit`. */
|
|
338
|
+
wipLimits?: Record<string, number>;
|
|
339
|
+
/**
|
|
340
|
+
* Fires when a stage TRANSITIONS into the exceeded state (count > limit) —
|
|
341
|
+
* once per crossing, including a board that mounts already over a limit;
|
|
342
|
+
* never on every render. Over-limit transitions still complete: the server
|
|
343
|
+
* is the source of truth, so WIP limits are a signal, not a gate.
|
|
344
|
+
*/
|
|
345
|
+
onWipExceeded?: (stageId: string, count: number, limit: number) => void;
|
|
346
|
+
|
|
347
|
+
// Swimlanes
|
|
348
|
+
/** Field name or accessor that groups the board vertically into lanes. */
|
|
349
|
+
swimlaneBy?: KanbanSwimlaneBy<Row>;
|
|
350
|
+
/** `{ key: label }` map or `(laneKey, rows) => label` function for lane headers. */
|
|
351
|
+
swimlaneLabels?: KanbanSwimlaneLabels<Row>;
|
|
352
|
+
/** Explicit lane order. Listed keys render first (even when empty); unlisted lanes append first-seen. */
|
|
353
|
+
swimlaneOrder?: string[];
|
|
354
|
+
/** Show the collapse chevron on lane headers. Default true. */
|
|
355
|
+
collapseLanes?: boolean;
|
|
356
|
+
/** Controlled list of collapsed lane keys. */
|
|
357
|
+
collapsedLanes?: string[];
|
|
358
|
+
/** Initial collapsed lane keys (uncontrolled). */
|
|
359
|
+
defaultCollapsedLanes?: string[];
|
|
360
|
+
onCollapsedLanesChange?: (laneKeys: string[]) => void;
|
|
361
|
+
/**
|
|
362
|
+
* Render the metrics panel inside each lane instead of globally. Requires
|
|
363
|
+
* `metrics` to be a function — it's called per lane with `(laneRows, laneKey)`.
|
|
364
|
+
*/
|
|
365
|
+
metricsPerLane?: boolean;
|
|
366
|
+
|
|
281
367
|
// Selection
|
|
282
368
|
selectable?: boolean;
|
|
283
369
|
selectedIds?: Id[];
|
|
@@ -345,8 +431,16 @@ export interface KanbanProps<Row = Record<string, unknown>, Id = string | number
|
|
|
345
431
|
onCollapsedStagesChange?: (stages: string[]) => void;
|
|
346
432
|
|
|
347
433
|
// Metrics panel (toggled via the toolbar's Metrics button)
|
|
348
|
-
/**
|
|
349
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Array of stat items for the <Statistics> panel, a custom ReactNode, or a
|
|
436
|
+
* function of the (filtered) rows. The function form receives
|
|
437
|
+
* `(rows, laneKey)` — laneKey is null for the global panel and the lane key
|
|
438
|
+
* when `metricsPerLane` is active.
|
|
439
|
+
*/
|
|
440
|
+
metrics?:
|
|
441
|
+
| KanbanMetricItem[]
|
|
442
|
+
| ReactNode
|
|
443
|
+
| ((rows: Row[], laneKey: string | null) => KanbanMetricItem[] | ReactNode);
|
|
350
444
|
/** Controlled visibility of the metrics panel. */
|
|
351
445
|
showMetrics?: boolean;
|
|
352
446
|
/** Fires when the Metrics button is clicked. Receives the new visible state. */
|
|
@@ -376,3 +470,60 @@ export interface KanbanProps<Row = Record<string, unknown>, Id = string | number
|
|
|
376
470
|
export declare function Kanban<Row = Record<string, unknown>, Id = string | number>(
|
|
377
471
|
props: KanbanProps<Row, Id>
|
|
378
472
|
): ReactElement | null;
|
|
473
|
+
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// Pure lane / WIP helpers (from kanbanLanes.js) — usable outside the component,
|
|
476
|
+
// e.g. to evaluate WIP server-side or render custom lane summaries.
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
/** Lane key used for rows whose swimlane value is null/undefined/"". */
|
|
480
|
+
export declare const UNASSIGNED_LANE_KEY: "__unassigned";
|
|
481
|
+
|
|
482
|
+
/** Resolve the swimlane key for a row (String-coerced; empty values map to UNASSIGNED_LANE_KEY). */
|
|
483
|
+
export declare function getLaneKey<Row = Record<string, unknown>>(
|
|
484
|
+
row: Row,
|
|
485
|
+
swimlaneBy: KanbanSwimlaneBy<Row>
|
|
486
|
+
): string;
|
|
487
|
+
|
|
488
|
+
/** Order lane keys: explicit swimlaneOrder first (deduped, kept even when empty), then first-seen. */
|
|
489
|
+
export declare function orderLaneKeys(seenKeys: string[], swimlaneOrder?: string[]): string[];
|
|
490
|
+
|
|
491
|
+
/** Partition rows into swimlanes — render order plus rows per lane. */
|
|
492
|
+
export declare function partitionLanes<Row = Record<string, unknown>>(
|
|
493
|
+
rows: Row[],
|
|
494
|
+
options?: { swimlaneBy?: KanbanSwimlaneBy<Row>; swimlaneOrder?: string[] }
|
|
495
|
+
): KanbanLanePartition<Row>;
|
|
496
|
+
|
|
497
|
+
/** Resolve a lane's display label from a map or function, with unassigned/key fallbacks. */
|
|
498
|
+
export declare function resolveLaneLabel<Row = Record<string, unknown>>(
|
|
499
|
+
laneKey: string,
|
|
500
|
+
swimlaneLabels?: KanbanSwimlaneLabels<Row>,
|
|
501
|
+
rows?: Row[],
|
|
502
|
+
unassignedLabel?: string
|
|
503
|
+
): ReactNode;
|
|
504
|
+
|
|
505
|
+
/** Effective WIP limit for a stage (wipLimits override beats stage.wipLimit; invalid → null). */
|
|
506
|
+
export declare function resolveWipLimit<Row = Record<string, unknown>>(
|
|
507
|
+
stage: KanbanStage<Row>,
|
|
508
|
+
wipLimits?: Record<string, number>
|
|
509
|
+
): number | null;
|
|
510
|
+
|
|
511
|
+
/** Per-stage counts for WIP evaluation (stageMeta.totalCount when present, else bucket length). */
|
|
512
|
+
export declare function computeStageCounts<Row = Record<string, unknown>>(
|
|
513
|
+
stages: KanbanStage<Row>[],
|
|
514
|
+
buckets: Record<string, Row[]>,
|
|
515
|
+
stageMeta?: Record<string, KanbanStageMeta>
|
|
516
|
+
): Record<string, number>;
|
|
517
|
+
|
|
518
|
+
/** Evaluate `{ count, limit, exceeded }` for every stage. */
|
|
519
|
+
export declare function evaluateWip<Row = Record<string, unknown>>(
|
|
520
|
+
stages: KanbanStage<Row>[],
|
|
521
|
+
counts: Record<string, number>,
|
|
522
|
+
wipLimits?: Record<string, number>
|
|
523
|
+
): KanbanWipEvaluation;
|
|
524
|
+
|
|
525
|
+
/** Diff two evaluations; returns stages that newly crossed into exceeded (the onWipExceeded contract). */
|
|
526
|
+
export declare function findNewlyExceededWip(
|
|
527
|
+
prev: KanbanWipEvaluation | null | undefined,
|
|
528
|
+
next: KanbanWipEvaluation
|
|
529
|
+
): KanbanWipExceededEvent[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Safe wrappers (`hs-uix/safe`)
|
|
2
|
+
|
|
3
|
+
Hardened drop-ins for the components that fail worst when given bad props.
|
|
4
|
+
HubSpot's primitives have two ugly failure modes:
|
|
5
|
+
|
|
6
|
+
- **Silent fails** — `Icon` renders *nothing* for an invalid `name` (no error,
|
|
7
|
+
no fallback, just empty space). `StatisticsTrend` rejects any `direction`
|
|
8
|
+
that isn't exactly `"increase"`/`"decrease"`.
|
|
9
|
+
- **Throw-blanks-page** — `EmptyState` throws on a bad `imageName`, and
|
|
10
|
+
collection components (`DataTable`, `Select`, …) throw inside HubSpot's
|
|
11
|
+
reconciler when a required array prop arrives `undefined` — which blanks the
|
|
12
|
+
**whole extension** in production.
|
|
13
|
+
|
|
14
|
+
Every wrapper here keeps the native prop API and turns those failures into
|
|
15
|
+
safe degrades plus a **one-time** `console.warn`. They were battle-tested in
|
|
16
|
+
[hs-uix-studio](https://github.com/05bmckay/hs-uix-studio)'s renderer, where
|
|
17
|
+
LLM-generated UIs hit every one of these traps constantly.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
import {
|
|
23
|
+
SafeIcon, // bad name → alias repair, else red xCircle placeholder
|
|
24
|
+
SafeEmptyState, // bad imageName → known alias or "components", not a throw
|
|
25
|
+
SafeDataTable, // data/columns/… undefined → [] (empty table, not a blank page)
|
|
26
|
+
SafePopover, // children auto-padded in a compact Tile
|
|
27
|
+
} from "hs-uix/safe";
|
|
28
|
+
|
|
29
|
+
<SafeIcon name="duplicate" /> {/* renders "copy", warns once */}
|
|
30
|
+
<SafeEmptyState imageName="new-project" title="Nothing here" />
|
|
31
|
+
<SafeDataTable data={rows} columns={columns} /> {/* rows may be undefined while loading */}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What's included
|
|
35
|
+
|
|
36
|
+
### Repairing wrappers (native components)
|
|
37
|
+
|
|
38
|
+
| Component | Repairs | Behavior |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `SafeIcon` | `name` | Valid → native pass-through. Known alias (`duplicate`→`copy`, `alert`→`warning`, `trash`→`delete`, …) → repaired with one-time warn. Unknown → alert-colored `xCircle` placeholder with `screenReaderText="Invalid icon: <name>"`. |
|
|
41
|
+
| `SafeEmptyState` | `imageName` | `null`/valid → pass-through. Known alias (`new-project`→`components`, …) or unknown → falls back (default `"components"`) instead of throwing. |
|
|
42
|
+
| `SafeStatisticsTrend` | `direction` | Valid / non-string → pass-through. Alias (`increasing`, `up`, `positive`, …) → repaired. Unknown string → `"increase"`. |
|
|
43
|
+
| `SafePopover` | padding | Wraps children in `<Tile compact>` so popover content gets default padding (the experimental Popover renders children flush). Nesting your own Tile still works. |
|
|
44
|
+
|
|
45
|
+
### Array-coercing wrappers
|
|
46
|
+
|
|
47
|
+
Required collection props are forced to arrays: `null`/`undefined` → `[]`
|
|
48
|
+
silently, any other non-array → `[]` with a one-time warn. The component's own
|
|
49
|
+
empty state renders instead of the page blanking. Refs forward through, so
|
|
50
|
+
`SafeFormBuilder` keeps FormBuilder's imperative ref API.
|
|
51
|
+
|
|
52
|
+
| Component | Coerced props |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `SafeSelect` / `SafeMultiSelect` / `SafeToggleGroup` | `options` |
|
|
55
|
+
| `SafeStepIndicator` | `stepNames` |
|
|
56
|
+
| `SafeDataTable` | `data`, `columns`, `searchFields`, `filters`, `selectionActions` |
|
|
57
|
+
| `SafeKanban` | `data`, `stages` |
|
|
58
|
+
| `SafeFormBuilder` | `fields` |
|
|
59
|
+
| `SafeAvatarStack` / `SafeKeyValueList` | `items` |
|
|
60
|
+
| `SafeFeed` | `items`, `fields` |
|
|
61
|
+
| `SafeCalendar` | `events` |
|
|
62
|
+
| `SafeCrmKanban` | `cardFields` |
|
|
63
|
+
|
|
64
|
+
`SafeCrmDataTable.columns` and `SafeCrmKanban.stages` are different: those
|
|
65
|
+
props **auto-derive when omitted** (columns from CRM properties, stages from
|
|
66
|
+
the batch), and a coerced `[]` would silently turn derivation off. They pass
|
|
67
|
+
`null`/`undefined` through untouched, and an invalid non-array is *dropped*
|
|
68
|
+
(with a warn) so the derive path takes over.
|
|
69
|
+
|
|
70
|
+
Wrap anything else yourself:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { withSafeArrayProps, SAFE_ARRAY_PROPS, SAFE_DERIVE_PROPS } from "hs-uix/safe";
|
|
74
|
+
|
|
75
|
+
const SafeMyList = withSafeArrayProps(MyList, "MyList", ["entries"]);
|
|
76
|
+
// withSafeArrayProps(Component, name, arrayProps, deriveProps?) — the
|
|
77
|
+
// SAFE_ARRAY_PROPS / SAFE_DERIVE_PROPS registries hold the lists used above
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Catalogs
|
|
81
|
+
|
|
82
|
+
The validation data is exported for building your own linting/repair layers
|
|
83
|
+
(spec validators, codegen checks):
|
|
84
|
+
|
|
85
|
+
| Export | Contents |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `NATIVE_ICON_NAMES` | the native `<Icon>` `name` whitelist (190 names) |
|
|
88
|
+
| `ICON_NAME_ALIASES` | common mistakes → nearest valid icon |
|
|
89
|
+
| `EMPTY_STATE_IMAGES` / `EMPTY_STATE_IMAGE_ALIASES` | valid `imageName` values + repairs |
|
|
90
|
+
| `TREND_DIRECTIONS` / `TREND_DIRECTION_ALIASES` | valid `direction` values + repairs |
|
|
91
|
+
| `SAFE_ARRAY_PROPS` | required collection props per component |
|
|
92
|
+
|
|
93
|
+
### Warnings
|
|
94
|
+
|
|
95
|
+
Each distinct problem warns **once** per session (a bad icon name in a 50-row
|
|
96
|
+
table logs one line, not fifty). `resetSafeWarnings()` clears the dedup memory
|
|
97
|
+
(useful in tests); `warnOnce(key, message)` is the underlying helper.
|
|
98
|
+
|
|
99
|
+
## When to use
|
|
100
|
+
|
|
101
|
+
- Rendering **model-generated or user-configured** UI, where prop values are
|
|
102
|
+
data, not code you control.
|
|
103
|
+
- Any surface where `props.data` comes from an async source that can resolve
|
|
104
|
+
`undefined` — the Safe collection components keep the page alive while you
|
|
105
|
+
fix the data path.
|
|
106
|
+
|
|
107
|
+
If your props are static and hand-written, the natives are fine — these
|
|
108
|
+
wrappers add one function call per render and nothing else.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import type { DataTableProps } from "../datatable/index";
|
|
3
|
+
import type { KanbanProps } from "../kanban/index";
|
|
4
|
+
import type { FormBuilderProps } from "../form/index";
|
|
5
|
+
import type { FeedItem, FeedProps } from "../feed/index";
|
|
6
|
+
import type { CalendarProps } from "../calendar/index";
|
|
7
|
+
import type { AvatarStackProps, KeyValueListProps } from "../../common-components";
|
|
8
|
+
import type { CrmDataTableProps, CrmKanbanProps } from "../../utils";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Catalogs
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** The native <Icon> `name` whitelist. */
|
|
15
|
+
export declare const NATIVE_ICON_NAMES: ReadonlySet<string>;
|
|
16
|
+
/** Common icon-name mistakes → the nearest valid native name. */
|
|
17
|
+
export declare const ICON_NAME_ALIASES: Readonly<Record<string, string>>;
|
|
18
|
+
/** Valid EmptyState `imageName` values. */
|
|
19
|
+
export declare const EMPTY_STATE_IMAGES: ReadonlySet<string>;
|
|
20
|
+
/** Common imageName mistakes → a valid value. */
|
|
21
|
+
export declare const EMPTY_STATE_IMAGE_ALIASES: Readonly<Record<string, string>>;
|
|
22
|
+
/** Valid StatisticsTrend `direction` values ("increase" | "decrease"). */
|
|
23
|
+
export declare const TREND_DIRECTIONS: ReadonlySet<string>;
|
|
24
|
+
/** Common direction mistakes ("increasing", "up", …) → a valid value. */
|
|
25
|
+
export declare const TREND_DIRECTION_ALIASES: Readonly<Record<string, string>>;
|
|
26
|
+
/** Required collection props per component name (coerced to []), as used by the Safe* exports. */
|
|
27
|
+
export declare const SAFE_ARRAY_PROPS: Readonly<Record<string, readonly string[]>>;
|
|
28
|
+
/**
|
|
29
|
+
* Auto-derived-when-omitted collection props (CrmDataTable columns, CrmKanban
|
|
30
|
+
* stages). Never coerced to [] — that would suppress the derive path; invalid
|
|
31
|
+
* non-array values are dropped instead.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SAFE_DERIVE_PROPS: Readonly<Record<string, readonly string[]>>;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Warning dedup
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** console.warn `message` the first time `key` is seen; no-op after that. */
|
|
40
|
+
export declare function warnOnce(key: string, message: string): void;
|
|
41
|
+
/** Clear the warn-once memory — for tests or long-lived sessions. */
|
|
42
|
+
export declare function resetSafeWarnings(): void;
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Generic hardening wrapper
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Wrap any component so the listed props are always arrays: null/undefined
|
|
50
|
+
* coerce to [] silently, any other non-array coerces to [] with a one-time
|
|
51
|
+
* console.warn. `derivePropNames` lists auto-derived-when-omitted props,
|
|
52
|
+
* which instead pass null/undefined through and DROP invalid non-arrays so
|
|
53
|
+
* the component's own derivation still runs. Refs forward through. Returns a
|
|
54
|
+
* drop-in with displayName `Safe<componentName>`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function withSafeArrayProps<Props extends object>(
|
|
57
|
+
Component: ComponentType<Props>,
|
|
58
|
+
componentName: string,
|
|
59
|
+
propNames: ReadonlyArray<string>,
|
|
60
|
+
derivePropNames?: ReadonlyArray<string>
|
|
61
|
+
): ComponentType<Props>;
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Hardened native components (drop-ins for @hubspot/ui-extensions)
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export interface SafeIconProps {
|
|
68
|
+
/** Native icon name. Known aliases auto-repair; anything else renders an alert-colored xCircle placeholder. */
|
|
69
|
+
name: string;
|
|
70
|
+
color?: "alert" | "warning" | "success" | "inherit";
|
|
71
|
+
size?: "sm" | "small" | "md" | "medium" | "lg" | "large";
|
|
72
|
+
screenReaderText?: string;
|
|
73
|
+
[prop: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SafeEmptyStateProps {
|
|
77
|
+
/** Invalid values fall back to a known alias or "components" instead of throwing. */
|
|
78
|
+
imageName?: string;
|
|
79
|
+
children?: ReactNode;
|
|
80
|
+
[prop: string]: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SafeStatisticsTrendProps {
|
|
84
|
+
/** "increase" | "decrease"; aliases like "increasing"/"up" auto-repair, anything else defaults to "increase". */
|
|
85
|
+
direction?: "increase" | "decrease" | (string & {});
|
|
86
|
+
value?: string;
|
|
87
|
+
[prop: string]: unknown;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SafePopoverProps {
|
|
91
|
+
/** Wrapped in a compact <Tile> so content gets default padding. */
|
|
92
|
+
children?: ReactNode;
|
|
93
|
+
[prop: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SafeOptionsProps {
|
|
97
|
+
/** Coerced to [] when missing or non-array. */
|
|
98
|
+
options?: Array<Record<string, unknown>> | null;
|
|
99
|
+
[prop: string]: unknown;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SafeStepIndicatorProps {
|
|
103
|
+
/** Coerced to [] when missing or non-array. */
|
|
104
|
+
stepNames?: string[] | null;
|
|
105
|
+
[prop: string]: unknown;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export declare const SafeIcon: ComponentType<SafeIconProps>;
|
|
109
|
+
export declare const SafeEmptyState: ComponentType<SafeEmptyStateProps>;
|
|
110
|
+
export declare const SafeStatisticsTrend: ComponentType<SafeStatisticsTrendProps>;
|
|
111
|
+
export declare const SafePopover: ComponentType<SafePopoverProps>;
|
|
112
|
+
export declare const SafeSelect: ComponentType<SafeOptionsProps>;
|
|
113
|
+
export declare const SafeMultiSelect: ComponentType<SafeOptionsProps>;
|
|
114
|
+
export declare const SafeToggleGroup: ComponentType<SafeOptionsProps>;
|
|
115
|
+
export declare const SafeStepIndicator: ComponentType<SafeStepIndicatorProps>;
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Hardened hs-uix components — same props as the originals, except the
|
|
119
|
+
// coerced collection props (see SAFE_ARRAY_PROPS / SAFE_DERIVE_PROPS) also
|
|
120
|
+
// accept null/undefined, matching the runtime tolerance that is the point of
|
|
121
|
+
// these wrappers.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/** The original props with the hardened collection props widened to optional-and-nullable. */
|
|
125
|
+
type WithNullableArrays<Props, K extends keyof Props> = Omit<Props, K> & {
|
|
126
|
+
[P in K]?: Props[P] | null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export declare function SafeDataTable<Row = Record<string, unknown>>(
|
|
130
|
+
props: WithNullableArrays<
|
|
131
|
+
DataTableProps<Row>,
|
|
132
|
+
"data" | "columns" | "searchFields" | "filters" | "selectionActions"
|
|
133
|
+
>
|
|
134
|
+
): ReactNode;
|
|
135
|
+
export declare function SafeKanban<Row = Record<string, unknown>, Id = string | number>(
|
|
136
|
+
props: WithNullableArrays<KanbanProps<Row, Id>, "data" | "stages">
|
|
137
|
+
): ReactNode;
|
|
138
|
+
export declare const SafeFormBuilder: ComponentType<
|
|
139
|
+
WithNullableArrays<FormBuilderProps, "fields">
|
|
140
|
+
>;
|
|
141
|
+
export declare function SafeFeed<Row = FeedItem>(
|
|
142
|
+
props: WithNullableArrays<FeedProps<Row>, "items" | "fields">
|
|
143
|
+
): ReactNode;
|
|
144
|
+
export declare function SafeCalendar<Event = Record<string, unknown>>(
|
|
145
|
+
props: WithNullableArrays<CalendarProps<Event>, "events">
|
|
146
|
+
): ReactNode;
|
|
147
|
+
export declare const SafeAvatarStack: ComponentType<
|
|
148
|
+
WithNullableArrays<AvatarStackProps, "items">
|
|
149
|
+
>;
|
|
150
|
+
export declare const SafeKeyValueList: ComponentType<
|
|
151
|
+
WithNullableArrays<KeyValueListProps, "items">
|
|
152
|
+
>;
|
|
153
|
+
export declare function SafeCrmDataTable<Row = Record<string, unknown>>(
|
|
154
|
+
props: WithNullableArrays<CrmDataTableProps<Row>, "columns">
|
|
155
|
+
): ReactNode;
|
|
156
|
+
export declare function SafeCrmKanban<Row = Record<string, unknown>>(
|
|
157
|
+
props: WithNullableArrays<CrmKanbanProps<Row>, "stages" | "cardFields">
|
|
158
|
+
): ReactNode;
|
package/src/utils/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Pure helper functions for formatting, mapping, guards, and lightweight data tran
|
|
|
12
12
|
- `viewAdapters.js` — shape transforms between DataTable columns and Kanban cardFields (power a single "same data, different view" toggle)
|
|
13
13
|
- `query.js` — shared collection query helpers: empty filter values, filter reset, active-filter chips, filtering, and search
|
|
14
14
|
- `crmSearchAdapters.js` — CRM-bound data components (`CrmDataTable`, `CrmKanban`) plus the lower-level CRM search hooks and config builders behind them
|
|
15
|
+
- `applyPatches.js` — RFC 6902 JSON Patch applier (add/replace/remove/move/copy) with structural sharing, built for streaming-UI flows
|
|
15
16
|
|
|
16
17
|
## Purpose
|
|
17
18
|
|
|
@@ -503,6 +504,44 @@ If you need to drive a custom view, the hooks and helpers are exported directly:
|
|
|
503
504
|
|
|
504
505
|
---
|
|
505
506
|
|
|
507
|
+
## applyPatches.js
|
|
508
|
+
|
|
509
|
+
RFC 6902 JSON Patch applier — the minimal subset that streaming UIs need.
|
|
510
|
+
Supported ops: `add`, `replace`, `remove`, `move`, `copy`; anything else
|
|
511
|
+
(including `test`) is skipped with a console warning.
|
|
512
|
+
|
|
513
|
+
### `applyPatches(doc, patches)`
|
|
514
|
+
|
|
515
|
+
```js
|
|
516
|
+
import { applyPatches } from "hs-uix/utils";
|
|
517
|
+
|
|
518
|
+
const doc = { meta: { title: "Dashboard" }, rows: [] };
|
|
519
|
+
|
|
520
|
+
const next = applyPatches(doc, [
|
|
521
|
+
{ op: "replace", path: "/meta/title", value: "Pipeline" },
|
|
522
|
+
{ op: "add", path: "/rows/-", value: { id: "r1" } },
|
|
523
|
+
]);
|
|
524
|
+
// → { meta: { title: "Pipeline" }, rows: [{ id: "r1" }] }
|
|
525
|
+
|
|
526
|
+
next === doc; // → false (never mutates input)
|
|
527
|
+
next.meta === doc.meta; // → false (changed branch)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Behavior worth knowing:
|
|
531
|
+
|
|
532
|
+
| Behavior | Detail |
|
|
533
|
+
|---|---|
|
|
534
|
+
| Immutability | Returns a **new** document; untouched branches keep reference equality (structural sharing), so memoized renderers only re-render what changed. |
|
|
535
|
+
| Permissive paths | Where RFC 6902 would error on a missing path prefix, `add`/`replace` create it — objects by default, arrays when the next segment is numeric or `-`. Built for patch streams where `/elements/x/props` can arrive before `/elements`. |
|
|
536
|
+
| Root pointer | `path: ""` (or `"/"`) replaces the whole document. |
|
|
537
|
+
| Array `add` vs `replace` | Per RFC 6902, `add` at an existing index **inserts** (shifts the rest right); `replace` overwrites in place. The standard `-` index appends (`{ op: "add", path: "/rows/-", value }`). |
|
|
538
|
+
| Array `remove` | An invalid or out-of-range index (`/rows/-`, `/rows/foo`, `/rows/99`) is a safe no-op — same spirit as removing a missing object key. |
|
|
539
|
+
| Escaping | Standard RFC 6901 unescaping: `~1` → `/`, `~0` → `~`. |
|
|
540
|
+
| `copy` / `move` | `copy` deep-clones the source (JSON-safe values only — no `Date`/`Map`/`Set`). Pointers resolve own properties only, so `/constructor`-style segments read as missing instead of leaking functions into the document. |
|
|
541
|
+
| Null doc | `applyPatches(null, patches)` starts from `{}`. Empty/missing `patches` returns the input as-is. |
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
506
545
|
## Guidelines
|
|
507
546
|
|
|
508
547
|
- Keep helpers pure and side-effect free
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Wizard (hs-uix/experimental)
|
|
2
|
+
|
|
3
|
+
Orchestrated multi-step flows for HubSpot UI Extensions — plus the "getting started" checklist card. `Wizard` steps render **arbitrary content** (tables, CRM pickers, review summaries — anything), which makes it the right tool when FormBuilder's form-only multi-step mode isn't enough. The Wizard owns the orchestration: a shared values bag, validate-gated Next, linear step reachability with success markers, a side step-nav (vertical) or native StepIndicator (horizontal), and a Back/Next/Finish footer. `OnboardingChecklist` is the companion setup tracker: a ProgressBar headline over rows of done/pending tasks with inline action buttons.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { Wizard } from "hs-uix/experimental";
|
|
9
|
+
import { Input, Flex, Text } from "@hubspot/ui-extensions";
|
|
10
|
+
|
|
11
|
+
<Wizard
|
|
12
|
+
steps={[
|
|
13
|
+
{
|
|
14
|
+
id: "details",
|
|
15
|
+
title: "Details",
|
|
16
|
+
description: "Who is this for?",
|
|
17
|
+
render: ({ values, setValues }) => (
|
|
18
|
+
<Input
|
|
19
|
+
name="email"
|
|
20
|
+
label="Email"
|
|
21
|
+
value={values.email || ""}
|
|
22
|
+
onChange={(email) => setValues({ email })}
|
|
23
|
+
/>
|
|
24
|
+
),
|
|
25
|
+
validate: ({ values }) => (values.email ? true : "Email is required."),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "options",
|
|
29
|
+
title: "Options",
|
|
30
|
+
optional: true,
|
|
31
|
+
render: ({ values, setValues }) => (
|
|
32
|
+
<Flex direction="column" gap="xs">{/* anything */}</Flex>
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "review",
|
|
37
|
+
title: "Review",
|
|
38
|
+
render: ({ values }) => <Text>Sending to {values.email || "--"}</Text>,
|
|
39
|
+
},
|
|
40
|
+
]}
|
|
41
|
+
onComplete={(values) => createRecord(values)}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```jsx
|
|
46
|
+
import { OnboardingChecklist } from "hs-uix/experimental";
|
|
47
|
+
|
|
48
|
+
<OnboardingChecklist
|
|
49
|
+
title="Getting started"
|
|
50
|
+
items={[
|
|
51
|
+
{ id: "connect", title: "Connect your calendar", done: true },
|
|
52
|
+
{
|
|
53
|
+
id: "import",
|
|
54
|
+
title: "Import contacts",
|
|
55
|
+
description: "CSV or CRM sync",
|
|
56
|
+
done: false,
|
|
57
|
+
action: { label: "Import", onClick: openImportPanel },
|
|
58
|
+
},
|
|
59
|
+
]}
|
|
60
|
+
onItemClick={(item) => openDetail(item.id)}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- **Arbitrary step content** — each step's `render(ctx)` returns any node; the wizard is layout, navigation, and gating only.
|
|
67
|
+
- **Shared values bag** — `ctx.values` / `ctx.setValues` let steps accumulate data; `onComplete(values)` hands the finished bag back.
|
|
68
|
+
- **Validate gating** — a step's `validate(ctx)` returning a non-empty string blocks Next/Finish and renders the message as an inline error `Alert`.
|
|
69
|
+
- **Linear reachability** — future steps are disabled until every prior required step is completed; `optional` steps are skippable; `allowJumpAhead` opens everything.
|
|
70
|
+
- **Side step-nav** (vertical, default): `checkCircle` success markers for completed steps, filled circle for the current step, hollow circles for upcoming; reachable steps are clickable Links.
|
|
71
|
+
- **Native StepIndicator** (horizontal): `stepNames` + `currentStep` + click-to-navigate, with `stepIndicatorProps` pass-through.
|
|
72
|
+
- **Back / Next / Finish footer** — secondary Back (disabled on the first step), primary Next/Finish; replace it wholesale with `renderFooter`.
|
|
73
|
+
- **Controlled or uncontrolled** — `step` / `defaultStep` / `onStepChange`, by step id or index.
|
|
74
|
+
- **OnboardingChecklist** — native `ProgressBar` (done/total), success-marker rows, inline action buttons, optional Accordion collapse that defaults open while work remains.
|
|
75
|
+
|
|
76
|
+
## Review / summary step
|
|
77
|
+
|
|
78
|
+
A review step is just a step that renders the values bag. Pair it with `KeyValueList` from `hs-uix/common-components`:
|
|
79
|
+
|
|
80
|
+
```jsx
|
|
81
|
+
import { KeyValueList } from "hs-uix/common-components";
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
id: "review",
|
|
85
|
+
title: "Review",
|
|
86
|
+
description: "Confirm before creating the record.",
|
|
87
|
+
render: ({ values, goTo }) => (
|
|
88
|
+
<Flex direction="column" gap="sm">
|
|
89
|
+
<KeyValueList
|
|
90
|
+
items={[
|
|
91
|
+
{ label: "Email", value: values.email },
|
|
92
|
+
{ label: "Plan", value: values.plan },
|
|
93
|
+
]}
|
|
94
|
+
/>
|
|
95
|
+
<Link onClick={() => goTo("details")}>Edit details</Link>
|
|
96
|
+
</Flex>
|
|
97
|
+
),
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The Finish button on the last step runs that step's `validate` (if any) and then calls `onComplete(values)`.
|
|
102
|
+
|
|
103
|
+
## `<Wizard>` props
|
|
104
|
+
|
|
105
|
+
| Prop | Type | Default | Notes |
|
|
106
|
+
|------|------|---------|-------|
|
|
107
|
+
| `steps` | `WizardStep[]` | `[]` | `{ id, title, description?, optional?, render?, validate? }`. Missing ids become `step-<index>`. |
|
|
108
|
+
| `step` | `string \| number` | — | Controlled current step (id or zero-based index). |
|
|
109
|
+
| `defaultStep` | `string \| number` | `0` | Initial step when uncontrolled. |
|
|
110
|
+
| `onStepChange` | `(stepId, stepIndex) => void` | — | Fires on every transition (Next, Back, nav click). |
|
|
111
|
+
| `onComplete` | `(values) => void` | — | Finish pressed on the last step and its validate passed. |
|
|
112
|
+
| `defaultValues` | `object` | `{}` | Initial contents of the shared values bag. |
|
|
113
|
+
| `onValuesChange` | `(values) => void` | — | Observer for every `setValues` call. |
|
|
114
|
+
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | Vertical = side step-nav; horizontal = native StepIndicator above content. |
|
|
115
|
+
| `showStepNav` | `boolean` | `true` | Hide the nav entirely (footer still navigates). |
|
|
116
|
+
| `allowJumpAhead` | `boolean` | `false` | Let users click any future step without completing prior ones. |
|
|
117
|
+
| `showStepHeader` | `boolean` | `true` | Render the active step's title/description above its content. |
|
|
118
|
+
| `labels` | `WizardLabels` | — | `{ back, next, finish, optional, errorTitle }` overrides. |
|
|
119
|
+
| `renderFooter` | `(ctx) => node` | — | Replaces the footer; ctx adds `{ isFirst, isLast, error, labels }`. |
|
|
120
|
+
| `navFlex` / `contentFlex` | `number` | `1` / `3` | Flex ratios for the vertical layout columns. |
|
|
121
|
+
| `stepIndicatorProps` | `object` | — | Spread onto the native StepIndicator (e.g. `variant="compact"`). |
|
|
122
|
+
|
|
123
|
+
### Step context (`ctx`)
|
|
124
|
+
|
|
125
|
+
Passed to `render`, `validate`, and `renderFooter`:
|
|
126
|
+
|
|
127
|
+
| Key | Type | Notes |
|
|
128
|
+
|-----|------|-------|
|
|
129
|
+
| `stepId` | `string` | Active step id. |
|
|
130
|
+
| `stepIndex` | `number` | Zero-based index. |
|
|
131
|
+
| `goNext()` | `() => void` | Validates, then advances (or completes on the last step). |
|
|
132
|
+
| `goBack()` | `() => void` | Previous step — never gated. |
|
|
133
|
+
| `goTo(stepOrId)` | `(string \| number) => void` | No-op when the target isn't reachable. |
|
|
134
|
+
| `values` | `object` | Shared values bag. |
|
|
135
|
+
| `setValues(patch)` | `(object \| fn) => void` | Object patches shallow-merge; updater functions replace. |
|
|
136
|
+
|
|
137
|
+
### Gating rules
|
|
138
|
+
|
|
139
|
+
- **Back** is never gated; revisiting a completed step keeps its success marker.
|
|
140
|
+
- **Next/Finish** runs the active step's `validate(ctx)`. A non-empty string blocks and shows an error Alert; `true`/`undefined`/`false` pass. Validation is sync-only.
|
|
141
|
+
- A future step is clickable only when every step before it is completed or `optional` (linear), or when `allowJumpAhead` is on.
|
|
142
|
+
- Completion is recorded per step id and is not invalidated by later edits — re-validate in `onComplete` if steps can be undone retroactively.
|
|
143
|
+
|
|
144
|
+
## `<OnboardingChecklist>` props
|
|
145
|
+
|
|
146
|
+
| Prop | Type | Default | Notes |
|
|
147
|
+
|------|------|---------|-------|
|
|
148
|
+
| `items` | `OnboardingChecklistItem[]` | `[]` | `{ id, title, description?, done, action? }`; `action` is `{ label, onClick, variant?, disabled? }`. |
|
|
149
|
+
| `title` | `ReactNode` | — | Card heading (Accordion title in collapsible mode). |
|
|
150
|
+
| `description` | `ReactNode` | — | Microcopy under the heading (non-collapsible mode only). |
|
|
151
|
+
| `progress` | `boolean` | `true` | Show the `ProgressBar` headline (`value=done`, `maxValue=total`). |
|
|
152
|
+
| `onItemClick` | `(item) => void` | — | Makes item titles clickable Links. |
|
|
153
|
+
| `collapsible` | `boolean` | `false` | Wrap in an Accordion; defaults open while items remain incomplete. |
|
|
154
|
+
| `defaultOpen` | `boolean` | data-driven | Override the collapsible default-open behavior. |
|
|
155
|
+
| `showCompletedActions` | `boolean` | `false` | Keep action buttons on done rows. |
|
|
156
|
+
| `labels` | `{ progress }` | — | `progress(done, total)` formats the bar's value description. |
|
|
157
|
+
|
|
158
|
+
Completion is data-driven: the host marks items `done`. The checklist renders state, it never owns it.
|