hs-uix 2.1.0 → 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.
Files changed (49) hide show
  1. package/README.md +3 -1
  2. package/common-components.d.ts +319 -68
  3. package/dist/calendar.js +397 -119
  4. package/dist/calendar.mjs +399 -119
  5. package/dist/common-components.js +3546 -88
  6. package/dist/common-components.mjs +3530 -84
  7. package/dist/datatable.js +108 -18
  8. package/dist/datatable.mjs +108 -18
  9. package/dist/experimental.js +2876 -0
  10. package/dist/experimental.mjs +2883 -0
  11. package/dist/feed.js +267 -38
  12. package/dist/feed.mjs +260 -37
  13. package/dist/filter.js +1379 -0
  14. package/dist/filter.mjs +1334 -0
  15. package/dist/form.js +222 -26
  16. package/dist/form.mjs +227 -27
  17. package/dist/index.js +3255 -353
  18. package/dist/index.mjs +3199 -344
  19. package/dist/kanban.js +282 -62
  20. package/dist/kanban.mjs +273 -61
  21. package/dist/safe.js +9207 -0
  22. package/dist/safe.mjs +9298 -0
  23. package/dist/utils.js +491 -75
  24. package/dist/utils.mjs +491 -75
  25. package/experimental.d.ts +1 -0
  26. package/filter.d.ts +1 -0
  27. package/index.d.ts +45 -3
  28. package/package.json +19 -1
  29. package/safe.d.ts +1 -0
  30. package/src/calendar/README.md +76 -5
  31. package/src/calendar/index.d.ts +108 -1
  32. package/src/common-components/README.md +140 -1
  33. package/src/datatable/README.md +0 -2
  34. package/src/experimental/README.md +126 -0
  35. package/src/experimental/index.d.ts +346 -0
  36. package/src/feed/README.md +69 -0
  37. package/src/feed/index.d.ts +103 -0
  38. package/src/filter/README.md +148 -0
  39. package/src/filter/index.d.ts +221 -0
  40. package/src/form/README.md +132 -4
  41. package/src/form/index.d.ts +82 -1
  42. package/src/kanban/README.md +119 -6
  43. package/src/kanban/index.d.ts +153 -2
  44. package/src/safe/README.md +108 -0
  45. package/src/safe/index.d.ts +158 -0
  46. package/src/utils/README.md +39 -0
  47. package/src/wizard/README.md +158 -0
  48. package/src/wizard/index.d.ts +138 -0
  49. package/utils.d.ts +17 -0
@@ -0,0 +1,126 @@
1
+ # experimental (`hs-uix/experimental`)
2
+
3
+ APIs that are shipping for real-world feedback but whose surface may still
4
+ change in a minor release. Graduating components move to their stable subpath
5
+ with a re-export left behind for one minor version.
6
+
7
+ Currently here:
8
+
9
+ - **`Wizard` / `OnboardingChecklist`** — re-exported from `hs-uix/wizard`.
10
+ - **`ExperimentalDataTable`** — DataTable with the in-progress row-expansion API.
11
+ - **`Skeleton`** (+ `SkeletonText`, `SkeletonBox`, `SkeletonCircle`,
12
+ `SkeletonTable`, `makeSkeletonDataUri`, `SKELETON_WIDTH_TOKENS`) — loading
13
+ placeholders, documented below.
14
+
15
+ ---
16
+
17
+ ## Skeleton
18
+
19
+ Content placeholders for loading states. `Spinner` says "something is
20
+ happening"; `Skeleton` holds the **shape** of the incoming content so the
21
+ layout doesn't jump when data lands. There is no native skeleton component and
22
+ CSS is forbidden, so each placeholder is a gray rounded-rect SVG data URI
23
+ rendered through the native `<Image>` — the same technique as `StyledText` and
24
+ `AvatarStack`.
25
+
26
+ One component, two modes.
27
+
28
+ ### Wrapper mode (auto) — the default way to use it
29
+
30
+ Wrap any surface, pass `loading`, done. While loading, `Skeleton` never
31
+ renders its children — it reads each child element's component name and props
32
+ and draws a placeholder with the same footprint; when `loading` flips false
33
+ the children render untouched.
34
+
35
+ ```jsx
36
+ import { Skeleton } from "hs-uix/experimental";
37
+
38
+ <Skeleton loading={isLoading}>
39
+ <DataTable data={rows} columns={COLUMNS} pageSize={10} />
40
+ </Skeleton>
41
+ // while loading → a 10-row table skeleton with COLUMNS.length columns
42
+ ```
43
+
44
+ Recognized children and what sizes their placeholder:
45
+
46
+ | Child | Shape | Sized from |
47
+ |---|---|---|
48
+ | `DataTable` / `CrmDataTable` | table rows | `columns.length` / `properties.length`, `pageSize` |
49
+ | native `Table` | table rows | 4 × 3 default |
50
+ | `Kanban` / `CrmKanban` | board columns of cards | `stages.length` |
51
+ | `Feed` | avatar + text rows | `pageSize` |
52
+ | `FormBuilder` / native `Form` | label + input rows | `fields.length` |
53
+ | `KeyValueList` / `DescriptionList` | label/value pairs | `items.length` / child count |
54
+ | native `Statistics` | metric blocks | child count |
55
+ | native field inputs (`Select`, `Input`, `DateInput`, …) | one label + input row | — |
56
+ | `Button` / `Tag` / `StatusTag` | small pill | — |
57
+ | `BarChart` / `LineChart` / `Calendar` / `Tile` / `Card` / `Image` / … | block | chart/image height |
58
+ | `Text` / `Heading` / `List` | text lines | child count |
59
+ | anything else | text block | `lines` (default 3) |
60
+
61
+ Native components work because they are remote *string* types (`"Table"`,
62
+ `"Select"`) — the element type is the name. hs-uix components carry explicit
63
+ `displayName`s, so matching survives minified bundles.
64
+
65
+ Overrides, when the inference isn't what you want:
66
+
67
+ ```jsx
68
+ // Pick the shape yourself…
69
+ <Skeleton loading={isLoading} variant="board" columns={4}>…</Skeleton>
70
+
71
+ // …or supply your own static blocks
72
+ <Skeleton
73
+ loading={isLoading}
74
+ skeleton={
75
+ <Flex direction="column" gap="md">
76
+ <SkeletonBox height={48} />
77
+ <SkeletonText lines={4} />
78
+ </Flex>
79
+ }
80
+ >
81
+ <MyCustomPanel … />
82
+ </Skeleton>
83
+ ```
84
+
85
+ `variant` replaces the inferred shape (`"table" | "board" | "list" | "form" |
86
+ "keyvalue" | "stats" | "input" | "chip" | "block" | "text" | "box" |
87
+ "circle"`); `rows` / `columns` / `lines` / `height` refine it. Multiple
88
+ children each get their own skeleton, stacked in a column.
89
+
90
+ ### Static mode — the building blocks
91
+
92
+ Without children, `Skeleton` is the placeholder primitive itself, and the
93
+ composite shapes work standalone too:
94
+
95
+ ```jsx
96
+ import {
97
+ Skeleton,
98
+ SkeletonText,
99
+ SkeletonBox,
100
+ SkeletonCircle,
101
+ SkeletonTable,
102
+ } from "hs-uix/experimental";
103
+
104
+ {loading ? <SkeletonText lines={3} width="md" /> : <Text>{description}</Text>}
105
+
106
+ <Flex direction="row" gap="sm" align="center">
107
+ <SkeletonCircle size={32} />
108
+ <SkeletonText lines={2} width="sm" height={10} gap={6} />
109
+ </Flex>
110
+
111
+ <SkeletonTable rows={5} columns={4} />
112
+ <Skeleton variant="board" columns={3} /> {/* composite variants work statically */}
113
+ ```
114
+
115
+ | Prop | Type | Default | Notes |
116
+ | ---- | ---- | ------- | ----- |
117
+ | `loading` | boolean | `false` | Wrapper mode: gate for `children`. |
118
+ | `skeleton` | node | — | Wrapper mode: your own placeholder; skips inference. |
119
+ | `variant` | shape | `"text"` | Static shape, or inference override in wrapper mode. |
120
+ | `width` | px \| `"sm"`\|`"md"`\|`"lg"` | varies | Tokens = 120 / 240 / 360 (`SKELETON_WIDTH_TOKENS`). |
121
+ | `height` | px | varies | Line height (text), block height (box), diameter (circle). |
122
+ | `lines` / `rows` / `columns` | number | varies | Sizing for text / composite shapes. |
123
+ | `lastLineWidth`, `gap`, `radius`, `columnGap`, `fill`, `alt` | — | — | Primitive styling (see JSDoc). |
124
+
125
+ `makeSkeletonDataUri(opts)` returns `{ src, width, height }` for composing
126
+ placeholders into larger SVGs.
@@ -0,0 +1,346 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { DataTableProps } from "../datatable/index";
3
+ import type {
4
+ ActiveFilterChipsProps,
5
+ AutoStatusTagProps,
6
+ AutoTagProps,
7
+ AvatarStackProps,
8
+ CollectionCountProps,
9
+ CollectionFilterControlProps,
10
+ CollectionSortSelectProps,
11
+ CollectionToolbarProps,
12
+ CrmLookupSelectProps,
13
+ CrmRecordPickerCreateOptionRules,
14
+ CrmRecordPickerId,
15
+ CrmRecordPickerOption,
16
+ CrmRecordPickerOptionConfig,
17
+ CrmRecordPickerProps,
18
+ CrmRecordPickerRecord,
19
+ CrmRecordPickerSelection,
20
+ CrmRecordPickerValue,
21
+ FormatCollectionCountParams,
22
+ KeyValueListProps,
23
+ SectionHeaderProps,
24
+ StyledTextFormat,
25
+ StyledTextProps,
26
+ } from "../../common-components";
27
+
28
+ export * from "../wizard/index";
29
+
30
+ export interface ExperimentalDataTableRowExpansionProps<
31
+ Row = Record<string, unknown>,
32
+ Id = string | number
33
+ > {
34
+ /** Enables expandable detail rows. Content renders in a full-span row directly under the data row. */
35
+ renderExpandedRow?: (row: Row) => ReactNode;
36
+ /** Controlled expansion state — array of expanded row IDs. */
37
+ expandedRowIds?: Id[];
38
+ /** Uncontrolled initial expansion state. */
39
+ defaultExpandedRowIds?: Id[];
40
+ /** Called with the next expanded id array on every toggle. */
41
+ onExpandedRowsChange?: (expandedRowIds: Id[]) => void;
42
+ /** "icon" adds a chevron column; "row" toggles through cell content links. */
43
+ expandOn?: "icon" | "row";
44
+ /** Accordion mode — expanding a row collapses the others. */
45
+ expandSingle?: boolean;
46
+ }
47
+
48
+ export type ExperimentalDataTableProps<
49
+ Row = Record<string, unknown>,
50
+ Id = string | number
51
+ > = DataTableProps<Row, Id> & ExperimentalDataTableRowExpansionProps<Row, Id>;
52
+
53
+ export declare function DataTable<
54
+ Row = Record<string, unknown>,
55
+ Id = string | number
56
+ >(props: ExperimentalDataTableProps<Row, Id>): ReactElement | null;
57
+
58
+ export { DataTable as ExperimentalDataTable };
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Skeleton loading placeholders (experimental)
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export type SkeletonShape =
65
+ | "table"
66
+ | "board"
67
+ | "list"
68
+ | "form"
69
+ | "keyvalue"
70
+ | "stats"
71
+ | "input"
72
+ | "chip"
73
+ | "block";
74
+
75
+ export type SkeletonVariant = "text" | "box" | "circle" | SkeletonShape;
76
+
77
+ /** Pixel number or a width token: sm = 120, md = 240, lg = 360. */
78
+ export type SkeletonWidth = number | "sm" | "md" | "lg";
79
+
80
+ export interface SkeletonDataUriOptions {
81
+ variant?: "text" | "box" | "circle";
82
+ width?: SkeletonWidth;
83
+ /** Per-line height for "text" (default 12), block height for "box" (default 96), diameter for "circle" (default 40). */
84
+ height?: number;
85
+ /** "text" only: number of stacked lines. Default 1. */
86
+ lines?: number;
87
+ /** "text" only: final-line width when lines > 1. (0, 1] = fraction of width; > 1 = px; tokens allowed. Default 0.6. */
88
+ lastLineWidth?: SkeletonWidth;
89
+ /** "text" only: px between lines. Default 8. */
90
+ gap?: number;
91
+ /** Corner radius px (ignored for "circle"). Default 3. */
92
+ radius?: number;
93
+ /** "box" only: split the block into N equal cells. Default 1. */
94
+ columns?: number;
95
+ /** "box" only: px between cells. Default 16. */
96
+ columnGap?: number;
97
+ /** Placeholder color. Default SKELETON_FILL. */
98
+ fill?: string;
99
+ }
100
+
101
+ export interface SkeletonDataUriResult {
102
+ src: string;
103
+ width: number;
104
+ height: number;
105
+ }
106
+
107
+ export interface SkeletonProps extends Omit<SkeletonDataUriOptions, "variant"> {
108
+ /** Static shape, or the override for the inferred shape in wrapper mode. */
109
+ variant?: SkeletonVariant;
110
+ /** Wrapper mode: while true, children are replaced by shape-matched placeholders. Default false. */
111
+ loading?: boolean;
112
+ /** Wrapper mode: your own placeholder node(s); skips auto-inference. */
113
+ skeleton?: ReactNode;
114
+ /** Wrapper mode: content gated by `loading`. */
115
+ children?: ReactNode;
116
+ /** Composite shapes: row count. */
117
+ rows?: number;
118
+ /** Accessible label on the underlying <Image>. Default "Loading". */
119
+ alt?: string;
120
+ [imageProp: string]: unknown;
121
+ }
122
+
123
+ export interface SkeletonTextProps extends Omit<SkeletonProps, "variant"> {
124
+ /** Default 3. */
125
+ lines?: number;
126
+ /** Default "md" (240). */
127
+ width?: SkeletonWidth;
128
+ }
129
+
130
+ export interface SkeletonBoxProps extends Omit<SkeletonProps, "variant"> {
131
+ /** Default "md" (240). */
132
+ width?: SkeletonWidth;
133
+ /** Default 96. */
134
+ height?: number;
135
+ }
136
+
137
+ export interface SkeletonCircleProps
138
+ extends Omit<SkeletonProps, "variant" | "width" | "height"> {
139
+ /** Diameter px. Default 40. */
140
+ size?: number;
141
+ }
142
+
143
+ export interface SkeletonTableProps {
144
+ /** Default 4. */
145
+ rows?: number;
146
+ /** Cells per row. Default 3. */
147
+ columns?: number;
148
+ /** Total row width: px or token. Default "lg" (360). */
149
+ width?: SkeletonWidth;
150
+ /** Px height of each row's cells. Default 16. */
151
+ rowHeight?: number;
152
+ /** Px between cells within a row. Default 16. */
153
+ columnGap?: number;
154
+ /** Flex gap token between rows. Default "sm". */
155
+ gap?: string;
156
+ /** Cell corner radius px. Default 3. */
157
+ radius?: number;
158
+ fill?: string;
159
+ /** Accessible label applied to each row image. Default "Loading table". */
160
+ alt?: string;
161
+ [flexProp: string]: unknown;
162
+ }
163
+
164
+ export type SpinnerName =
165
+ | "braille"
166
+ | "braillewave"
167
+ | "dna"
168
+ | "scan"
169
+ | "rain"
170
+ | "scanline"
171
+ | "pulse"
172
+ | "snake"
173
+ | "sparkle"
174
+ | "cascade"
175
+ | "columns"
176
+ | "orbit"
177
+ | "breathe"
178
+ | "waverows"
179
+ | "checkerboard"
180
+ | "helix"
181
+ | "fillsweep"
182
+ | "diagswipe";
183
+
184
+ export interface SpinnerPreset {
185
+ frames: readonly string[];
186
+ interval: number;
187
+ }
188
+
189
+ export interface SpinnerProps {
190
+ name?: SpinnerName | string;
191
+ frames?: readonly string[];
192
+ interval?: number;
193
+ label?: ReactNode;
194
+ children?: ReactNode;
195
+ paused?: boolean;
196
+ gap?: string;
197
+ variant?: "bodytext" | "microcopy";
198
+ format?: StyledTextFormat;
199
+ inline?: boolean;
200
+ truncate?: boolean | { tooltipText?: string };
201
+ }
202
+
203
+ export type IconSize =
204
+ | number
205
+ | "xs"
206
+ | "extra-small"
207
+ | "sm"
208
+ | "small"
209
+ | "md"
210
+ | "med"
211
+ | "medium"
212
+ | "lg"
213
+ | "large"
214
+ | "xl"
215
+ | "extra-large";
216
+
217
+ export interface IconPathObject {
218
+ d: string;
219
+ fill?: string;
220
+ fillRule?: "nonzero" | "evenodd";
221
+ }
222
+
223
+ export type IconPath = string | IconPathObject;
224
+
225
+ export interface IconEntry {
226
+ /** Defaults to "0 0 24 24" when omitted. */
227
+ viewBox?: string;
228
+ paths: IconPath[];
229
+ /** Optional transform applied to all paths (e.g. a mirror/rotation). */
230
+ transform?: string;
231
+ }
232
+
233
+ export interface IconProps {
234
+ /** A registered glyph name (native or custom). Unknown names render nothing. */
235
+ name: string;
236
+ /** A semantic token ("inherit" | "alert" | "warning" | "success") or any CSS color. */
237
+ color?: string;
238
+ size?: IconSize;
239
+ /** Accessible label for screen readers. */
240
+ screenReaderText?: string;
241
+ /** Passed through to native HubSpot Icon when possible; fallback Image also receives it. */
242
+ onClick?: (...args: unknown[]) => void;
243
+ /** Passed through to native HubSpot Icon when possible; fallback Image also receives it. */
244
+ href?: string | { url: string; external?: boolean };
245
+ }
246
+
247
+ export interface IconDataUriResult {
248
+ src: string;
249
+ width: number;
250
+ height: number;
251
+ }
252
+
253
+ export interface IconDataUriOptions {
254
+ size?: IconSize;
255
+ color?: string;
256
+ }
257
+
258
+ export declare function Icon(props: IconProps): ReactNode;
259
+ /** Custom glyph names registered in this library (excludes native names). */
260
+ export declare const ICON_NAMES: string[];
261
+ /** The custom glyph registry, keyed by icon name. */
262
+ export declare const ICONS: Record<string, IconEntry>;
263
+ /** The native `@hubspot/ui-extensions` `<Icon>` name whitelist, sorted. */
264
+ export declare const NATIVE_ICON_NAME_LIST: string[];
265
+ /** Build an SVG data URI from a registered name or an inline entry. Null for unknown names. */
266
+ export declare function makeIconDataUri(
267
+ nameOrEntry: string | IconEntry,
268
+ options?: IconDataUriOptions
269
+ ): IconDataUriResult | null;
270
+ /** Parse a raw `<svg>` string into a registry entry (drops mask/defs, keeps per-path fills). */
271
+ export declare function svgToIconEntry(raw: string): IconEntry;
272
+
273
+ export declare function AutoTag(props: AutoTagProps): ReactNode;
274
+ export declare function AutoStatusTag(props: AutoStatusTagProps): ReactNode;
275
+ export declare function ActiveFilterChips(props: ActiveFilterChipsProps): ReactNode;
276
+ export declare function CollectionCount(props: CollectionCountProps): ReactNode;
277
+ export declare function formatCollectionCount(params: FormatCollectionCountParams): ReactNode;
278
+ export declare function CollectionFilterControl(props: CollectionFilterControlProps): ReactNode;
279
+ export declare function CollectionSortSelect(props: CollectionSortSelectProps): ReactNode;
280
+ export declare function CollectionToolbar(props: CollectionToolbarProps): ReactNode;
281
+ export declare function SectionHeader(props: SectionHeaderProps): ReactNode;
282
+ export declare function KeyValueList(props: KeyValueListProps): ReactNode;
283
+ export declare function AvatarStack(props: AvatarStackProps): ReactNode;
284
+ export declare function CrmLookupSelect(props: CrmLookupSelectProps): ReactNode;
285
+ export declare function CrmRecordPicker(props: CrmRecordPickerProps): ReactNode;
286
+
287
+ /** Sentinel option value for CrmRecordPicker's inline "Create" option. */
288
+ export declare const CREATE_OPTION_VALUE: "__create__";
289
+ /** True when the value looks like a record object (vs a scalar id). */
290
+ export declare function isRecordLike(value: unknown): boolean;
291
+ /** Extract a record's id (objectId | id | hs_object_id | properties.hs_object_id). */
292
+ export declare function getRecordId(record: unknown): CrmRecordPickerId | undefined;
293
+ /** Normalize a picker value (ids and/or records, scalar or array) to { ids, records }. */
294
+ export declare function normalizeRecordSelection(
295
+ value: CrmRecordPickerValue | null | undefined
296
+ ): CrmRecordPickerSelection;
297
+ /** Map a record to a { label, value, description? } option. */
298
+ export declare function recordToPickerOption(
299
+ record: CrmRecordPickerRecord,
300
+ config?: CrmRecordPickerOptionConfig
301
+ ): CrmRecordPickerOption;
302
+ /** Merge search-page options with selected options so selections stay visible. */
303
+ export declare function mergePickerOptions(
304
+ options: CrmRecordPickerOption[],
305
+ selectedOptions?: CrmRecordPickerOption | CrmRecordPickerOption[] | null
306
+ ): CrmRecordPickerOption[];
307
+ /** Trim a selection to its first `max` ids (rejects picks beyond the cap). */
308
+ export declare function enforceSelectionMax(
309
+ ids: CrmRecordPickerId[] | CrmRecordPickerId | null | undefined,
310
+ max?: number
311
+ ): CrmRecordPickerId[];
312
+ /** Injection rules for the inline create option. */
313
+ export declare function shouldShowCreateOption(
314
+ rules?: CrmRecordPickerCreateOptionRules
315
+ ): boolean;
316
+ /** Build the `Create "<term>"` option for a search term. */
317
+ export declare function makeCreateOption(
318
+ term: string,
319
+ label?: string | ((term: string) => string)
320
+ ): CrmRecordPickerOption;
321
+ /** Split an onChange payload into real ids + whether the create sentinel was chosen. */
322
+ export declare function splitCreateSelection(
323
+ next: CrmRecordPickerId[] | CrmRecordPickerId | null | undefined
324
+ ): { ids: CrmRecordPickerId[]; create: boolean };
325
+ /** Map ids back to records via a Map/object registry; unknown ids become { objectId } stubs. */
326
+ export declare function mapIdsToRecords(
327
+ ids: CrmRecordPickerId[] | CrmRecordPickerId | null | undefined,
328
+ recordsById?: Map<CrmRecordPickerId, CrmRecordPickerRecord> | Record<string, CrmRecordPickerRecord>
329
+ ): CrmRecordPickerRecord[];
330
+ /** Upsert records into a list deduped by record id (later wins). */
331
+ export declare function upsertRecords(
332
+ records: CrmRecordPickerRecord[] | null | undefined,
333
+ additions: CrmRecordPickerRecord | CrmRecordPickerRecord[] | null | undefined
334
+ ): CrmRecordPickerRecord[];
335
+ export declare function StyledText(props: StyledTextProps): ReactNode;
336
+ export declare function Skeleton(props: SkeletonProps): ReactNode;
337
+ export declare function SkeletonText(props: SkeletonTextProps): ReactNode;
338
+ export declare function SkeletonBox(props: SkeletonBoxProps): ReactNode;
339
+ export declare function SkeletonCircle(props: SkeletonCircleProps): ReactNode;
340
+ export declare function SkeletonTable(props: SkeletonTableProps): ReactNode;
341
+ /** Build the SVG data URI + intrinsic dimensions for a skeleton placeholder. */
342
+ export declare function makeSkeletonDataUri(
343
+ options?: SkeletonDataUriOptions
344
+ ): SkeletonDataUriResult;
345
+ /** Width tokens accepted anywhere a skeleton takes a `width` (sm/md/lg → px). */
346
+ export declare const SKELETON_WIDTH_TOKENS: { sm: number; md: number; lg: number };
@@ -75,6 +75,8 @@ If the primary mental model is “what happened, and when?”, use Feed. If the
75
75
  - Built-in item count (`5 of 12 events`) with custom `recordLabel` / `itemCountText`
76
76
  - View-more pagination (`pageSize`) plus server/external load-more (`hasMore`, `onLoadMore`)
77
77
  - Client-side or server-side mode (`serverSide`) with unified `onParamsChange`
78
+ - Real-time append: `newItemsBehavior="pill"` buffers live arrivals behind a "Show N new items" pill; `highlightNew` marks fresh items with an info `Tag`
79
+ - Per-type display presets (`typePresets` + `DEFAULT_FEED_TYPE_PRESETS`) covering HubSpot's standard activity types with verified native icon names
78
80
  - Built-in loading, error, and empty states with render overrides
79
81
  - Tile-backed outer/item containers and divider mode
80
82
  - Render escape hatches for item, toolbar, and individual item regions
@@ -183,6 +185,64 @@ Feed shows `pageSize` items initially and a transparent “View more” button w
183
185
  />
184
186
  ```
185
187
 
188
+ ## Real-time append
189
+
190
+ Live feeds prepend items while the user is reading. By default (`newItemsBehavior="immediate"`) new items just render. Set `newItemsBehavior="pill"` to hold items that arrive **newer than the previously-newest visible timestamp** in a buffer and show a centered "Show N new items" pill instead — clicking it flushes the buffer into the list:
191
+
192
+ ```jsx
193
+ <Feed
194
+ items={liveActivity} // parent prepends as events stream in
195
+ newItemsBehavior="pill"
196
+ onNewItemsFlush={(flushed) => console.log(`released ${flushed.length} items`)}
197
+ highlightNew={30000} // flushed items carry a "New" tag for 30s
198
+ sortOptions={[{ value: "newest", label: "Newest first", field: "timestamp", direction: "desc" }]}
199
+ defaultSort="newest"
200
+ />
201
+ ```
202
+
203
+ Behavior details (all decided by the pure, exhaustively-tested `partitionNewItems` / `flushBuffer` kernel in `feedLiveBuffer.js`):
204
+
205
+ - The **first load never buffers** — there is no previous watermark.
206
+ - Only items **strictly newer** than the watermark buffer; equal timestamps and unparseable/missing timestamps stay visible.
207
+ - **Updates to already-visible items (matched by key) never buffer**, even if their timestamp moved forward — a visible row must not vanish into the pill. Give live items stable `id`s; index-based fallback keys defeat update detection.
208
+ - The pill renders even when the visible list is empty (the buffer may hold the only items), and the item count reflects visible items only.
209
+ - `highlightNew={ms}` marks flushed (pill) or freshly-prepended (immediate) items with an info `Tag` ("New") for that window, then clears automatically. `false` (default) disables it. Custom `renderItem` rows bypass the marker.
210
+ - Labels: `labels.newItems` (string or `(count) => string`) and `labels.newItemTag`.
211
+
212
+ `partitionNewItems(prevNewestTs, items, getTs, { knownIds, getId })`, `flushBuffer(visible, buffered, getTs)`, and `toTimestampMs(value)` are exported for server adapters and tests.
213
+
214
+ ## Per-type presets
215
+
216
+ Stop repeating `iconName: "calling"` on every call row. `typePresets` maps `item.type` machine values to display defaults, merged **under** item-level values (the item always wins):
217
+
218
+ ```jsx
219
+ import { Feed, DEFAULT_FEED_TYPE_PRESETS } from "hs-uix/feed";
220
+
221
+ // Built-in HubSpot activity-type presets:
222
+ <Feed items={engagements} typePresets /> // or typePresets={DEFAULT_FEED_TYPE_PRESETS}
223
+
224
+ // Extend or override:
225
+ <Feed
226
+ items={engagements}
227
+ typePresets={{
228
+ ...DEFAULT_FEED_TYPE_PRESETS,
229
+ call: { ...DEFAULT_FEED_TYPE_PRESETS.call, statusVariant: "info" },
230
+ deploy: { icon: "rotate", label: "Deployment" },
231
+ }}
232
+ />
233
+ ```
234
+
235
+ A preset fills these item keys when missing: `icon` → `iconName`, `color` → `iconColor`, `label` → `typeLabel`, `statusVariant` → `statusVariant`. Lookup by `type` is case-insensitive and snake_case-normalized, so API values like `"EMAIL"` or `"Postal Mail"` resolve.
236
+
237
+ `DEFAULT_FEED_TYPE_PRESETS` covers `call`, `email`, `incoming_email`, `forwarded_email`, `meeting`, `note`, `task`, `sms`, `whatsapp`, `linkedin_message`, `postal_mail`, and `conversation` — every icon name is verified against the native HubSpot icon whitelist by a unit test (invalid native icon names render **nothing**).
238
+
239
+ | Preset key | Description |
240
+ |---|---|
241
+ | `icon` | Native HubSpot icon name (verify against the whitelist — invalid names render nothing) |
242
+ | `color` | Icon color: native enum (`alert`/`warning`/`success`/`inherit`) or any CSS color (SVG fallback) |
243
+ | `label` | Display label used as `typeLabel` |
244
+ | `statusVariant` | `StatusTag` variant fallback: `default`, `info`, `success`, `warning`, `danger` |
245
+
186
246
  ## Server-side mode
187
247
 
188
248
  Use `serverSide` when the parent/API owns filtering, sorting, searching, and pagination. Feed renders the toolbar and calls back with params, but does not mutate `items`.
@@ -223,4 +283,13 @@ Use `serverSide` when the parent/API owns filtering, sorting, searching, and pag
223
283
 
224
284
  ## Props
225
285
 
286
+ Live-append and preset props:
287
+
288
+ | Prop | Type | Default | Notes |
289
+ |---|---|---|---|
290
+ | `newItemsBehavior` | `"immediate" \| "pill"` | `"immediate"` | `"pill"` buffers strictly-newer arrivals behind a "Show N new items" Button (variant `secondary`, centered) |
291
+ | `onNewItemsFlush` | `(items) => void` | — | Fired with the released items when the pill is clicked |
292
+ | `highlightNew` | `number \| false` | `false` | Window (ms) during which flushed/prepended items carry an info `Tag` "New" marker; initial load is never marked |
293
+ | `typePresets` | `object \| true` | — | `{ [type]: { icon, color, label, statusVariant } }` merged under item values; `true` uses `DEFAULT_FEED_TYPE_PRESETS` |
294
+
226
295
  See `feed.d.ts` for the full typed API.