hs-uix 1.6.5 → 2.0.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.
@@ -0,0 +1,246 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+
3
+ export type DataTableSortDirection = "none" | "ascending" | "descending";
4
+ export type DataTableWidth = "min" | "max" | "auto";
5
+ export type DataTableColumnWidth = DataTableWidth | number;
6
+ export type DataTableEditMode = "discrete" | "inline";
7
+ export type DataTableFilterType = "select" | "multiselect" | "dateRange";
8
+
9
+ export interface DataTableDateValue {
10
+ year: number;
11
+ month: number;
12
+ date: number;
13
+ }
14
+
15
+ export interface DataTableTimeValue {
16
+ hours: number;
17
+ minutes: number;
18
+ }
19
+
20
+ export interface DataTableDateRangeValue {
21
+ from: DataTableDateValue | null;
22
+ to: DataTableDateValue | null;
23
+ }
24
+
25
+ export interface DataTableOption<T = unknown> {
26
+ label: string;
27
+ value: T;
28
+ }
29
+
30
+ export interface DataTableFilterConfig<Row = Record<string, unknown>> {
31
+ name: string;
32
+ type?: DataTableFilterType;
33
+ placeholder?: string;
34
+ options?: DataTableOption[];
35
+ chipLabel?: string;
36
+ filterFn?: (row: Row, value: unknown) => boolean;
37
+ }
38
+
39
+ export type DataTableEditType =
40
+ | "text"
41
+ | "textarea"
42
+ | "number"
43
+ | "currency"
44
+ | "stepper"
45
+ | "select"
46
+ | "multiselect"
47
+ | "date"
48
+ | "time"
49
+ | "datetime"
50
+ | "toggle"
51
+ | "checkbox";
52
+
53
+ export interface DataTableColumn<Row = Record<string, unknown>> {
54
+ field: string;
55
+ label: ReactNode;
56
+ description?: ReactNode;
57
+ sortable?: boolean;
58
+ sortOrder?: unknown[];
59
+ sortComparator?: (
60
+ aValue: unknown,
61
+ bValue: unknown,
62
+ rowA: Row,
63
+ rowB: Row
64
+ ) => number;
65
+ width?: DataTableColumnWidth;
66
+ cellWidth?: DataTableWidth;
67
+ align?: "left" | "center" | "right";
68
+ truncate?: true | number | { maxLength?: number };
69
+ editable?: boolean;
70
+ editType?: DataTableEditType;
71
+ editOptions?: DataTableOption[];
72
+ editValidate?: (value: unknown, row: Row) => true | string | undefined | null;
73
+ editProps?: Record<string, unknown>;
74
+ renderCell?: (value: unknown, row: Row) => ReactNode;
75
+ footer?: ReactNode | ((rows: Row[]) => ReactNode);
76
+ }
77
+
78
+ export interface DataTableSelectionAction<Id = string | number> {
79
+ label: string;
80
+ icon?: string;
81
+ variant?: string;
82
+ onClick: (selectedIds: Id[]) => void;
83
+ }
84
+
85
+ export interface DataTableRowAction<Row = Record<string, unknown>> {
86
+ label?: string;
87
+ icon?: string;
88
+ variant?: string;
89
+ onClick: (row: Row) => void;
90
+ }
91
+
92
+ export interface DataTableGroupBy<Row = Record<string, unknown>> {
93
+ field: string;
94
+ label?: (value: unknown, rows: Row[]) => ReactNode;
95
+ sort?: "asc" | "desc" | ((a: string, b: string) => number);
96
+ defaultExpanded?: boolean;
97
+ aggregations?: Record<string, (rows: Row[], groupKey: string) => ReactNode>;
98
+ groupValues?: Record<string, Record<string, ReactNode>>;
99
+ }
100
+
101
+ export interface DataTableSortObject {
102
+ field: string;
103
+ direction: DataTableSortDirection;
104
+ }
105
+
106
+ export interface DataTableParams {
107
+ search: string;
108
+ filters: Record<string, unknown>;
109
+ sort: { field: string; direction: Exclude<DataTableSortDirection, "none"> } | null;
110
+ page: number;
111
+ }
112
+
113
+ export interface DataTableSelectAllRequestPayload<Id = string | number> {
114
+ selectedIds: Id[];
115
+ pageIds: Id[];
116
+ totalCount: number;
117
+ }
118
+
119
+ export interface DataTableLabels {
120
+ selected?: string | ((count: number, countLabel: string) => string); // Selection bar: "{count} {label} selected"
121
+ selectAll?: string | ((totalCount: number, countLabel: string) => string); // Selection bar: "Select all {count} {label}"
122
+ deselectAll?: string; // Selection bar: "Deselect all"
123
+ filtersButton?: string; // Overflow filters toggle button: "Filters"
124
+ clearAll?: string; // Active filter chips reset: "Clear all"
125
+ dateFrom?: string; // Date range filter start placeholder: "From"
126
+ dateTo?: string; // Date range filter end placeholder: "To"
127
+ loading?: string; // Loading state title: "Loading {pluralLabel}..."
128
+ loadingMessage?: string; // Loading state body message
129
+ errorTitle?: string; // Error state heading: "Something went wrong."
130
+ errorMessage?: string; // Error state body (non-string error): "An error occurred while loading data."
131
+ retryMessage?: string; // Error state body (string error): "Please try again."
132
+ }
133
+
134
+ export interface DataTableSelectionBarRenderContext<Id = string | number> {
135
+ selectedIds: Set<Id>;
136
+ selectedCount: number;
137
+ displayCount: number;
138
+ countLabel: (n: number) => string;
139
+ allSelected: boolean;
140
+ onSelectAll: () => void;
141
+ onDeselectAll: () => void;
142
+ selectionActions: DataTableSelectionAction<Id>[];
143
+ }
144
+
145
+ export interface DataTableEmptyStateRenderContext {
146
+ title: string;
147
+ message: string;
148
+ }
149
+
150
+ export interface DataTableLoadingStateRenderContext {
151
+ label: string;
152
+ }
153
+
154
+ export interface DataTableErrorStateRenderContext {
155
+ error: string | boolean;
156
+ title: string;
157
+ message: string;
158
+ }
159
+
160
+ export interface DataTableProps<Row = Record<string, unknown>, Id = string | number> {
161
+ data: Row[];
162
+ columns: DataTableColumn<Row>[];
163
+ renderRow?: (row: Row) => ReactNode;
164
+ title?: ReactNode;
165
+
166
+ searchFields?: string[];
167
+ searchPlaceholder?: string;
168
+ fuzzySearch?: boolean;
169
+ fuzzyOptions?: Record<string, unknown>;
170
+
171
+ filters?: DataTableFilterConfig<Row>[];
172
+ showFilterBadges?: boolean; // Show active filter chips/badges (default true)
173
+ showClearFiltersButton?: boolean; // Show "Clear all" reset button; defaults to showFilterBadges when omitted
174
+
175
+ pageSize?: number;
176
+ maxVisiblePageButtons?: number;
177
+ showButtonLabels?: boolean;
178
+ showFirstLastButtons?: boolean;
179
+
180
+ showRowCount?: boolean;
181
+ rowCountBold?: boolean;
182
+ rowCountText?: (shownOnPage: number, totalMatching: number) => string;
183
+
184
+ bordered?: boolean;
185
+ flush?: boolean;
186
+ scrollable?: boolean;
187
+
188
+ defaultSort?: Record<string, DataTableSortDirection>;
189
+ groupBy?: DataTableGroupBy<Row>;
190
+ footer?: (rows: Row[]) => ReactNode;
191
+ emptyTitle?: string;
192
+ emptyMessage?: string;
193
+
194
+ serverSide?: boolean;
195
+ loading?: boolean;
196
+ error?: string | boolean;
197
+ totalCount?: number;
198
+ page?: number;
199
+ searchValue?: string;
200
+ filterValues?: Record<string, unknown>;
201
+ sort?: Record<string, DataTableSortDirection> | DataTableSortObject;
202
+ searchDebounce?: number;
203
+ resetPageOnChange?: boolean;
204
+ onSearchChange?: (searchTerm: string) => void;
205
+ onFilterChange?: (filterValues: Record<string, unknown>) => void;
206
+ onSortChange?: (field: string, direction: DataTableSortDirection) => void;
207
+ onPageChange?: (page: number) => void;
208
+ onParamsChange?: (params: DataTableParams) => void;
209
+
210
+ selectable?: boolean;
211
+ rowIdField?: string;
212
+ selectedIds?: Id[];
213
+ onSelectionChange?: (selectedIds: Id[]) => void;
214
+ onSelectAllRequest?: (payload: DataTableSelectAllRequestPayload<Id>) => void;
215
+ selectionActions?: DataTableSelectionAction<Id>[];
216
+ selectionResetKey?: unknown;
217
+ resetSelectionOnQueryChange?: boolean;
218
+ recordLabel?: { singular: string; plural: string };
219
+
220
+ rowActions?: DataTableRowAction<Row>[] | ((row: Row) => DataTableRowAction<Row>[]);
221
+ hideRowActionsWhenSelectionActive?: boolean;
222
+
223
+ editMode?: DataTableEditMode;
224
+ editingRowId?: Id;
225
+ onRowEdit?: (row: Row, field: string, newValue: unknown) => void;
226
+ onRowEditInput?: (row: Row, field: string, inputValue: unknown) => void;
227
+ onEditStart?: (row: Row, field: string, currentValue: unknown) => void; // Fires when editing begins on a cell
228
+ onEditCancel?: (row: Row, field: string) => void; // Fires when editing is cancelled without commit
229
+
230
+ autoWidth?: boolean;
231
+
232
+ showSearch?: boolean; // Show/hide the search input (default true)
233
+ showSelectionBar?: boolean; // Show/hide the selection action bar when rows are selected (default true)
234
+ filterInlineLimit?: number; // Max filters shown inline before overflow into "Filters" button (default 2)
235
+
236
+ labels?: DataTableLabels; // Override hardcoded UI strings for i18n
237
+
238
+ renderSelectionBar?: (context: DataTableSelectionBarRenderContext<Id>) => ReactNode; // Replace the default selection action bar
239
+ renderEmptyState?: (context: DataTableEmptyStateRenderContext) => ReactNode; // Replace the default empty state
240
+ renderLoadingState?: (context: DataTableLoadingStateRenderContext) => ReactNode; // Replace the default loading spinner
241
+ renderErrorState?: (context: DataTableErrorStateRenderContext) => ReactNode; // Replace the default error state
242
+ }
243
+
244
+ export declare function DataTable<Row = Record<string, unknown>, Id = string | number>(
245
+ props: DataTableProps<Row, Id>
246
+ ): ReactElement | null;
@@ -0,0 +1,224 @@
1
+ # Feed (hs-uix/feed)
2
+
3
+ Activity feed / timeline component for HubSpot UI Extensions. Feed is the default choice for chronological activity streams, audit logs, recent-events panels, interaction history, and lightweight timelines — the same way `DataTable` is the default for tabular list managers.
4
+
5
+ Feed gives you search, filters, sort, grouping, count text, view-more pagination, loading/empty/error states, declarative item regions, and render escape hatches while staying fully renderable with HubSpot primitives: `Tile`, `Flex`, `Text`, `Icon`, `AvatarStack`, `Tag`, `StatusTag`, `DescriptionList`, `List`, `ButtonRow`, `SearchInput`, `Select`, `MultiSelect`, `EmptyState`, `Alert`, and `LoadingSpinner`.
6
+
7
+ ## Quick Start
8
+
9
+ ```jsx
10
+ import { Feed } from "hs-uix/feed";
11
+
12
+ const activity = [
13
+ {
14
+ id: "1",
15
+ type: "Note",
16
+ typeVariant: "info",
17
+ iconName: "comment",
18
+ title: "Note added",
19
+ actor: { name: "Avery Reed", initials: "AR" },
20
+ timestamp: "2026-04-26T13:41:00Z",
21
+ body: "Customer asked for implementation timeline and pricing details.",
22
+ meta: ["Sales", "High intent"],
23
+ actions: [{ label: "Open", icon: "record", href: { url: recordUrl } }],
24
+ },
25
+ ];
26
+
27
+ <Feed
28
+ title="Activity"
29
+ description="Latest timeline events for this record."
30
+ items={activity}
31
+ searchFields={["title", "body", "type"]}
32
+ filters={[
33
+ {
34
+ name: "type",
35
+ type: "multiselect",
36
+ label: "Activity type",
37
+ options: [
38
+ { label: "Emails", value: "Email" },
39
+ { label: "Calls", value: "Call" },
40
+ { label: "Meetings", value: "Meeting" },
41
+ { label: "Notes", value: "Note" },
42
+ ],
43
+ },
44
+ ]}
45
+ sortOptions={[
46
+ { value: "newest", label: "Newest first", field: "timestamp", direction: "desc" },
47
+ { value: "oldest", label: "Oldest first", field: "timestamp", direction: "asc" },
48
+ ]}
49
+ defaultSort="newest"
50
+ groupByDate
51
+ />
52
+ ```
53
+
54
+ That gives you a searchable, filterable, sorted, date-grouped timeline with a native toolbar and empty/loading/error states.
55
+
56
+ ## When to use Feed vs DataTable vs Kanban
57
+
58
+ | Need | Use |
59
+ |---|---|
60
+ | Chronological history, audit log, recent events, activity timeline | `Feed` |
61
+ | Compare records across columns, edit cells, aggregate totals, export-like list manager | `DataTable` |
62
+ | Records moving through stages/status columns | `Kanban` |
63
+
64
+ If the primary mental model is “what happened, and when?”, use Feed. If the user needs to compare many attributes side-by-side, use DataTable. If the user needs WIP/stage visibility, use Kanban.
65
+
66
+ ## Features
67
+
68
+ - Standard activity item shape (`type`, `typeVariant`, `iconName`, `timestamp`, `actor`, `body`, `meta`, `actions`)
69
+ - Declarative `fields` with Kanban-like placements: `title`, `subtitle`, `meta`, `body`, `footer`
70
+ - Full-text search across configured fields
71
+ - DataTable/Kanban-style toolbar layout with search, inline filters, overflow filters, sort, and count text
72
+ - `select`, `multiselect`, and `dateRange` filters
73
+ - Sort dropdown via `sortOptions` (`field` + `direction` or custom `comparator`)
74
+ - Date grouping (`Today`, `Yesterday`, localized older dates) or custom `groupBy`
75
+ - Built-in item count (`5 of 12 events`) with custom `recordLabel` / `itemCountText`
76
+ - View-more pagination (`pageSize`) plus server/external load-more (`hasMore`, `onLoadMore`)
77
+ - Client-side or server-side mode (`serverSide`) with unified `onParamsChange`
78
+ - Built-in loading, error, and empty states with render overrides
79
+ - Tile-backed outer/item containers and divider mode
80
+ - Render escape hatches for item, toolbar, and individual item regions
81
+
82
+ ## Standard item shape
83
+
84
+ Feed works out of the box with these item keys:
85
+
86
+ | Key | Description |
87
+ |---|---|
88
+ | `id` / `key` | Stable item key |
89
+ | `type` | Activity type label, rendered in a `StatusTag` by default |
90
+ | `typeLabel` | Optional display label when `type` is a machine value |
91
+ | `typeVariant` | `StatusTag` variant: `default`, `info`, `success`, `warning`, `danger` |
92
+ | `iconName` / `icon` | Activity/entity icon. Use verified HubSpot icon names (`email`, `calling`, `appointment`, `comment`, `description`, etc.) |
93
+ | `title` / `subject` | Main item heading |
94
+ | `href` | Optional link wrapping the title |
95
+ | `body` / `description` / `content` / `preview` / `notePreview` | Main text body |
96
+ | `timestamp` / `time` / `date` / `createdAt` / `dateLabel` | Time display and date grouping input |
97
+ | `actor` / `actorName` / `author` | Actor text or `{ name, avatar/avatarUrl/src/initials }` |
98
+ | `avatar` | Avatar source/initials, rendered via `AvatarStack` |
99
+ | `meta` / `metadata` | Inline metadata, rendered via `List variant="inline-divided"` when array |
100
+ | `actions` | ReactNode or action objects rendered through `ButtonRow` |
101
+ | `footer` | Optional footer content |
102
+
103
+ Prefer precomputing `typeVariant` in data when the values are known. That mirrors the Studio knowledge-base derivation pattern and keeps the render deterministic.
104
+
105
+ ## Declarative fields
106
+
107
+ Use `fields` when your rows have custom properties. Placements mirror Kanban's card-field vocabulary.
108
+
109
+ ```jsx
110
+ <Feed
111
+ items={events}
112
+ fields={[
113
+ { field: "subject", placement: "title", href: (row) => row.url },
114
+ { field: "channel", placement: "subtitle" },
115
+ { field: "owner", label: "Owner", placement: "body" },
116
+ { field: "nextStep", label: "Next step", placement: "body" },
117
+ { field: "priority", placement: "meta", type: "tag", variant: "warning" },
118
+ { field: "outcome", placement: "footer", type: "status", variant: "success" },
119
+ ]}
120
+ />
121
+ ```
122
+
123
+ Labeled body fields render in one `DescriptionList`; `type: "tag"` and `type: "status"` render HubSpot `Tag` and `StatusTag`. Keep `label` a string — HubSpot's `DescriptionListItem.label` does not accept nodes.
124
+
125
+ ## Search, filters, and sort
126
+
127
+ The default toolbar intentionally mirrors DataTable/Kanban: left column for Search + quick filters (+ overflow Filters button), right column for Sort + item count.
128
+
129
+ ```jsx
130
+ <Feed
131
+ items={activity}
132
+ searchFields={["title", "body", "type", "actorName"]}
133
+ filters={[
134
+ { name: "type", type: "multiselect", label: "Activity type", options: TYPE_OPTIONS },
135
+ { name: "outcome", type: "select", label: "Outcome", options: OUTCOME_OPTIONS },
136
+ { name: "timestamp", type: "dateRange", label: "Date" },
137
+ ]}
138
+ sortOptions={[
139
+ { value: "newest", label: "Newest first", field: "timestamp", direction: "desc" },
140
+ { value: "oldest", label: "Oldest first", field: "timestamp", direction: "asc" },
141
+ { value: "type", label: "Type A-Z", field: "type", direction: "asc" },
142
+ ]}
143
+ defaultSort="newest"
144
+ filterInlineLimit={2}
145
+ />
146
+ ```
147
+
148
+ Filter config:
149
+
150
+ | Prop | Description |
151
+ |---|---|
152
+ | `name` | Filter key; defaults to reading `item[name]` |
153
+ | `field` | Optional field/accessor when the item key differs from `name` |
154
+ | `type` | `select`, `multiselect`, or `dateRange` |
155
+ | `label` / `placeholder` | Native input label/placeholder |
156
+ | `options` | `{ label, value }[]` for select/multiselect |
157
+ | `filterFn(item, value)` | Custom matching logic |
158
+
159
+ Sort options accept either `field` + `direction` or a custom `comparator(a, b)`.
160
+
161
+ ## Grouping and pagination
162
+
163
+ ```jsx
164
+ <Feed
165
+ items={activity}
166
+ groupByDate
167
+ pageSize={5}
168
+ />
169
+ ```
170
+
171
+ `groupByDate` groups by `Today`, `Yesterday`, then localized dates. Use `groupBy="type"` or `groupBy={(item) => item.bucket}` for arbitrary grouping.
172
+
173
+ Feed shows `pageSize` items initially and a transparent “View more” button when more client-side items are available. For server/external loading, pass `hasMore`, `loadingMore`, and `onLoadMore`:
174
+
175
+ ```jsx
176
+ <Feed
177
+ items={activity}
178
+ hasMore={hasMore}
179
+ loadingMore={loadingMore}
180
+ onLoadMore={loadMore}
181
+ />
182
+ ```
183
+
184
+ ## Server-side mode
185
+
186
+ 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`.
187
+
188
+ ```jsx
189
+ <Feed
190
+ serverSide
191
+ items={pageItems}
192
+ searchValue={params.search}
193
+ filterValues={params.filters}
194
+ sort={params.sort}
195
+ onParamsChange={(next) => fetchActivity(next)}
196
+ filters={filters}
197
+ sortOptions={sortOptions}
198
+ hasMore={hasMore}
199
+ onLoadMore={loadMore}
200
+ />
201
+ ```
202
+
203
+ ## Containers
204
+
205
+ ```jsx
206
+ <Feed container="tile" itemContainer="none" />
207
+ <Feed container="none" itemContainer="tile" />
208
+ <Feed container="card" compact /> // alias for Tile, kept for ergonomic naming
209
+ ```
210
+
211
+ - `container`: `"tile"` (default), `"none"`, or `"card"` (`card` is a Tile-backed alias)
212
+ - `itemContainer`: `"none"` (default), `"tile"`, or `"card"` (`card` is a Tile-backed alias)
213
+ - `showDividers`: dividers between items when `itemContainer="none"`
214
+
215
+ ## Render escape hatches
216
+
217
+ - `renderItem(item, index)` replaces the entire row
218
+ - `renderToolbar(context)` replaces search/filter/sort/count toolbar
219
+ - `renderActor`, `renderTimestamp`, `renderMeta`, `renderActions`, `renderFooter` replace individual regions
220
+ - `renderEmptyState`, `renderLoadingState`, `renderErrorState` mirror DataTable/Kanban state override APIs
221
+
222
+ ## Props
223
+
224
+ See `feed.d.ts` for the full typed API.