@netsapiens/horizon-sdk 0.1.2 → 0.1.3

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 CHANGED
@@ -29,18 +29,18 @@ Peer deps: `react ^18 || ^19`, `react-dom ^18 || ^19`, `loglevel ^1.9`.
29
29
 
30
30
  ```tsx
31
31
  // src/App.tsx
32
- import { useEffect } from 'react';
33
- import { type HorizonContext, useRemoteApp } from '@netsapiens/horizon-sdk';
34
- import MyButton from './extensions/MyButton';
32
+ import { useEffect } from "react";
33
+ import { type HorizonContext, useRemoteApp } from "@netsapiens/horizon-sdk";
34
+ import MyButton from "./extensions/MyButton";
35
35
 
36
36
  export default function App(horizonContext: HorizonContext) {
37
- const { sdk, user, theme } = useRemoteApp(horizonContext, 'my-app');
37
+ const { sdk, user, theme } = useRemoteApp(horizonContext, "my-app");
38
38
 
39
39
  useEffect(() => {
40
40
  sdk.registerDynamicExtension({
41
- id: 'my-app.export-button',
42
- zone: 'page-header-actions',
43
- routes: [{ pattern: '/manage/*/call-logs' }],
41
+ id: "my-app.export-button",
42
+ zone: "page-header-actions",
43
+ routes: [{ pattern: "/manage/*/call-logs" }],
44
44
  component: MyButton,
45
45
  });
46
46
  }, [sdk]);
@@ -51,14 +51,20 @@ export default function App(horizonContext: HorizonContext) {
51
51
 
52
52
  ```tsx
53
53
  // src/extensions/MyButton.tsx
54
- import { type ExtensionComponentProps, useTheme } from '@netsapiens/horizon-sdk';
54
+ import {
55
+ type ExtensionComponentProps,
56
+ useTheme,
57
+ } from "@netsapiens/horizon-sdk";
55
58
 
56
59
  export default function MyButton({ context }: ExtensionComponentProps) {
57
60
  const { theme } = useTheme(context.eventBus); // reactive — no manual event wiring
58
61
  const { Button, Icon } = context.ui ?? {};
59
62
  if (!Button || !Icon) return null;
60
63
  return (
61
- <Button startIcon={<Icon icon="mdi:download" />} onClick={() => alert('Exported!')}>
64
+ <Button
65
+ startIcon={<Icon icon="mdi:download" />}
66
+ onClick={() => alert("Exported!")}
67
+ >
62
68
  Export
63
69
  </Button>
64
70
  );
@@ -91,8 +97,11 @@ export default function MyButton({ context }: ExtensionComponentProps) {
91
97
  Register a full page route using the SDK. Wrap your page component in `HorizonContextProvider` so it gets a live, reactive context — including automatic dark mode — without any extra wiring.
92
98
 
93
99
  ```tsx
94
- import { HorizonContextProvider, useHorizonContext } from '@netsapiens/horizon-sdk';
95
- import MySettingsPage from './pages/MySettingsPage';
100
+ import {
101
+ HorizonContextProvider,
102
+ useHorizonContext,
103
+ } from "@netsapiens/horizon-sdk";
104
+ import MySettingsPage from "./pages/MySettingsPage";
96
105
 
97
106
  // In your App component:
98
107
  const MySettingsRoute = useMemo(
@@ -109,12 +118,12 @@ const MySettingsRoute = useMemo(
109
118
  );
110
119
 
111
120
  sdk.registerRoute({
112
- id: 'my-app.settings',
113
- parentPath: '/home', // attach under Apps menu
114
- path: 'my-settings', // → /home/my-settings
115
- label: 'My Settings',
116
- icon: 'mdi:cog',
117
- placement: { after: 'settings' }, // position after the Settings item
121
+ id: "my-app.settings",
122
+ parentPath: "/home", // attach under Apps menu
123
+ path: "my-settings", // → /home/my-settings
124
+ label: "My Settings",
125
+ icon: "mdi:cog",
126
+ placement: { after: "settings" }, // position after the Settings item
118
127
  component: MySettingsRoute,
119
128
  });
120
129
  ```
@@ -125,12 +134,12 @@ Use `placement` to control where your route appears in the menu. You can positio
125
134
 
126
135
  ```tsx
127
136
  sdk.registerRoute({
128
- id: 'my-app.settings',
129
- parentPath: '/home',
130
- path: 'my-settings',
131
- label: 'My Settings',
132
- icon: 'mdi:cog',
133
- placement: { after: 'settings' }, // After the Settings item
137
+ id: "my-app.settings",
138
+ parentPath: "/home",
139
+ path: "my-settings",
140
+ label: "My Settings",
141
+ icon: "mdi:cog",
142
+ placement: { after: "settings" }, // After the Settings item
134
143
  component: MySettingsRoute,
135
144
  });
136
145
  ```
@@ -148,7 +157,7 @@ Inside the page component, read context with `useHorizonContext()` instead of ac
148
157
 
149
158
  ```tsx
150
159
  // src/pages/MySettingsPage.tsx
151
- import { useHorizonContext } from '@netsapiens/horizon-sdk';
160
+ import { useHorizonContext } from "@netsapiens/horizon-sdk";
152
161
 
153
162
  export default function MySettingsPage() {
154
163
  const { ui, user, navigate, theme } = useHorizonContext();
@@ -156,10 +165,13 @@ export default function MySettingsPage() {
156
165
  const { Button, Stack, Typography } = ui;
157
166
 
158
167
  return (
159
- <PageTemplate title="Settings" breadcrumbs={[{ label: 'Apps', url: '/apps' }]}>
168
+ <PageTemplate
169
+ title="Settings"
170
+ breadcrumbs={[{ label: "Apps", url: "/apps" }]}
171
+ >
160
172
  <Stack spacing={2}>
161
173
  <Typography variant="body1">Hello, {user.displayName}.</Typography>
162
- <Button variant="contained" onClick={() => navigate('/apps')}>
174
+ <Button variant="contained" onClick={() => navigate("/apps")}>
163
175
  Back
164
176
  </Button>
165
177
  </Stack>
@@ -179,9 +191,14 @@ If your route component lives in a separate exposed module, use `useRouteFromMod
179
191
  ```tsx
180
192
  const { loading, error } = useRouteFromModule(
181
193
  horizonContext.eventBus,
182
- 'my-app',
183
- { id: 'my-app.settings', parentPath: '/home', path: 'my-settings', label: 'My Settings' },
184
- { scope: 'myApp', module: './SettingsPage' },
194
+ "my-app",
195
+ {
196
+ id: "my-app.settings",
197
+ parentPath: "/home",
198
+ path: "my-settings",
199
+ label: "My Settings",
200
+ },
201
+ { scope: "myApp", module: "./SettingsPage" },
185
202
  );
186
203
  ```
187
204
 
@@ -191,31 +208,31 @@ Extensions render at named **zones** on routes that match a **pattern**. You don
191
208
 
192
209
  ```tsx
193
210
  sdk.registerDynamicExtension({
194
- id: 'my-app.row-action',
195
- zone: 'table-row-actions',
196
- routes: [{ pattern: '/manage/*/call-logs' }],
197
- priority: 10, // higher = first
198
- requiredPermissions: ['calls:read'],
199
- condition: (ctx) => ctx.user.domain === 'special.com',
211
+ id: "my-app.row-action",
212
+ zone: "table-row-actions",
213
+ routes: [{ pattern: "/manage/*/call-logs" }],
214
+ priority: 10, // higher = first
215
+ requiredPermissions: ["calls:read"],
216
+ condition: (ctx) => ctx.user.domain === "special.com",
200
217
  component: RowAction,
201
218
  });
202
219
  ```
203
220
 
204
221
  **Available zones:**
205
222
 
206
- | Zone | Where it renders |
207
- | -------------------------- | -------------------------------------------- |
208
- | `page-header-actions` | Right side of any page header |
209
- | `page-header-secondary` | Subtitle row of page header (badges, status) |
210
- | `page-content-before` | Above main page content |
211
- | `page-content-after` | Below main page content |
212
- | `page-sidebar` | Page sidebar |
213
- | `table-toolbar` | Above any DataGrid |
214
- | `table-row-actions` | Per-row action column |
215
- | `detail-panel-tabs` | Detail-panel tab strip |
216
- | `detail-panel-actions` | Detail-panel action area |
217
- | `topbar-actions` | Global top app bar (after comms, before utilities) |
218
- | `inbound-call-content` | Incoming-call notification body |
223
+ | Zone | Where it renders |
224
+ | ----------------------- | ------------------------------------------------------------------------------ |
225
+ | `page-header-actions` | Right side of any page header |
226
+ | `page-header-secondary` | Subtitle row of page header (badges, status) |
227
+ | `page-content-after` | Below main page content |
228
+ | `sidetray` | Side tray panel (open/close via host toggle) |
229
+ | `table-toolbar` | Above any DataGrid |
230
+ | `table-filter-bar` | Filter chips inline with search bar; pass a row predicate via `onFilterChange` |
231
+ | `table-row-actions` | Per-row action column |
232
+ | `detail-panel-tabs` | Detail-panel tab strip |
233
+ | `detail-panel-actions` | Detail-panel action area |
234
+ | `topbar-actions` | Global top app bar (after comms, before utilities) |
235
+ | `inbound-call-content` | Incoming-call notification body |
219
236
 
220
237
  > **Note**: Check Platform → UI SDK Management → SDK Settings for the current list of available zones and their descriptions.
221
238
 
@@ -233,26 +250,102 @@ Custom zones (any string) work for pages that explicitly render `<DynamicExtensi
233
250
  Hook form:
234
251
 
235
252
  ```tsx
236
- useDynamicExtension(horizonContext.eventBus, 'my-app', {
237
- id: 'my-app.banner',
238
- zone: 'page-content-before',
239
- routes: [{ pattern: '/manage/:domain/users' }],
253
+ useDynamicExtension(horizonContext.eventBus, "my-app", {
254
+ id: "my-app.banner",
255
+ zone: "page-content-after",
256
+ routes: [{ pattern: "/manage/:domain/users" }],
240
257
  component: Banner,
241
258
  });
242
259
  ```
243
260
 
261
+ #### table-filter-bar zone
262
+
263
+ Pages that render a filter bar (e.g. active calls, hardphone devices) expose a
264
+ `TableFilterBarContext` via `context.pageContext`. Extensions in this zone render
265
+ alongside the host's built-in filter chips and can drive row-level filtering by
266
+ passing a **predicate function** to `onFilterChange`. The host applies the function
267
+ generically — your extension owns the filtering logic, not the host.
268
+
269
+ ```tsx
270
+ import { useState } from "react";
271
+ import type {
272
+ ExtensionComponentProps,
273
+ TableFilterBarContext,
274
+ } from "@netsapiens/horizon-sdk";
275
+
276
+ export function VipFilter({ context }: ExtensionComponentProps) {
277
+ const filterCtx = context.pageContext as TableFilterBarContext | undefined;
278
+ const { ToggleButtonGroup } = context.ui ?? {};
279
+ const [active, setActive] = useState(false);
280
+
281
+ function handleChange(_e: React.SyntheticEvent, value: string | null) {
282
+ const next = value === "vip";
283
+ setActive(next);
284
+ if (next) {
285
+ filterCtx?.onFilterChange((row) => {
286
+ const r = row as Record<string, unknown>;
287
+ return r["customer-tier"] === "vip";
288
+ });
289
+ } else {
290
+ filterCtx?.onFilterChange(null); // clear — show all rows
291
+ }
292
+ }
293
+
294
+ if (!ToggleButtonGroup) return null;
295
+
296
+ return (
297
+ <ToggleButtonGroup
298
+ value={active ? "vip" : null}
299
+ exclusive
300
+ onChange={handleChange}
301
+ options={[{ value: "vip", label: "VIP Only" }]}
302
+ />
303
+ );
304
+ }
305
+
306
+ // Register against any page that exposes a filter bar
307
+ sdk.registerDynamicExtension({
308
+ id: "my-app.vip-filter",
309
+ zone: "table-filter-bar",
310
+ routes: [{ pattern: "/manage/:domain/call-logs" }],
311
+ component: VipFilter,
312
+ });
313
+ ```
314
+
315
+ `TableFilterBarContext` fields:
316
+
317
+ | Field | Type | Description |
318
+ | ---------------- | --------------------------------------------------------- | ----------------------------------------------------- |
319
+ | `onFilterChange` | `(filterFn: ((row: unknown) => boolean) \| null) => void` | Pass a predicate to filter rows; pass `null` to clear |
320
+
321
+ **Important:** track active/inactive state locally with `useState` in your component —
322
+ `TableFilterBarContext` does not expose the current filter state back to you.
323
+
324
+ > **React `useState` gotcha** — never pass a filter function directly to `setState`:
325
+ >
326
+ > ```ts
327
+ > // ❌ React treats functions as lazy initializers, calling fn(prevState)
328
+ > setMyFilter(filterFn);
329
+ >
330
+ > // ✅ Wrap in an arrow function so React stores the function, not its return value
331
+ > setMyFilter(() => filterFn);
332
+ > ```
333
+ >
334
+ > This only affects you if you're storing the filter function in your own component state.
335
+ > The host DataTable handles this correctly internally.
336
+
244
337
  ### Dynamic table columns
245
338
 
246
339
  Add columns to host DataGrids that opt in (zone names like `call-logs-columns`, `users-columns`, etc.):
247
340
 
248
341
  ```tsx
249
342
  sdk.registerDynamicColumn({
250
- id: 'my-app.priority',
251
- zone: 'call-logs-columns',
252
- routes: [{ pattern: '/manage/*/call-logs' }],
343
+ id: "my-app.priority",
344
+ zone: "call-logs-columns",
345
+ routes: [{ pattern: "/manage/*/call-logs" }],
253
346
  column: {
254
- field: 'priority',
255
- headerName: 'Priority',
347
+ field: "priority",
348
+ headerName: "Priority",
256
349
  width: 120,
257
350
  sortable: true,
258
351
  filterable: true,
@@ -274,7 +367,7 @@ The recommended pattern uses `useHorizonContext()` (see Routes above). The old p
274
367
 
275
368
  ```tsx
276
369
  // Recommended — page component using the hook (fully reactive theme)
277
- import { useHorizonContext } from '@netsapiens/horizon-sdk';
370
+ import { useHorizonContext } from "@netsapiens/horizon-sdk";
278
371
 
279
372
  export default function MyPage() {
280
373
  const { ui, user } = useHorizonContext();
@@ -282,7 +375,10 @@ export default function MyPage() {
282
375
  const { Button, Stack, Typography } = ui;
283
376
 
284
377
  return (
285
- <PageTemplate title="My Page" breadcrumbs={[{ label: 'Apps', url: '/apps' }]}>
378
+ <PageTemplate
379
+ title="My Page"
380
+ breadcrumbs={[{ label: "Apps", url: "/apps" }]}
381
+ >
286
382
  <Stack spacing={2}>
287
383
  <Typography variant="h5">Hello, {user.displayName}</Typography>
288
384
  <Button variant="contained">Action</Button>
@@ -302,11 +398,11 @@ function MyButton({ context }: ExtensionComponentProps) {
302
398
 
303
399
  Available under `horizonContext.ui` (or via `useHorizonContext().ui`):
304
400
 
305
- - `templates`: `PageTemplate`, `PageTemplateWithExtensions`, `FormTemplate`, `SideTrayTemplate`, `DatagridTemplate`
401
+ - `templates`: `PageTemplate`, `PageTemplateWithExtensions`, `FormTemplate`, `SidePanel`, `FormPanel`, `DatagridTemplate`
306
402
 
307
403
  > **`DatagridTemplate` and the MUI X Pro license** — `DatagridTemplate` wraps MUI's `DataGridPro` internally, which is a paid licensed component. The license is held by the Horizon host; **you do not need an MUI X Pro license** and must not import `@mui/x-data-grid-pro` directly in your remote app. Always use `DatagridTemplate` from the SDK.
308
404
 
309
- - Form: `Button`, `IconButton`, `TextField`, `Select`, `Checkbox`, `Radio`, `RadioGroup`, `Switch`, `ToggleButton`, `ToggleButtonGroup`, `FormLabel`
405
+ - Form: `Button`, `IconButton`, `TextField`, `Select`, `Checkbox`, `Radio`, `RadioGroup`, `Switch`, `ToggleButton`, `ToggleButtonGroup`, `FormLabel`, `FormControlLabel`
310
406
  - Display: `Typography`, `Chip`, `Avatar`, `Divider`, `Tooltip`, `Icon`
311
407
  - Layout: `Stack`, `Paper`
312
408
  - Feedback: `Alert`
@@ -331,6 +427,80 @@ The TypeScript types reject the children pattern at compile time. If you ever se
331
427
 
332
428
  Other themed components (`Button`, `Stack`, `Paper`, etc.) follow the standard MUI children pattern.
333
429
 
430
+ #### SidePanel & FormPanel — right-side drawers
431
+
432
+ `SidePanel` and `FormPanel` are the shared right-drawer components Horizon uses
433
+ for its own add/edit forms and detail panels. Reach for these when building a
434
+ right-side drawer so it matches the host exactly and picks up the
435
+ `form-section-*` extension zones automatically.
436
+
437
+ **`SidePanel`** — the drawer shell (sticky header, scrollable body, optional
438
+ sticky footer). Use for detail/view/settings panels:
439
+
440
+ ```tsx
441
+ const { SidePanel } = ui.templates;
442
+
443
+ <SidePanel
444
+ open={open}
445
+ onClose={() => setOpen(false)}
446
+ title="Details"
447
+ subtitle="Read-only"
448
+ width="lg" // 'sm' | 'md' | 'lg' | 'xl'
449
+ footer={<Button onClick={() => setOpen(false)}>Close</Button>}
450
+ >
451
+ {/* body */}
452
+ </SidePanel>;
453
+ ```
454
+
455
+ **`FormPanel`** — `SidePanel` + a `<form>`, a Submit/Cancel footer, and the
456
+ `form-section-before` / `form-section-after` extension zones built in:
457
+
458
+ ```tsx
459
+ const { FormPanel } = ui.templates;
460
+
461
+ <FormPanel
462
+ open={open}
463
+ onClose={() => setOpen(false)}
464
+ title="Add widget"
465
+ formType="widget" // identifies the form to extensions
466
+ mode="add" // 'add' | 'edit'
467
+ formData={values} // live values handed to extensions
468
+ onSubmit={(e) => {
469
+ e?.preventDefault();
470
+ save(values);
471
+ }}
472
+ submitLabel="Add"
473
+ isSubmitting={pending}
474
+ error={error}
475
+ >
476
+ {/* field sections */}
477
+ </FormPanel>;
478
+ ```
479
+
480
+ **Multi-step wizard** — pass `steps` instead of `children`; FormPanel renders the
481
+ stepper header and a Back/Next/Submit footer. Each step's optional `validate`
482
+ gates forward navigation (return `boolean | Promise<boolean>`):
483
+
484
+ ```tsx
485
+ <FormPanel
486
+ open={open}
487
+ onClose={() => setOpen(false)}
488
+ title="Setup"
489
+ formType="setup"
490
+ mode="add"
491
+ onSubmit={handleSubmit(onValid)}
492
+ isComplete={isValid} // offer Submit before the last step
493
+ steps={[
494
+ {
495
+ label: "Basics",
496
+ content: <BasicsStep />,
497
+ validate: () => trigger(["name"]),
498
+ },
499
+ { label: "Options", content: <OptionsStep /> },
500
+ ]}
501
+ />
502
+ ```
503
+
334
504
  ---
335
505
 
336
506
  ## Dark mode
@@ -340,7 +510,7 @@ Dark mode is handled automatically — **you write no theme-switching code for M
340
510
  MUI components (`Button`, `Paper`, `Stack`, `DatagridTemplate`, etc.) inherit the host's Aurora ThemeProvider via the shared module singleton and switch the moment the user toggles the theme. For any conditional logic that reads the theme string (custom colors, conditional icons, etc.), use `useTheme()`:
341
511
 
342
512
  ```tsx
343
- import { useTheme } from '@netsapiens/horizon-sdk';
513
+ import { useTheme } from "@netsapiens/horizon-sdk";
344
514
  ```
345
515
 
346
516
  `useTheme()` is a single hook that works identically in both contexts:
@@ -354,14 +524,16 @@ import { useTheme } from '@netsapiens/horizon-sdk';
354
524
  // Page component
355
525
  export default function MyPage() {
356
526
  const { theme } = useTheme(); // no argument needed inside HorizonContextProvider
357
- return <div style={{ background: theme === 'dark' ? '#1B2124' : '#fff' }}>...</div>;
527
+ return (
528
+ <div style={{ background: theme === "dark" ? "#1B2124" : "#fff" }}>...</div>
529
+ );
358
530
  }
359
531
 
360
532
  // Extension component
361
533
  export default function MyButton({ context }: ExtensionComponentProps) {
362
534
  const { theme } = useTheme(context.eventBus); // pass eventBus for extensions
363
535
  const { Button } = context.ui ?? {};
364
- return <Button>{theme === 'dark' ? '🌙' : '☀️'} Export</Button>;
536
+ return <Button>{theme === "dark" ? "🌙" : "☀️"} Export</Button>;
365
537
  }
366
538
  ```
367
539
 
@@ -373,9 +545,11 @@ export default function MyButton({ context }: ExtensionComponentProps) {
373
545
 
374
546
  ```tsx
375
547
  const { api, user } = horizonContext;
376
- const devices = await api.get(`/domains/${user.domain}/users/${user.displayName}/devices`);
548
+ const devices = await api.get(
549
+ `/domains/${user.domain}/users/${user.displayName}/devices`,
550
+ );
377
551
  await api.post(`/domains/${user.domain}/users/${user.displayName}/contacts`, {
378
- 'name-first-name': 'John',
552
+ "name-first-name": "John",
379
553
  });
380
554
  ```
381
555
 
@@ -389,9 +563,9 @@ For cross-app messages and host events:
389
563
 
390
564
  ```tsx
391
565
  useEffect(() => {
392
- const handler = (data: unknown) => console.log('theme changed', data);
393
- horizonContext.eventBus.on('theme:changed', handler);
394
- return () => horizonContext.eventBus.off('theme:changed', handler);
566
+ const handler = (data: unknown) => console.log("theme changed", data);
567
+ horizonContext.eventBus.on("theme:changed", handler);
568
+ return () => horizonContext.eventBus.off("theme:changed", handler);
395
569
  }, []);
396
570
  ```
397
571
 
@@ -426,25 +600,30 @@ During local development the webpack dev server sets `Access-Control-Allow-Origi
426
600
  Use webpack Module Federation. Minimal `webpack.config.js`:
427
601
 
428
602
  ```js
429
- const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
603
+ const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
430
604
 
431
605
  module.exports = (_env, argv) => ({
432
- entry: './src/App.tsx',
433
- output: { publicPath: 'auto', filename: 'remoteEntry.js', clean: true },
434
- resolve: { extensions: ['.tsx', '.ts', '.js'] },
606
+ entry: "./src/App.tsx",
607
+ output: { publicPath: "auto", filename: "remoteEntry.js", clean: true },
608
+ resolve: { extensions: [".tsx", ".ts", ".js"] },
435
609
  module: {
436
- rules: [{ test: /\.(tsx?|jsx?)$/, exclude: /node_modules/, use: 'babel-loader' }],
610
+ rules: [
611
+ { test: /\.(tsx?|jsx?)$/, exclude: /node_modules/, use: "babel-loader" },
612
+ ],
437
613
  },
438
614
  plugins: [
439
615
  new ModuleFederationPlugin({
440
- name: 'myApp', // your Extension App ID — must match exactly what you enter during registration
441
- filename: 'remoteEntry.js',
442
- exposes: { './App': './src/App' },
616
+ name: "myApp", // your Extension App ID — must match exactly what you enter during registration
617
+ filename: "remoteEntry.js",
618
+ exposes: { "./App": "./src/App" },
443
619
  shared: {
444
- react: { singleton: true, requiredVersion: '^19.0.0' },
445
- 'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
446
- loglevel: { singleton: true, requiredVersion: '^1.9.0' },
447
- '@netsapiens/horizon-sdk': { singleton: true, requiredVersion: '^1.0.0' },
620
+ react: { singleton: true, requiredVersion: "^19.0.0" },
621
+ "react-dom": { singleton: true, requiredVersion: "^19.0.0" },
622
+ loglevel: { singleton: true, requiredVersion: "^1.9.0" },
623
+ "@netsapiens/horizon-sdk": {
624
+ singleton: true,
625
+ requiredVersion: "^1.0.0",
626
+ },
448
627
 
449
628
  // Do NOT add @mui/material, @emotion/*, or @mui/x-data-grid-pro here.
450
629
  // The host's federation loader does not register MUI as a shared module,
@@ -455,7 +634,7 @@ module.exports = (_env, argv) => ({
455
634
  },
456
635
  }),
457
636
  ],
458
- devServer: { port: 5005, headers: { 'Access-Control-Allow-Origin': '*' } },
637
+ devServer: { port: 5005, headers: { "Access-Control-Allow-Origin": "*" } },
459
638
  });
460
639
  ```
461
640
 
@@ -594,10 +773,10 @@ Use **Disable** when you need to take an app offline temporarily (maintenance, i
594
773
  Throw and catch typed errors with codes:
595
774
 
596
775
  ```tsx
597
- import { HorizonSDKError, apiError } from '@netsapiens/horizon-sdk';
776
+ import { HorizonSDKError, apiError } from "@netsapiens/horizon-sdk";
598
777
 
599
778
  try {
600
- await api.get('/...');
779
+ await api.get("/...");
601
780
  } catch (err) {
602
781
  if (HorizonSDKError.isHorizonSDKError(err)) {
603
782
  console.error(err.code, err.getUserMessage());