hs-uix 2.0.0 → 2.1.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/feed.d.ts CHANGED
@@ -1 +1 @@
1
- export * from "./packages/feed/index";
1
+ export * from "./src/feed/index";
package/form.d.ts CHANGED
@@ -1,28 +1 @@
1
- export {
2
- FormBuilder,
3
- useFormPrefill,
4
- FormBuilderProps,
5
- FormBuilderField,
6
- FormBuilderFieldType,
7
- FormBuilderOption,
8
- FormBuilderDateValue,
9
- FormBuilderTimeValue,
10
- FormBuilderDateTimeValue,
11
- FormBuilderValidationContext,
12
- FormBuilderValidatorResult,
13
- FormBuilderValidator,
14
- FormBuilderDependsOnConfig,
15
- FormBuilderRepeaterProps,
16
- FormBuilderGroupOptions,
17
- FormBuilderLabels,
18
- FormBuilderAlertConfig,
19
- FormBuilderButtonsRenderContext,
20
- FormBuilderSubmitAlign,
21
- FormBuilderLayout,
22
- FormBuilderLayoutEntry,
23
- FormBuilderSection,
24
- FormBuilderSectionContext,
25
- FormBuilderStep,
26
- FormBuilderRef,
27
- FieldTypePlugin,
28
- } from "./packages/form/index";
1
+ export * from "./src/form/index";
package/index.d.ts CHANGED
@@ -1,107 +1,31 @@
1
- export { DataTable } from "./packages/datatable/index";
2
- export type {
3
- DataTableProps,
4
- DataTableColumn,
5
- DataTableFilterConfig,
6
- DataTableFilterType,
7
- DataTableGroupBy,
8
- DataTableSortDirection,
9
- DataTableSortObject,
10
- DataTableParams,
11
- DataTableOption,
12
- DataTableDateValue,
13
- DataTableTimeValue,
14
- DataTableDateRangeValue,
15
- DataTableWidth,
16
- DataTableColumnWidth,
17
- DataTableEditMode,
18
- DataTableEditType,
19
- DataTableSelectionAction,
20
- DataTableRowAction,
21
- DataTableSelectAllRequestPayload,
22
- DataTableLabels,
23
- DataTableSelectionBarRenderContext,
24
- DataTableEmptyStateRenderContext,
25
- DataTableLoadingStateRenderContext,
26
- DataTableErrorStateRenderContext,
27
- } from "./packages/datatable/index";
28
-
29
- export { Kanban, KanbanCardActions } from "./packages/kanban/index";
30
- export type {
31
- KanbanProps,
32
- KanbanStage,
33
- KanbanStageMeta,
34
- KanbanStageVariant,
35
- KanbanStageControlMode,
36
- KanbanCardField,
37
- KanbanCardFieldPlacement,
38
- KanbanCardDensity,
39
- KanbanCardBodyMode,
40
- KanbanCardDividers,
41
- KanbanCardRenderContext,
42
- KanbanFilterConfig,
43
- KanbanFilterType,
44
- KanbanSortOption,
45
- KanbanSelectionAction,
46
- KanbanCardAction,
47
- KanbanCardActionsProps,
48
- KanbanLabels,
49
- KanbanParams,
50
- KanbanStageChangeResult,
51
- KanbanMetricItem,
52
- KanbanSelectionBarRenderContext,
53
- KanbanEmptyStateRenderContext,
54
- KanbanLoadingStateRenderContext,
55
- KanbanErrorStateRenderContext,
56
- KanbanTransitionPromptContext,
57
- KanbanOption,
58
- } from "./packages/kanban/index";
59
-
60
- export { Feed } from "./packages/feed/index";
61
- export type {
62
- FeedAction,
63
- FeedActor,
64
- FeedContainer,
65
- FeedEmptyStateRenderContext,
66
- FeedErrorStateRenderContext,
67
- FeedField,
68
- FeedFieldType,
69
- FeedItem,
70
- FeedLabels,
71
- FeedLoadingStateRenderContext,
72
- FeedPlacement,
73
- FeedProps,
74
- } from "./packages/feed/index";
1
+ // Each component module prefixes its type names (DataTable*, Kanban*, Feed*,
2
+ // Calendar*, FormBuilder*) and its value exports are unique, so `export *` is
3
+ // collision-free and keeps this root barrel automatically in sync with every
4
+ // type each component declares — no hand-maintained list to fall out of date.
5
+ export * from "./src/datatable/index";
6
+ export * from "./src/kanban/index";
7
+ export * from "./src/feed/index";
8
+ export * from "./src/calendar/index";
9
+ export * from "./src/form/index";
75
10
 
76
- export { FormBuilder, useFormPrefill } from "./packages/form/index";
77
- export type {
78
- FormBuilderProps,
79
- FormBuilderField,
80
- FormBuilderFieldType,
81
- FormBuilderOption,
82
- FormBuilderDateValue,
83
- FormBuilderTimeValue,
84
- FormBuilderDateTimeValue,
85
- FormBuilderValidationContext,
86
- FormBuilderValidatorResult,
87
- FormBuilderValidator,
88
- FormBuilderDependsOnConfig,
89
- FormBuilderRepeaterProps,
90
- FormBuilderGroupOptions,
91
- FormBuilderLabels,
92
- FormBuilderAlertConfig,
93
- FormBuilderButtonsRenderContext,
94
- FormBuilderSubmitAlign,
95
- FormBuilderLayout,
96
- FormBuilderLayoutEntry,
97
- FormBuilderSection,
98
- FormBuilderSectionContext,
99
- FormBuilderStep,
100
- FormBuilderRef,
101
- FieldTypePlugin,
102
- } from "./packages/form/index";
103
-
104
- export { AutoTag, AutoStatusTag, KeyValueList, SectionHeader, Spinner, SPINNERS, SPINNER_NAMES, gridToBraille, makeGrid } from "./common-components";
11
+ export {
12
+ ActiveFilterChips,
13
+ AutoTag,
14
+ AutoStatusTag,
15
+ CollectionCount,
16
+ CollectionFilterControl,
17
+ CollectionSortSelect,
18
+ CollectionToolbar,
19
+ CrmLookupSelect,
20
+ formatCollectionCount,
21
+ KeyValueList,
22
+ SectionHeader,
23
+ Spinner,
24
+ SPINNERS,
25
+ SPINNER_NAMES,
26
+ gridToBraille,
27
+ makeGrid,
28
+ } from "./common-components";
105
29
  export {
106
30
  Icon,
107
31
  ICON_NAMES,
@@ -131,8 +55,11 @@ export {
131
55
  DEFAULT_SVG_FONT_WEIGHT,
132
56
  } from "./common-components";
133
57
  export {
58
+ buildActiveFilterChips,
59
+ buildCrmSearchConfig,
134
60
  buildOptions,
135
61
  createStatusTagSortComparator,
62
+ crmSearchResultToOption,
136
63
  CrmDataTable,
137
64
  CrmKanban,
138
65
  findOptionLabel,
@@ -143,8 +70,21 @@ export {
143
70
  formatPercentage,
144
71
  getAutoTagVariant,
145
72
  getAutoStatusTagVariant,
73
+ getEmptyFilterValue,
74
+ getEmptyFilterValues,
75
+ isFilterActive,
76
+ makeCrmSearchMultiSelectField,
77
+ makeCrmSearchSelectField,
78
+ normalizeCrmSearchRecord,
79
+ normalizeCrmSearchRows,
80
+ resetFilterValues,
146
81
  resolveCrmObjectType,
82
+ searchRows,
83
+ filterRows,
147
84
  sumBy,
85
+ toStableKey,
86
+ useCrmSearchDataSource,
87
+ useCrmSearchOptions,
148
88
  } from "./utils";
149
89
  export type {
150
90
  AutoTagOptions,
@@ -156,9 +96,16 @@ export type {
156
96
  CrmKanbanProps,
157
97
  CrmSearchDataSource,
158
98
  CrmSearchConfigOptions,
99
+ CrmSearchOptionsDataSource,
100
+ CrmSearchPagination,
159
101
  CrmSearchParams,
102
+ QueryFilterConfig,
103
+ ActiveFilterChip,
104
+ FilterResetOptions,
105
+ BuildActiveFilterChipsOptions,
160
106
  } from "./utils";
161
107
  export type {
108
+ ActiveFilterChipsProps,
162
109
  AutoTagProps,
163
110
  AutoStatusTagProps,
164
111
  AvatarStackDataUriResult,
@@ -172,6 +119,10 @@ export type {
172
119
  IconSize,
173
120
  IconDataUriResult,
174
121
  IconDataUriOptions,
122
+ CollectionCountProps,
123
+ CollectionFilterControlProps,
124
+ CollectionSortSelectProps,
125
+ CollectionToolbarProps,
175
126
  CrmLookupSelectProps,
176
127
  DateDirectionLabels,
177
128
  DatePresetOption,
package/kanban.d.ts CHANGED
@@ -1 +1 @@
1
- export * from "./packages/kanban/index";
1
+ export * from "./src/kanban/index";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hs-uix",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Production-ready UI components for HubSpot UI Extensions — DataTable, FormBuilder, and more",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -32,6 +32,11 @@
32
32
  "import": "./dist/feed.mjs",
33
33
  "require": "./dist/feed.js"
34
34
  },
35
+ "./calendar": {
36
+ "types": "./calendar.d.ts",
37
+ "import": "./dist/calendar.mjs",
38
+ "require": "./dist/calendar.js"
39
+ },
35
40
  "./common-components": {
36
41
  "types": "./common-components.d.ts",
37
42
  "import": "./dist/common-components.mjs",
@@ -50,18 +55,17 @@
50
55
  "form.d.ts",
51
56
  "kanban.d.ts",
52
57
  "feed.d.ts",
58
+ "calendar.d.ts",
53
59
  "common-components.d.ts",
54
60
  "utils.d.ts",
55
- "packages/*/index.d.ts",
56
- "packages/*/README.md",
61
+ "src/**/*.d.ts",
57
62
  "README.md"
58
63
  ],
59
- "workspaces": [
60
- "packages/*"
61
- ],
62
64
  "scripts": {
63
65
  "build": "tsup",
64
66
  "build:icons": "node scripts/build-icons.mjs",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
65
69
  "dev": "tsup --watch",
66
70
  "link:demos": "npm link && cd ../hs-uix-demos/src/app/pages && npm link hs-uix",
67
71
  "pack:demos": "node scripts/pack-demos.mjs",
@@ -0,0 +1,301 @@
1
+ # Calendar (hs-uix/calendar)
2
+
3
+ [![npm version](https://img.shields.io/npm/v/hs-uix)](https://www.npmjs.com/package/hs-uix)
4
+ [![npm downloads](https://img.shields.io/npm/dm/hs-uix)](https://www.npmjs.com/package/hs-uix)
5
+ [![license](https://img.shields.io/npm/l/hs-uix)](https://github.com/05bmckay/hs-uix/blob/main/LICENSE)
6
+
7
+ A presentational calendar surface for HubSpot UI Extensions. Hand it an array of records plus an `eventFields` map and it renders a **Month**, **Week**, **Day**, or **Agenda** view with a Today / ‹ › / view-switcher toolbar, optional search + filters, and click-to-open event overlays (Popover, Modal, or Panel). Like Kanban and Feed, the calendar is data-driven and presentational — the caller owns fetching.
8
+
9
+ ```jsx
10
+ import { Calendar } from "hs-uix/calendar";
11
+
12
+ <Calendar
13
+ events={deals}
14
+ eventFields={{ id: "id", start: "closeDate", title: "name", subtitle: "owner", color: "color" }}
15
+ defaultView="month"
16
+ />
17
+ ```
18
+
19
+ ## Why Calendar?
20
+
21
+ HubSpot UI Extensions give you primitives, not a calendar. Rolling your own means building a month matrix by hand, equal-width day columns out of a `Table` with numeric widths, an hour grid that can't use `rowspan`, "+N more" overflow popovers, a Today/prev/next toolbar, and date coercion for the half-dozen shapes a CRM property can arrive in — all while working around the platform's missing height/scroll primitives. Calendar does that for you and exposes a small, controlled-or-uncontrolled API.
22
+
23
+ It maps **your** records to calendar roles via `eventFields` (a key or an accessor per role), so you don't reshape your data:
24
+
25
+ ```jsx
26
+ const fields = {
27
+ id: "id",
28
+ start: "closeDate", // a key…
29
+ end: (deal) => deal.dueDate, // …or an accessor
30
+ title: "name",
31
+ subtitle: "owner",
32
+ color: "stageColor", // a Tag variant: default|info|success|warning|error
33
+ href: (deal) => `/deal/${deal.id}`,
34
+ };
35
+ ```
36
+
37
+ ## Features
38
+
39
+ - Four stable views behind a `Select` switcher: **Month** (7-column day grid), **Week** / **Day** (hour-row time grid), and **Agenda** (day-grouped list)
40
+ - Controlled or uncontrolled `view` and `focusedDate`, with `onViewChange` / `onNavigate`
41
+ - Date coercion for every shape a HubSpot field arrives in: `Date`, epoch ms (number **or** string), ISO string, or a `DateInput` value object (`{ year, month, date }`, 0-indexed month) — date-only strings are parsed as **local** midnight so events never land a day early
42
+ - Multi-day events render across each day they touch; timed events show their start time
43
+ - Month-cell overflow collapses to a **"+N more"** popover; cap per cell with `maxEventsPerDay`
44
+ - Time grid honors `dayStartHour` / `dayEndHour`, an all-day band, and multi-hour blocks that note "↑ cont. through …"
45
+ - Search + filters reuse the same config shape as DataTable / Kanban (`select`, `multiselect`, `dateRange`), with optional fuzzy matching
46
+ - Event overlays via the standard `overlay` trigger: `overlayMode` of `"popover"` (default, experimental), `"modal"`, `"panel"`, or `"none"`, plus a `renderEventDetail` escape hatch and an always-fires `onEventClick`
47
+ - **Timezone support (opt-in)** — off by default (events render exactly as sent); set `timeZone` or enable the built-in `showTimeZoneSelect` dropdown and every time, grid placement, and day-grouping resolves in the chosen IANA zone, DST-correct
48
+ - Server-side friendly: `onRangeChange` fires on mount and whenever the visible range changes, with `loading` / `error` states
49
+ - Render-override slots: `renderToolbar`, `renderDayCell`, `renderEmptyState`, `renderLoadingState`, `renderErrorState`
50
+ - Full i18n surface via `labels`
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install hs-uix
56
+ ```
57
+
58
+ Import it in your card:
59
+
60
+ ```jsx
61
+ import { Calendar } from "hs-uix/calendar";
62
+ ```
63
+
64
+ Requires `@hubspot/ui-extensions` >= 0.14.0 and `react` >= 18.0.0 as peer dependencies (already present in any HubSpot UI Extensions project). TypeScript declarations are bundled with `hs-uix` (`calendar.d.ts`).
65
+
66
+ ---
67
+
68
+ ## Views
69
+
70
+ | View | What it shows | Navigation step |
71
+ |---|---|---|
72
+ | `month` | A 7-column day grid for the focused month (5–6 week rows). Each day stacks up to `maxEventsPerDay` event chips, then a "+N more" popover. | ± 1 month |
73
+ | `week` | An hour-row time grid for the focused week, plus an all-day band. | ± 1 week |
74
+ | `day` | The single-day hour schedule (the same grid as week, one wide column) with an all-day band and a "now" current-hour marker. | ± 1 day |
75
+ | `agenda` | The focused week's events grouped under day headers (time · title · owner rows). | ± 1 week |
76
+
77
+ The view switcher is a `Select` (not `Tabs`) because the platform caches `Tabs` bodies and they won't re-render on data changes.
78
+
79
+ ---
80
+
81
+ ## Examples
82
+
83
+ ### Deal close-date calendar
84
+
85
+ Deals plotted on their close date, colored by stage, with search + a stage filter and click-to-open popovers.
86
+
87
+ ```jsx
88
+ const STAGE_COLOR = {
89
+ "Closed won": "success",
90
+ "Contract sent": "info",
91
+ Negotiation: "warning",
92
+ Discovery: "default",
93
+ "Closed lost": "error",
94
+ };
95
+
96
+ <Calendar
97
+ events={deals.map((d) => ({ ...d, color: STAGE_COLOR[d.stage] }))}
98
+ eventFields={{ id: "id", start: "closeDate", title: "name", subtitle: "owner", color: "color" }}
99
+ defaultView="month"
100
+ showSearch
101
+ searchFields={["name", "owner", "stage"]}
102
+ filters={[
103
+ {
104
+ name: "stage",
105
+ type: "multiselect",
106
+ placeholder: "All stages",
107
+ chipLabel: "Stage",
108
+ options: Object.keys(STAGE_COLOR).map((s) => ({ label: s, value: s })),
109
+ },
110
+ ]}
111
+ maxEventsPerDay={3}
112
+ overlayMode="popover"
113
+ />
114
+ ```
115
+
116
+ ### Week schedule (time grid)
117
+
118
+ A rep's week in an hour-row grid, Monday start, 8 AM–6 PM, opening a Panel per meeting.
119
+
120
+ ```jsx
121
+ <Calendar
122
+ events={meetings}
123
+ eventFields={{ id: "id", start: "start", end: "end", title: "title", subtitle: "owner", color: "color" }}
124
+ defaultView="week"
125
+ views={["week", "day", "agenda"]}
126
+ weekStartsOn={1}
127
+ dayStartHour={8}
128
+ dayEndHour={18}
129
+ overlayMode="panel"
130
+ />
131
+ ```
132
+
133
+ ### Server-driven data
134
+
135
+ Fetch only the visible range. `onRangeChange` fires on mount and on every navigation / view change.
136
+
137
+ ```jsx
138
+ const [events, setEvents] = useState([]);
139
+ const [loading, setLoading] = useState(true);
140
+
141
+ <Calendar
142
+ events={events}
143
+ eventFields={fields}
144
+ serverSide
145
+ loading={loading}
146
+ onRangeChange={async ({ start, end, view }) => {
147
+ setLoading(true);
148
+ setEvents(await fetchEvents({ start, end }));
149
+ setLoading(false);
150
+ }}
151
+ />
152
+ ```
153
+
154
+ ### Controlled view + date
155
+
156
+ ```jsx
157
+ const [view, setView] = useState("month");
158
+ const [date, setDate] = useState(new Date());
159
+
160
+ <Calendar
161
+ events={events}
162
+ eventFields={fields}
163
+ view={view}
164
+ onViewChange={setView}
165
+ focusedDate={date}
166
+ onNavigate={(next) => setDate(next)}
167
+ />
168
+ ```
169
+
170
+ ### Custom overlay body
171
+
172
+ Override what opens when an event is clicked (works for popover / modal / panel).
173
+
174
+ ```jsx
175
+ <Calendar
176
+ events={events}
177
+ eventFields={fields}
178
+ overlayMode="modal"
179
+ renderEventDetail={(event) => (
180
+ <Flex direction="column" gap="sm">
181
+ <Text format={{ fontWeight: "demibold" }}>{event.title}</Text>
182
+ <Link href={`/deal/${event.id}`}>Open deal</Link>
183
+ </Flex>
184
+ )}
185
+ onEventClick={(raw, event) => track("event_opened", { id: event.id })}
186
+ />
187
+ ```
188
+
189
+ ### Timezones
190
+
191
+ **Timezones are off by default** — the calendar renders each event exactly as the data provides it (no conversion), so you never have to think about zones unless you want to. To pin a specific zone — or let users switch — opt into the timezone layer. Every time, hour-grid placement, and day-grouping then resolves in the chosen IANA zone, DST-correct.
192
+
193
+ ```jsx
194
+ // Built-in selector in the toolbar ("UTC −05:00 Central Time"); the layer starts at UTC:
195
+ <Calendar events={events} eventFields={fields} defaultView="week" showTimeZoneSelect />
196
+
197
+ // Or pin a zone with no selector:
198
+ <Calendar events={events} eventFields={fields} timeZone="America/New_York" />
199
+
200
+ // Controlled, with a custom zone list:
201
+ <Calendar
202
+ events={events}
203
+ eventFields={fields}
204
+ showTimeZoneSelect
205
+ timeZone={tz}
206
+ onTimeZoneChange={setTz}
207
+ timeZoneOptions={["UTC", "America/Chicago", "Europe/London", "Asia/Tokyo"]}
208
+ />
209
+ ```
210
+
211
+ How it works: each event instant is converted to a "wall-clock" `Date` in the active zone, so the existing local-time grid logic resolves in that zone with no other changes. Leave all the `timeZone*` props unset (and `showTimeZoneSelect` off) and the calendar does no conversion at all — events render as-sent in the viewer's local time.
212
+
213
+ ---
214
+
215
+ ## API Reference
216
+
217
+ ### Calendar Props
218
+
219
+ | Prop | Type | Default | Description |
220
+ |---|---|---|---|
221
+ | `events` | `Event[]` | — | Your records. The calendar is presentational; you own fetching. |
222
+ | `eventFields` | `CalendarEventFields` | sensible keys | Maps event fields to roles (`start` / `end` / `title` / `subtitle` / `color` / `href` / `id`). Each is a key or an accessor. |
223
+ | `view` | `CalendarView` | — | Controlled current view. |
224
+ | `defaultView` | `CalendarView` | `"month"` | Uncontrolled initial view. |
225
+ | `onViewChange` | `(view) => void` | — | Fires when the view changes. |
226
+ | `views` | `CalendarView[]` | all | Which views to expose in the switcher (month / week / day / agenda). |
227
+ | `focusedDate` | `CalendarDateInput` | — | Controlled focused date. |
228
+ | `defaultFocusedDate` | `CalendarDateInput` | today | Uncontrolled initial date. |
229
+ | `onNavigate` | `(date, { view }) => void` | — | Fires on prev / next / today. |
230
+ | `weekStartsOn` | `0 \| 1` | `0` | 0 = Sunday, 1 = Monday. |
231
+ | `hideWeekends` | `boolean` | `false` | Hide Sat/Sun in month + week views. |
232
+ | `maxEventsPerDay` | `number` | `3` | Chips per month cell before "+N more". |
233
+ | `dayStartHour` | `number` | `8` | First hour row in week/day (0–23). |
234
+ | `dayEndHour` | `number` | `20` | Last hour row in week/day (0–23). |
235
+ | `timeZone` | `string` | — | Controlled IANA zone (e.g. `"America/Chicago"`). Setting it opts into the tz layer; all times/placement/grouping resolve here. |
236
+ | `defaultTimeZone` | `string` | — | Uncontrolled initial zone. Opts into the tz layer; the layer itself starts at `"UTC"` if engaged without this. |
237
+ | `onTimeZoneChange` | `(tz) => void` | — | Fires when the zone changes. |
238
+ | `showTimeZoneSelect` | `boolean` | `false` | Show the built-in zone dropdown. Opts into the tz layer (starts at UTC). Off ⇒ no tz layer, events render as-sent. |
239
+ | `timeZoneOptions` | `(string \| { value, label? })[]` | curated list | Override the selector's zones. |
240
+ | `filters` | `CalendarFilterConfig[]` | — | `select` / `multiselect` / `dateRange` filters (DataTable shape). |
241
+ | `searchFields` | `string[]` | — | Top-level keys searched by the search box. **Required** for `showSearch` to match anything. |
242
+ | `searchValue` / `onSearchChange` | `string` / `fn` | — | Controlled search. |
243
+ | `filterValues` / `onFilterChange` | `object` / `fn` | — | Controlled filter values. |
244
+ | `fuzzySearch` | `boolean` | `false` | Fuzzy match (Fuse.js) instead of substring. |
245
+ | `showSearch` | `boolean` | `false` | Show the search box (pair with `searchFields`). |
246
+ | `serverSide` | `boolean` | `false` | Treat data as server-provided. |
247
+ | `loading` | `boolean` | `false` | Render the loading state. |
248
+ | `error` | `string \| boolean` | `false` | Render the error state. |
249
+ | `onRangeChange` | `({ start, end, view }) => void` | — | Fires on mount + range change — fetch here. |
250
+ | `overlayMode` | `"popover" \| "modal" \| "panel" \| "none"` | `"popover"` | How an event opens. Popover is experimental. |
251
+ | `renderEventDetail` | `(event) => ReactNode` | — | Override the overlay body. |
252
+ | `onEventClick` | `(raw, event) => void` | — | Always fires on event click. |
253
+ | `renderToolbar` | `(api) => ReactNode` | — | Replace the toolbar. |
254
+ | `renderDayCell` | `(day, events) => ReactNode` | — | Replace a month day cell's body. |
255
+ | `renderLoadingState` / `renderErrorState` | `fn` | — | Replace those states. |
256
+ | `renderEmptyState` | `fn` | — | Replace the empty state in the **agenda** view (the month/week/day grids always render). |
257
+ | `labels` | `CalendarLabels` | English | i18n strings. |
258
+
259
+ ### `eventFields`
260
+
261
+ | Role | Type | Notes |
262
+ |---|---|---|
263
+ | `id` | key \| `(event) => string \| number` | Stable id (used for React keys + overlay ids). |
264
+ | `start` | key \| accessor → `CalendarDateInput` | Required for an event to appear. |
265
+ | `end` | key \| accessor → `CalendarDateInput` | Optional; enables multi-day + duration. |
266
+ | `title` | key \| accessor → `ReactNode` | The event label. |
267
+ | `subtitle` | key \| accessor → `ReactNode` | Secondary text (e.g. owner). |
268
+ | `color` | key \| accessor → `string` | A Tag variant: `default` / `info` / `success` / `warning` / `error`. |
269
+ | `href` | `CalendarHref` \| accessor | `string` or `{ url, external? }`. |
270
+
271
+ `CalendarDateInput` = `Date` | epoch `number` | ISO/epoch `string` | `{ year, month, date, hour?, minute? }` (0-indexed `month`).
272
+
273
+ ### Normalized event
274
+
275
+ Render callbacks (`renderEventDetail`, `onEventClick`, `renderDayCell`) receive the normalized event:
276
+
277
+ ```ts
278
+ { key, id, start: Date | null, end: Date | null, title, subtitle, color, href, raw }
279
+ ```
280
+
281
+ `raw` is your original record.
282
+
283
+ ---
284
+
285
+ ## Limitations
286
+
287
+ These come from HubSpot UI Extensions itself, not Calendar:
288
+
289
+ | Limitation | Details |
290
+ |---|---|
291
+ | No height / vertical scroll | Layout primitives have no `height` / `overflow`. Long lists expand the page; the month grid caps chips per cell (`maxEventsPerDay`) and overflows to a popover. |
292
+ | View switching is a `Select` | `Tabs` cache their body and don't re-render on data changes, so the switcher is a `Select`. |
293
+ | Time grid can't span rows | No `rowspan` / height control, so a multi-hour block repeats with a "↑ cont. through …" note rather than one tall block. |
294
+ | Equal day columns via numeric widths | Equal-width columns require a `Table` with equal **numeric** widths (content-sized `flex` columns come out uneven). |
295
+ | Popover is experimental | `overlayMode="popover"` uses `@hubspot/ui-extensions/experimental`. Use `"modal"` / `"panel"` if you need a stable overlay. |
296
+
297
+ ---
298
+
299
+ ## License
300
+
301
+ MIT