@nebulahome/package-sdk 1.3.0 → 1.5.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/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  The SDK's version is the version of the package API the platform implements: a minor version adds
4
4
  to it, a major one breaks it, and a patch leaves it as it is.
5
5
 
6
+ ## 1.5.0
7
+
8
+ - **The live channel.** A package's server and its open interfaces talk over the interface's own
9
+ socket, both ways, with no polling and no HTTP rate limit. On the server
10
+ `ctx.live.publish(topic, data, { isRetained })` sends to every interface following the topic -
11
+ a retained message is handed at once to whoever follows it later - and
12
+ `ctx.live.onMessage(topic, { input }, handler)` hears what they send, with who sent it. In the
13
+ interface `usePackageLive(topic)` follows a topic (one subscription however many components
14
+ follow it) and `usePackageSend()` sends. Whoever may read packages may follow and send. Topics
15
+ are a letter, then letters, digits and `.`, `_`, `:`, `-`; a message is at most 1 MB from the
16
+ server, 64 KB from an interface, which sends at most 50 a second.
17
+ - `createTestContext()` records what is published (`published`) and sends a message as the
18
+ interface would (`sendLive`).
19
+
20
+ ## 1.4.0
21
+
22
+ - **Toasts.** `useToast()` from `@nebulahome/package-sdk/ui` shows the interface's own toasts -
23
+ `success`, `warning`, `error`, `info`, each with a title, a line or two and at most one action -
24
+ for the outcome of something the viewer just did.
25
+
6
26
  ## 1.3.0
7
27
 
8
28
  - **The page's actions in its header.** `<PageActions>` from `@nebulahome/package-sdk/ui` draws
package/README.md CHANGED
@@ -140,18 +140,60 @@ import { sunPosition } from '@nebulahome/package-sdk/sun';
140
140
  const { elevation, azimuth } = sunPosition(at, { latitude, longitude });
141
141
  ```
142
142
 
143
+ ## Toasts
144
+
145
+ The interface's own toasts, for the outcome of something the viewer just did (since 1.4):
146
+
147
+ ```tsx
148
+ const toast = useToast();
149
+ const save = usePackageMutation('rules.save');
150
+
151
+ try {
152
+ await save.mutate(rules);
153
+ toast.success({ title: t('save.done', 'Saved') });
154
+ } catch (error) {
155
+ toast.error({ title: t('save.failed', "Couldn't save"), message: String(error) });
156
+ }
157
+ ```
158
+
159
+ ## The live channel
160
+
161
+ Messages between the server and its open interfaces over the interface's own socket, both ways,
162
+ with no polling and no HTTP rate limit (since 1.5). Whoever may read packages follows and sends.
163
+
164
+ ```ts
165
+ // on the server: what every page and card draws, kept for whoever opens one later
166
+ ctx.live.publish('overview', overviewOf(), { isRetained: true });
167
+
168
+ // and what the interface sends, with who sent it
169
+ ctx.live.onMessage('player.report', { input: reportSchema }, (report, { caller }) => {
170
+ ctx.live.publish(`player.${report.playerId}`, orderFor(report));
171
+ });
172
+ ```
173
+
174
+ ```tsx
175
+ const { data, isConnected } = usePackageLive<Overview>('overview'); // one subscription per page
176
+ const send = usePackageSend();
177
+
178
+ send('player.report', { playerId, position });
179
+ ```
180
+
181
+ A topic is a letter, then letters, digits and `.`, `_`, `:`, `-`; a message from the server is at
182
+ most 1 MB as JSON, one from the interface 64 KB, and an interface sends at most 50 a second.
183
+ Nothing answers a message: a call that wants an answer or an error stays `usePackageCall`.
184
+
143
185
  ## Entry points
144
186
 
145
- | Import | What |
146
- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147
- | `@nebulahome/package-sdk` | `definePackage`, `defineSettings`, `defineCard`, `defineEntityType`, `definePage`, `defineContainer`, `defineCompose`, `field`, entity id helpers, `SDK_VERSION`, the value types. |
148
- | `/server` | `defineServer`, and the context `start` is given: settings (live or restart), devices and entities, typed custom entities, the rest of the house and its past, methods for the interface, services between packages. |
149
- | `/ui` | The props of a page, a card and an entity view; hooks for the package, its methods, its settings, the house, its areas, its past and its pictures, the viewer, the theme, its strings; navigation inside the package's page and the platform's. |
150
- | `/sun` | Where the sun is at any moment, by the platform's own formulas. |
151
- | `/components` | The part of the interface's design system a package may draw with. |
152
- | `/vite` | `nebula()`, the Vite plugin that builds both halves: the interface in chunks with its CSS scoped to the package, the server bundled whole. |
153
- | `/testing` | `createTestContext()`: run `start` in a unit test, every effect recorded. |
154
- | `/styles.css` | The interface's Tailwind tokens, to `@reference`. |
187
+ | Import | What |
188
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
189
+ | `@nebulahome/package-sdk` | `definePackage`, `defineSettings`, `defineCard`, `defineEntityType`, `definePage`, `defineContainer`, `defineCompose`, `field`, entity id helpers, `SDK_VERSION`, the value types. |
190
+ | `/server` | `defineServer`, and the context `start` is given: settings (live or restart), devices and entities, typed custom entities, the rest of the house and its past, methods and the live channel for the interface, services between packages. |
191
+ | `/ui` | The props of a page, a card and an entity view; hooks for the package, its methods and live topics, its settings, the house, its areas, its past and its pictures, the viewer, the theme, its strings; navigation inside the package's page and the platform's. |
192
+ | `/sun` | Where the sun is at any moment, by the platform's own formulas. |
193
+ | `/components` | The part of the interface's design system a package may draw with. |
194
+ | `/vite` | `nebula()`, the Vite plugin that builds both halves: the interface in chunks with its CSS scoped to the package, the server bundled whole. |
195
+ | `/testing` | `createTestContext()`: run `start` in a unit test, every effect recorded. |
196
+ | `/styles.css` | The interface's Tailwind tokens, to `@reference`. |
155
197
 
156
198
  The `nebulahome-package` command checks, builds and packs a package:
157
199
 
@@ -189,8 +231,9 @@ git tag package-sdk@<version> && git push origin package-sdk@<version>
189
231
  ```
190
232
 
191
233
  `.github/workflows/release-package-sdk.yml` checks the tag says the package's version, runs the
192
- SDK's gates and publishes a GitHub release with the archive npm will receive. The maintainer
193
- publishes that archive, signed in to npm:
234
+ SDK's gates and keeps the archive npm will receive as the run's artifact; it makes no GitHub
235
+ release, npm is where the SDK is published. The maintainer downloads that artifact from the run
236
+ and publishes it, signed in to npm:
194
237
 
195
238
  ```sh
196
239
  npm publish nebulahome-package-sdk-<version>.tgz
@@ -255,6 +255,25 @@ type RpcMethod = {
255
255
  readonly input: Schema<TInput>;
256
256
  }, handler: (input: TInput, call: RpcCall) => Awaitable<JsonValue | undefined>): void;
257
257
  };
258
+ /** How a live message is sent. Since 1.5. */
259
+ type LivePublishOptions = {
260
+ /**
261
+ * Kept by the platform, and handed at once to an interface that follows the topic later, until
262
+ * the next retained message on it replaces it: for the state a page draws. Off by default.
263
+ */
264
+ readonly isRetained?: boolean;
265
+ };
266
+ /** What a live message's handler is told besides the message: who sent it. Since 1.5. */
267
+ type LiveCall = {
268
+ readonly caller: RpcCaller;
269
+ };
270
+ /** Hearing one topic of the package's interfaces' live messages. Since 1.5. */
271
+ type LiveOnMessage = {
272
+ (topic: string, handler: (data: JsonValue, call: LiveCall) => Awaitable<void>): Unsubscribe;
273
+ <TInput>(topic: string, options: {
274
+ readonly input: Schema<TInput>;
275
+ }, handler: (data: TInput, call: LiveCall) => Awaitable<void>): Unsubscribe;
276
+ };
258
277
  /** Offering a service. */
259
278
  type ServiceProvide = {
260
279
  (name: string, handler: (input: unknown, call: ServiceCall) => Awaitable<JsonValue | undefined>): void;
@@ -403,6 +422,23 @@ type ServerContext<TSettings = undefined> = {
403
422
  /** Registers a method its interface calls; a name registered twice throws. */
404
423
  readonly method: RpcMethod;
405
424
  };
425
+ /**
426
+ * The live channel to its open interfaces, over the interface's own socket: no polling, no
427
+ * HTTP rate limit. Whoever may read packages hears it and may send to it. Since 1.5.
428
+ */
429
+ readonly live: {
430
+ /**
431
+ * Sends a message to every open interface following the topic. A topic is a letter, then
432
+ * letters, digits and `.`, `_`, `:`, `-` (128 at most); a message is at most 1 MB as JSON.
433
+ * One that is not is logged, not sent.
434
+ */
435
+ readonly publish: (topic: string, data: JsonValue, options?: LivePublishOptions) => void;
436
+ /**
437
+ * Hears what its interfaces send on a topic (`usePackageSend`); a topic heard twice throws.
438
+ * A message its schema refuses is logged and dropped: nobody waits for an answer.
439
+ */
440
+ readonly onMessage: LiveOnMessage;
441
+ };
406
442
  readonly services: {
407
443
  /** Answers a service other packages call; a name provided twice, or reserved, throws. */
408
444
  readonly provide: ServiceProvide;
@@ -411,4 +447,4 @@ type ServerContext<TSettings = undefined> = {
411
447
  };
412
448
  };
413
449
  //#endregion
414
- export { Schema as A, ServiceProvide as C, TelemetryRead as D, SyncResult as E, StandardResult as M, StandardSchemaV1 as N, Unsubscribe as O, ServiceCallFunction as S, StateChange as T, RpcCaller as _, CustomEntityInput as a, ServerSettings as b, EntityInput as c, HistoryRead as d, LogMethod as f, RpcCall as g, RpcAccess as h, CustomEntities as i, SchemaIssue as j, SafeParser as k, EntitySnapshot as l, ReportOptions as m, CapabilityEntityInput as n, DeviceInput as o, Logger as p, CustomCommand as r, EntityFilter as s, Awaitable as t, HistoryAnswer as u, RpcMethod as v, SettingsChange as w, ServiceCall as x, ServerContext as y };
450
+ export { TelemetryRead as A, ServerSettings as C, SettingsChange as D, ServiceProvide as E, StandardResult as F, StandardSchemaV1 as I, SafeParser as M, Schema as N, StateChange as O, SchemaIssue as P, ServerContext as S, ServiceCallFunction as T, ReportOptions as _, CustomEntityInput as a, RpcCaller as b, EntityInput as c, HistoryRead as d, LiveCall as f, Logger as g, LogMethod as h, CustomEntities as i, Unsubscribe as j, SyncResult as k, EntitySnapshot as l, LivePublishOptions as m, CapabilityEntityInput as n, DeviceInput as o, LiveOnMessage as p, CustomCommand as r, EntityFilter as s, Awaitable as t, HistoryAnswer as u, RpcAccess as v, ServiceCall as w, RpcMethod as x, RpcCall as y };
@@ -6850,7 +6850,7 @@ function composeFileProblems(text, settingKeys) {
6850
6850
  * the two are compared before a package is installed or started (PACKAGES_DOCS.md 11).
6851
6851
  */
6852
6852
  /** The SDK's own version, `1.0.0`. */
6853
- const SDK_VERSION = "1.3.0";
6853
+ const SDK_VERSION = "1.5.0";
6854
6854
  //#endregion
6855
6855
  //#region src/vite/catalogues.ts
6856
6856
  /**
@@ -7,6 +7,6 @@
7
7
  * the two are compared before a package is installed or started (PACKAGES_DOCS.md 11).
8
8
  */
9
9
  /** The SDK's own version, `1.0.0`. */
10
- const SDK_VERSION = "1.3.0";
10
+ const SDK_VERSION = "1.5.0";
11
11
  //#endregion
12
12
  export { SDK_VERSION as t };
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { i as SettingsDefinition } from "./chunks/json.js";
2
2
  import { a as TelemetryResolution, i as TelemetryPoint, n as HistorySeries, r as StateHistoryPoint, t as ChangeCause } from "./chunks/history.js";
3
- import { A as Schema, C as ServiceProvide, D as TelemetryRead, E as SyncResult, M as StandardResult, N as StandardSchemaV1, O as Unsubscribe, S as ServiceCallFunction, T as StateChange, _ as RpcCaller, a as CustomEntityInput, b as ServerSettings, c as EntityInput, d as HistoryRead, f as LogMethod, g as RpcCall, h as RpcAccess, i as CustomEntities, j as SchemaIssue, k as SafeParser, l as EntitySnapshot, m as ReportOptions, n as CapabilityEntityInput, o as DeviceInput, p as Logger, r as CustomCommand, s as EntityFilter, t as Awaitable, u as HistoryAnswer, v as RpcMethod, w as SettingsChange, x as ServiceCall, y as ServerContext } from "./chunks/context.js";
3
+ import { A as TelemetryRead, C as ServerSettings, D as SettingsChange, E as ServiceProvide, F as StandardResult, I as StandardSchemaV1, M as SafeParser, N as Schema, O as StateChange, P as SchemaIssue, S as ServerContext, T as ServiceCallFunction, _ as ReportOptions, a as CustomEntityInput, b as RpcCaller, c as EntityInput, d as HistoryRead, f as LiveCall, g as Logger, h as LogMethod, i as CustomEntities, j as Unsubscribe, k as SyncResult, l as EntitySnapshot, m as LivePublishOptions, n as CapabilityEntityInput, o as DeviceInput, p as LiveOnMessage, r as CustomCommand, s as EntityFilter, t as Awaitable, u as HistoryAnswer, v as RpcAccess, w as ServiceCall, x as RpcMethod, y as RpcCall } from "./chunks/context.js";
4
4
  //#region src/server/index.d.ts
5
5
  /** What stops what `start` opened. */
6
6
  export type Stop = () => Awaitable<void>;
@@ -28,4 +28,4 @@ export type ServerDefinition<TSettings extends SettingsDefinition | undefined =
28
28
  */
29
29
  export declare function defineServer<TSettings extends SettingsDefinition | undefined = undefined>(server: Omit<ServerDefinition<TSettings>, 'kind' | 'sdk'>): ServerDefinition<TSettings>;
30
30
  //#endregion
31
- export type { Awaitable, CapabilityEntityInput, ChangeCause, CustomCommand, CustomEntities, CustomEntityInput, DeviceInput, EntityFilter, EntityInput, EntitySnapshot, HistoryAnswer, HistoryRead, HistorySeries, LogMethod, Logger, ReportOptions, RpcAccess, RpcCall, RpcCaller, RpcMethod, SafeParser, Schema, SchemaIssue, ServerContext, ServerSettings, ServiceCall, ServiceCallFunction, ServiceProvide, SettingsChange, StandardResult, StandardSchemaV1, StateChange, StateHistoryPoint, SyncResult, TelemetryPoint, TelemetryRead, TelemetryResolution, Unsubscribe };
31
+ export type { Awaitable, CapabilityEntityInput, ChangeCause, CustomCommand, CustomEntities, CustomEntityInput, DeviceInput, EntityFilter, EntityInput, EntitySnapshot, HistoryAnswer, HistoryRead, HistorySeries, LiveCall, LiveOnMessage, LivePublishOptions, LogMethod, Logger, ReportOptions, RpcAccess, RpcCall, RpcCaller, RpcMethod, SafeParser, Schema, SchemaIssue, ServerContext, ServerSettings, ServiceCall, ServiceCallFunction, ServiceProvide, SettingsChange, StandardResult, StandardSchemaV1, StateChange, StateHistoryPoint, SyncResult, TelemetryPoint, TelemetryRead, TelemetryResolution, Unsubscribe };
package/dist/styles.css CHANGED
@@ -276,6 +276,11 @@
276
276
  --text-display--line-height: 1.15;
277
277
  --text-display--letter-spacing: -0.03em;
278
278
 
279
+ /** The title of the screen every page shows while Nebula Home restarts or updates. */
280
+ --text-display-lg: 40px;
281
+ --text-display-lg--line-height: 1.1;
282
+ --text-display-lg--letter-spacing: -0.035em;
283
+
279
284
  /** The number an entity card is about; its unit follows at half size in `foreground-subtle`. */
280
285
  --text-reading: 32px;
281
286
  --text-reading--line-height: 1;
@@ -384,6 +389,92 @@
384
389
  * the dashes go by `animation-direction`.
385
390
  */
386
391
  --animate-energy-flow: energy-flow 1.6s linear infinite;
392
+
393
+ /*
394
+ * Nebula Home restarting or updating itself (boards N-Restart, N-Update, N-Transition-Flow): the
395
+ * ring turns slowly and unevenly while the core breathes out of phase, rings ripple out from the
396
+ * mark, its glow swells, a bar sweeps or shimmers, and the screen arrives out of a blur.
397
+ */
398
+ --animate-orbit-work: orbit 2.4s cubic-bezier(0.55, 0.1, 0.35, 0.9) infinite;
399
+ --animate-breathe: breathe 2.4s ease-in-out infinite;
400
+ --animate-ripple: ripple 2.4s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
401
+ --animate-glow: glow 3.6s ease-in-out infinite;
402
+ --animate-sweep: sweep 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
403
+ --animate-shimmer: shimmer 2s linear infinite;
404
+ --animate-enter: enter 0.8s cubic-bezier(0.2, 0.7, 0.2, 1) both;
405
+ --animate-arrive: arrive 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) both;
406
+ }
407
+
408
+ /* The core of the mark breathing while Nebula Home works on itself. */
409
+ @keyframes breathe {
410
+ 0%,
411
+ 100% {
412
+ transform: scale(1);
413
+ opacity: 1;
414
+ }
415
+ 50% {
416
+ transform: scale(0.86);
417
+ opacity: 0.75;
418
+ }
419
+ }
420
+
421
+ /* A ring leaving the mark and fading as it widens. */
422
+ @keyframes ripple {
423
+ from {
424
+ transform: scale(0.55);
425
+ opacity: 0.55;
426
+ }
427
+ to {
428
+ transform: scale(1.9);
429
+ opacity: 0;
430
+ }
431
+ }
432
+
433
+ @keyframes glow {
434
+ 0%,
435
+ 100% {
436
+ opacity: 0.45;
437
+ }
438
+ 50% {
439
+ opacity: 0.8;
440
+ }
441
+ }
442
+
443
+ /* A bar that cannot say how far it is: a highlight crossing it again and again. */
444
+ @keyframes sweep {
445
+ from {
446
+ left: -40%;
447
+ }
448
+ to {
449
+ left: 100%;
450
+ }
451
+ }
452
+
453
+ /* A light passing along a bar that is moving. */
454
+ @keyframes shimmer {
455
+ from {
456
+ background-position: -200% 0;
457
+ }
458
+ to {
459
+ background-position: 200% 0;
460
+ }
461
+ }
462
+
463
+ /* A screen arriving out of a blur, a little small. */
464
+ @keyframes enter {
465
+ from {
466
+ opacity: 0;
467
+ transform: scale(0.96);
468
+ filter: blur(10px);
469
+ }
470
+ }
471
+
472
+ /* A note rising into place. */
473
+ @keyframes arrive {
474
+ from {
475
+ opacity: 0;
476
+ transform: translateY(10px);
477
+ }
387
478
  }
388
479
 
389
480
  /* One period of a wire's dash pattern, which is 12 units long: 2 drawn, 10 gap. */
@@ -566,6 +657,47 @@
566
657
  * The Matter mark's square, lit violet from its top-left corner: the broker's hero and its
567
658
  * dialogs' lead. Over the square's own ground, so it sits on `bg-surface-raised`.
568
659
  */
660
+ @utility modbus-glow {
661
+ background-image: radial-gradient(
662
+ 120% 120% at 0% 0%,
663
+ color-mix(in srgb, var(--color-cyan) 18%, transparent),
664
+ transparent 60%
665
+ );
666
+ }
667
+
668
+ /* The newer brokers' squares, each lit in its own mark's colour the same way. */
669
+ @utility thread-glow {
670
+ background-image: radial-gradient(
671
+ 120% 120% at 0% 0%,
672
+ color-mix(in srgb, var(--color-thread) 20%, transparent),
673
+ transparent 60%
674
+ );
675
+ }
676
+
677
+ @utility bluetooth-glow {
678
+ background-image: radial-gradient(
679
+ 120% 120% at 0% 0%,
680
+ color-mix(in srgb, #5b9dff 20%, transparent),
681
+ transparent 60%
682
+ );
683
+ }
684
+
685
+ @utility serial-glow {
686
+ background-image: radial-gradient(
687
+ 120% 120% at 0% 0%,
688
+ color-mix(in srgb, #f0c27b 18%, transparent),
689
+ transparent 60%
690
+ );
691
+ }
692
+
693
+ @utility infrared-glow {
694
+ background-image: radial-gradient(
695
+ 120% 120% at 0% 0%,
696
+ color-mix(in srgb, #f7895c 18%, transparent),
697
+ transparent 60%
698
+ );
699
+ }
700
+
569
701
  @utility matter-glow {
570
702
  background-image: radial-gradient(
571
703
  120% 120% at 0% 0%,
package/dist/testing.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { V as CapabilityCommand, et as ReportedCapabilityState, i as SettingsDefinition, lt as EntityId, r as JsonValue, t as JsonObject } from "./chunks/json.js";
2
2
  import { i as TelemetryPoint, r as StateHistoryPoint } from "./chunks/history.js";
3
- import { T as StateChange, _ as RpcCaller, l as EntitySnapshot, m as ReportOptions, o as DeviceInput, t as Awaitable, y as ServerContext } from "./chunks/context.js";
3
+ import { O as StateChange, S as ServerContext, _ as ReportOptions, b as RpcCaller, l as EntitySnapshot, o as DeviceInput, t as Awaitable } from "./chunks/context.js";
4
4
  //#region src/testing/index.d.ts
5
5
  /** What a test context starts with. */
6
6
  export type TestContextOptions<TSettings extends SettingsDefinition | undefined> = {
@@ -30,6 +30,12 @@ export type RecordedReport = {
30
30
  readonly state: ReportedCapabilityState;
31
31
  readonly options?: ReportOptions;
32
32
  };
33
+ /** One live message the package sent its interfaces. */
34
+ export type RecordedLiveMessage = {
35
+ readonly topic: string;
36
+ readonly data: JsonValue;
37
+ readonly isRetained: boolean;
38
+ };
33
39
  /** One line the package logged. */
34
40
  export type RecordedLog = {
35
41
  readonly level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
@@ -55,6 +61,17 @@ export type TestContext<TSettings extends SettingsDefinition | undefined> = {
55
61
  };
56
62
  /** Every line logged. */
57
63
  readonly logs: readonly RecordedLog[];
64
+ /** Every live message published and sent, in order. */
65
+ readonly published: readonly RecordedLiveMessage[];
66
+ /**
67
+ * Sends the package a live message as its interface would, through the topic's schema.
68
+ *
69
+ * @param topic - The topic.
70
+ * @param data - The message.
71
+ * @param caller - Who sends; a test user when not said.
72
+ * @returns Once handled; rejects when nothing hears the topic or its schema refuses it.
73
+ */
74
+ readonly sendLive: (topic: string, data: JsonValue, caller?: RpcCaller) => Promise<void>;
58
75
  /**
59
76
  * Routes a command as the platform would: to the entity type's handler for a custom one, to
60
77
  * `devices.onCommand` otherwise.
package/dist/testing.js CHANGED
@@ -99,6 +99,9 @@ const TEST_CALLER = {
99
99
  username: "test",
100
100
  role: "owner"
101
101
  };
102
+ /** A live topic, and the most a live message may carry as JSON, as the platform has them. */
103
+ const LIVE_TOPIC_PATTERN = /^[a-z][a-zA-Z0-9._:-]{0,127}$/;
104
+ const LIVE_MAX_BYTES = 1e6;
102
105
  /**
103
106
  * Says whether a value is a record.
104
107
  *
@@ -149,6 +152,8 @@ function createTestContext(options = {}) {
149
152
  const methods = /* @__PURE__ */ new Map();
150
153
  const services = /* @__PURE__ */ new Map();
151
154
  const answers = /* @__PURE__ */ new Map();
155
+ const published = [];
156
+ const liveHandlers = /* @__PURE__ */ new Map();
152
157
  const customHandlers = /* @__PURE__ */ new Map();
153
158
  const settingsHandlers = /* @__PURE__ */ new Set();
154
159
  const subscriptions = /* @__PURE__ */ new Set();
@@ -309,6 +314,68 @@ function createTestContext(options = {}) {
309
314
  });
310
315
  }
311
316
  /**
317
+ * Hears a live topic.
318
+ *
319
+ * @param topic - The topic.
320
+ * @param optionsOrHandler - What a message takes, or what to do with one.
321
+ * @param handler - What to do with one, after its schema.
322
+ * @returns What stops it hearing.
323
+ * @throws {Error} When the topic is not one, is heard already, or there is no handler.
324
+ */
325
+ function onLiveMessage(topic, optionsOrHandler, handler) {
326
+ /**
327
+ * Says whether a message is the topic's: its schema checked it, or it takes any.
328
+ *
329
+ * @param data - The message.
330
+ * @returns True.
331
+ */
332
+ const isInput = (data) => data === data;
333
+ const hear = typeof optionsOrHandler === "function" ? optionsOrHandler : handler;
334
+ if (!LIVE_TOPIC_PATTERN.test(topic)) throw new Error(`${topic} is not a live topic: a letter first, then letters, digits, .:_-`);
335
+ if (liveHandlers.has(topic)) throw new Error(`The live topic ${topic} is heard already`);
336
+ if (hear === void 0) throw new Error(`The live topic ${topic} has no handler`);
337
+ liveHandlers.set(topic, {
338
+ input: typeof optionsOrHandler === "function" ? void 0 : optionsOrHandler.input,
339
+ /**
340
+ * Hands the handler the message, once checked.
341
+ *
342
+ * @param data - The message.
343
+ * @param call - Who sent it.
344
+ * @returns Once handled.
345
+ * @throws {Error} Never: every message is the topic's.
346
+ */
347
+ handler: (data, call) => {
348
+ if (!isInput(data)) throw new Error(`${topic} was handed what it does not take`);
349
+ return hear(data, call);
350
+ }
351
+ });
352
+ return () => {
353
+ liveHandlers.delete(topic);
354
+ };
355
+ }
356
+ /**
357
+ * Publishes a live message, or logs why it cannot, as the platform does.
358
+ *
359
+ * @param topic - The topic.
360
+ * @param data - The message.
361
+ * @param options - Whether it is retained.
362
+ */
363
+ const publishLive = (topic, data, options = {}) => {
364
+ if (!LIVE_TOPIC_PATTERN.test(topic)) {
365
+ ctx.logger.error({ topic }, "A live message was not sent: that is not a topic");
366
+ return;
367
+ }
368
+ if (new TextEncoder().encode(JSON.stringify(data)).length > LIVE_MAX_BYTES) {
369
+ ctx.logger.error({ topic }, "A live message was not sent: it is larger than 1 MB");
370
+ return;
371
+ }
372
+ published.push({
373
+ topic,
374
+ data,
375
+ isRetained: options.isRetained === true
376
+ });
377
+ };
378
+ /**
312
379
  * Calls a service.
313
380
  *
314
381
  * @param target - The package or service.
@@ -329,215 +396,220 @@ function createTestContext(options = {}) {
329
396
  if (!checked.isValid) throw new Error(`${target}.${name} answered what was not asked: ${checked.message}`);
330
397
  return checked.value;
331
398
  }
332
- return {
333
- ctx: {
334
- id,
335
- version: options.version ?? "0.0.0",
336
- platform: { version: "0.0.0-test" },
337
- sdk: { version: SDK_VERSION },
399
+ const ctx = {
400
+ id,
401
+ version: options.version ?? "0.0.0",
402
+ platform: { version: "0.0.0-test" },
403
+ sdk: { version: SDK_VERSION },
404
+ /**
405
+ * The settings as they stand.
406
+ *
407
+ * @returns Them.
408
+ */
409
+ get settings() {
410
+ return settingsNow();
411
+ },
412
+ /**
413
+ * Hears live changes of the settings.
414
+ *
415
+ * @param handler - What hears them.
416
+ * @returns What stops it hearing.
417
+ */
418
+ onSettingsChange: (handler) => {
419
+ settingsHandlers.add(handler);
420
+ return () => {
421
+ settingsHandlers.delete(handler);
422
+ };
423
+ },
424
+ dataDir: options.dataDir ?? `/tmp/${id}/data`,
425
+ cacheDir: options.cacheDir ?? `/tmp/${id}/cache`,
426
+ signal: abort.signal,
427
+ logger: loggerWith({}),
428
+ /**
429
+ * One of the package's entities' ids.
430
+ *
431
+ * @param domain - Its domain.
432
+ * @param objectId - What follows the prefix.
433
+ * @returns The id.
434
+ */
435
+ entityId: (domain, objectId) => `${domain}.${prefix}${objectId}`,
436
+ objectIdOf,
437
+ devices: {
338
438
  /**
339
- * The settings as they stand.
439
+ * Records the devices, and answers as the platform would.
340
440
  *
341
- * @returns Them.
441
+ * @param devices - Every device.
442
+ * @returns What was written.
342
443
  */
343
- get settings() {
344
- return settingsNow();
444
+ sync: async (devices) => {
445
+ synced = devices;
446
+ return Promise.resolve({
447
+ devices: devices.map((device) => ({
448
+ deviceId: `device-${device.externalId}`,
449
+ externalId: device.externalId,
450
+ entityIds: device.entities.map((entity) => `${"type" in entity ? "custom" : entity.domain}.${prefix}${entity.objectId}`)
451
+ })),
452
+ refused: [],
453
+ kept: []
454
+ });
345
455
  },
346
456
  /**
347
- * Hears live changes of the settings.
457
+ * Records a report.
348
458
  *
349
- * @param handler - What hears them.
350
- * @returns What stops it hearing.
459
+ * @param entityId - The entity.
460
+ * @param state - Its state.
461
+ * @param reportOptions - Its attributes and availability.
462
+ */
463
+ report: (entityId, state, reportOptions) => {
464
+ reports.push({
465
+ entityId,
466
+ state,
467
+ ...reportOptions === void 0 ? {} : { options: reportOptions }
468
+ });
469
+ },
470
+ /**
471
+ * Takes the handler of commands.
472
+ *
473
+ * @param handler - The handler.
474
+ * @returns What removes it.
475
+ * @throws {Error} When there is one already.
351
476
  */
352
- onSettingsChange: (handler) => {
353
- settingsHandlers.add(handler);
477
+ onCommand: (handler) => {
478
+ if (commandHandler !== void 0) throw new Error("devices.onCommand has a handler already");
479
+ commandHandler = handler;
354
480
  return () => {
355
- settingsHandlers.delete(handler);
481
+ commandHandler = void 0;
356
482
  };
357
- },
358
- dataDir: options.dataDir ?? `/tmp/${id}/data`,
359
- cacheDir: options.cacheDir ?? `/tmp/${id}/cache`,
360
- signal: abort.signal,
361
- logger: loggerWith({}),
483
+ }
484
+ },
485
+ /**
486
+ * The entities of one of the package's entity types.
487
+ *
488
+ * @param type - The entity type.
489
+ * @returns Their ids, reports and commands.
490
+ */
491
+ custom: (type) => ({
362
492
  /**
363
- * One of the package's entities' ids.
493
+ * The id of one of its entities of this type.
364
494
  *
365
- * @param domain - Its domain.
366
495
  * @param objectId - What follows the prefix.
367
496
  * @returns The id.
368
497
  */
369
- entityId: (domain, objectId) => `${domain}.${prefix}${objectId}`,
370
- objectIdOf,
371
- devices: {
372
- /**
373
- * Records the devices, and answers as the platform would.
374
- *
375
- * @param devices - Every device.
376
- * @returns What was written.
377
- */
378
- sync: async (devices) => {
379
- synced = devices;
380
- return Promise.resolve({
381
- devices: devices.map((device) => ({
382
- deviceId: `device-${device.externalId}`,
383
- externalId: device.externalId,
384
- entityIds: device.entities.map((entity) => `${"type" in entity ? "custom" : entity.domain}.${prefix}${entity.objectId}`)
385
- })),
386
- refused: [],
387
- kept: []
388
- });
389
- },
390
- /**
391
- * Records a report.
392
- *
393
- * @param entityId - The entity.
394
- * @param state - Its state.
395
- * @param reportOptions - Its attributes and availability.
396
- */
397
- report: (entityId, state, reportOptions) => {
398
- reports.push({
399
- entityId,
400
- state,
401
- ...reportOptions === void 0 ? {} : { options: reportOptions }
402
- });
403
- },
404
- /**
405
- * Takes the handler of commands.
406
- *
407
- * @param handler - The handler.
408
- * @returns What removes it.
409
- * @throws {Error} When there is one already.
410
- */
411
- onCommand: (handler) => {
412
- if (commandHandler !== void 0) throw new Error("devices.onCommand has a handler already");
413
- commandHandler = handler;
414
- return () => {
415
- commandHandler = void 0;
416
- };
417
- }
418
- },
498
+ entityId: (objectId) => `custom.${prefix}${objectId}`,
419
499
  /**
420
- * The entities of one of the package's entity types.
500
+ * Records a report of an entity's values.
421
501
  *
422
- * @param type - The entity type.
423
- * @returns Their ids, reports and commands.
502
+ * @param entityId - The entity.
503
+ * @param entityValues - Its values.
504
+ * @param reportOptions - Its attributes and availability.
424
505
  */
425
- custom: (type) => ({
426
- /**
427
- * The id of one of its entities of this type.
428
- *
429
- * @param objectId - What follows the prefix.
430
- * @returns The id.
431
- */
432
- entityId: (objectId) => `custom.${prefix}${objectId}`,
433
- /**
434
- * Records a report of an entity's values.
435
- *
436
- * @param entityId - The entity.
437
- * @param entityValues - Its values.
438
- * @param reportOptions - Its attributes and availability.
439
- */
440
- report: (entityId, entityValues, reportOptions) => {
441
- reports.push({
442
- entityId,
443
- state: { custom: { values: Object.fromEntries(Object.entries(entityValues).filter((entry) => isCustomValue(entry[1]))) } },
444
- ...reportOptions === void 0 ? {} : { options: reportOptions }
445
- });
446
- },
447
- /**
448
- * Takes the handler of one of the type's commands.
449
- *
450
- * @param command - The command's id.
451
- * @param handler - The handler.
452
- * @returns What removes it.
453
- * @throws {Error} When the command has one already.
454
- */
455
- onCommand: (command, handler) => {
456
- const key = `${type.id}:${command}`;
457
- /**
458
- * Says whether a command's arguments are its own: the platform checked them against
459
- * the entity type before routing it.
460
- *
461
- * @param args - The arguments.
462
- * @returns True for a record.
463
- */
464
- const isArgs = (args) => isRecord(args);
465
- if (customHandlers.has(key)) throw new Error(`${type.id} has a handler for ${command} already`);
466
- customHandlers.set(key, (call) => {
467
- if (!isArgs(call.args)) throw new Error(`${command} was sent without arguments`);
468
- return handler({
469
- entityId: call.entityId,
470
- objectId: call.objectId,
471
- args: call.args
472
- });
473
- });
474
- return () => {
475
- customHandlers.delete(key);
476
- };
477
- }
478
- }),
479
- entities: {
480
- /**
481
- * One entity of the house.
482
- *
483
- * @param entityId - The entity.
484
- * @returns It, or undefined.
485
- */
486
- get: async (entityId) => Promise.resolve(entities.get(entityId)),
487
- /**
488
- * The entities a filter names.
489
- *
490
- * @param filter - Which.
491
- * @returns Them.
492
- */
493
- list: async (filter = {}) => Promise.resolve([...entities.values()].filter((each) => matches(filter, each.entityId))),
494
- /**
495
- * Hears changes of the entities a filter names.
496
- *
497
- * @param filter - Which.
498
- * @param handler - What hears them.
499
- * @returns What stops it hearing.
500
- */
501
- subscribe: (filter, handler) => {
502
- const subscription = {
503
- filter,
504
- handler
505
- };
506
- subscriptions.add(subscription);
507
- return () => {
508
- subscriptions.delete(subscription);
509
- };
510
- }
506
+ report: (entityId, entityValues, reportOptions) => {
507
+ reports.push({
508
+ entityId,
509
+ state: { custom: { values: Object.fromEntries(Object.entries(entityValues).filter((entry) => isCustomValue(entry[1]))) } },
510
+ ...reportOptions === void 0 ? {} : { options: reportOptions }
511
+ });
511
512
  },
512
- history: {
513
- /**
514
- * The changes of some entities in a window.
515
- *
516
- * @param read - Which, when, how many.
517
- * @returns Them, oldest first.
518
- */
519
- states: async (read) => Promise.resolve(readWindow(read, (entityId) => histories.get(entityId)?.states ?? [])),
513
+ /**
514
+ * Takes the handler of one of the type's commands.
515
+ *
516
+ * @param command - The command's id.
517
+ * @param handler - The handler.
518
+ * @returns What removes it.
519
+ * @throws {Error} When the command has one already.
520
+ */
521
+ onCommand: (command, handler) => {
522
+ const key = `${type.id}:${command}`;
520
523
  /**
521
- * The readings of some entities in a window, as given: a test gives the buckets it wants.
524
+ * Says whether a command's arguments are its own: the platform checked them against
525
+ * the entity type before routing it.
522
526
  *
523
- * @param read - Which, when, how many, at which resolution.
524
- * @returns Them, oldest first, and the resolution.
527
+ * @param args - The arguments.
528
+ * @returns True for a record.
525
529
  */
526
- telemetry: async (read) => {
527
- const span = new Date(read.to).getTime() - new Date(read.from).getTime();
528
- const resolution = read.resolution === void 0 || read.resolution === "auto" ? resolutionFor(span) : read.resolution;
529
- return Promise.resolve({
530
- ...readWindow(read, (entityId) => histories.get(entityId)?.telemetry ?? []),
531
- resolution
530
+ const isArgs = (args) => isRecord(args);
531
+ if (customHandlers.has(key)) throw new Error(`${type.id} has a handler for ${command} already`);
532
+ customHandlers.set(key, (call) => {
533
+ if (!isArgs(call.args)) throw new Error(`${command} was sent without arguments`);
534
+ return handler({
535
+ entityId: call.entityId,
536
+ objectId: call.objectId,
537
+ args: call.args
532
538
  });
533
- }
534
- },
535
- rpc: { method: rpcMethod },
536
- services: {
537
- provide,
538
- call: callService
539
+ });
540
+ return () => {
541
+ customHandlers.delete(key);
542
+ };
539
543
  }
544
+ }),
545
+ entities: {
546
+ /**
547
+ * One entity of the house.
548
+ *
549
+ * @param entityId - The entity.
550
+ * @returns It, or undefined.
551
+ */
552
+ get: async (entityId) => Promise.resolve(entities.get(entityId)),
553
+ /**
554
+ * The entities a filter names.
555
+ *
556
+ * @param filter - Which.
557
+ * @returns Them.
558
+ */
559
+ list: async (filter = {}) => Promise.resolve([...entities.values()].filter((each) => matches(filter, each.entityId))),
560
+ /**
561
+ * Hears changes of the entities a filter names.
562
+ *
563
+ * @param filter - Which.
564
+ * @param handler - What hears them.
565
+ * @returns What stops it hearing.
566
+ */
567
+ subscribe: (filter, handler) => {
568
+ const subscription = {
569
+ filter,
570
+ handler
571
+ };
572
+ subscriptions.add(subscription);
573
+ return () => {
574
+ subscriptions.delete(subscription);
575
+ };
576
+ }
577
+ },
578
+ history: {
579
+ /**
580
+ * The changes of some entities in a window.
581
+ *
582
+ * @param read - Which, when, how many.
583
+ * @returns Them, oldest first.
584
+ */
585
+ states: async (read) => Promise.resolve(readWindow(read, (entityId) => histories.get(entityId)?.states ?? [])),
586
+ /**
587
+ * The readings of some entities in a window, as given: a test gives the buckets it wants.
588
+ *
589
+ * @param read - Which, when, how many, at which resolution.
590
+ * @returns Them, oldest first, and the resolution.
591
+ */
592
+ telemetry: async (read) => {
593
+ const span = new Date(read.to).getTime() - new Date(read.from).getTime();
594
+ const resolution = read.resolution === void 0 || read.resolution === "auto" ? resolutionFor(span) : read.resolution;
595
+ return Promise.resolve({
596
+ ...readWindow(read, (entityId) => histories.get(entityId)?.telemetry ?? []),
597
+ resolution
598
+ });
599
+ }
600
+ },
601
+ rpc: { method: rpcMethod },
602
+ live: {
603
+ publish: publishLive,
604
+ onMessage: onLiveMessage
540
605
  },
606
+ services: {
607
+ provide,
608
+ call: callService
609
+ }
610
+ };
611
+ return {
612
+ ctx,
541
613
  /**
542
614
  * The last list of devices.
543
615
  *
@@ -555,6 +627,26 @@ function createTestContext(options = {}) {
555
627
  last: (entityId) => reports.findLast((each) => each.entityId === entityId)?.state
556
628
  },
557
629
  logs,
630
+ published,
631
+ /**
632
+ * Sends the package a live message.
633
+ *
634
+ * @param topic - The topic.
635
+ * @param data - The message.
636
+ * @param caller - Who sends.
637
+ * @throws {Error} When nothing hears the topic, or its schema refuses the message.
638
+ */
639
+ sendLive: async (topic, data, caller = TEST_CALLER) => {
640
+ const heard = liveHandlers.get(topic);
641
+ if (heard === void 0) throw new Error(`Nothing hears the live topic ${topic}`);
642
+ if (heard.input === void 0) {
643
+ await heard.handler(data, { caller });
644
+ return;
645
+ }
646
+ const checked = await check(heard.input, data);
647
+ if (!checked.isValid) throw new Error(`${topic} refused its message: ${checked.message}`);
648
+ await heard.handler(checked.value, { caller });
649
+ },
558
650
  /**
559
651
  * Routes a command.
560
652
  *
package/dist/ui.d.ts CHANGED
@@ -164,6 +164,23 @@ type RpcOptions = {
164
164
  /** False holds the read back. */
165
165
  readonly isEnabled?: boolean;
166
166
  };
167
+ /** How a live topic is followed. Since 1.5. */
168
+ type LiveOptions = {
169
+ /** False stops following it. */
170
+ readonly isEnabled?: boolean;
171
+ };
172
+ /** A live topic of the package's server, as it stands. Since 1.5. */
173
+ type LiveState<TData> = {
174
+ /** The last message on it: the retained one at first, if there is one; undefined until then. */
175
+ readonly data: TData | undefined;
176
+ /** Whether the interface's socket is open: messages sent while it is not are lost. */
177
+ readonly isConnected: boolean;
178
+ };
179
+ /**
180
+ * Sends the package's server a message on a topic (`ctx.live.onMessage`), over the interface's
181
+ * socket; nothing answers it. Since 1.5.
182
+ */
183
+ type PackageSend = (topic: string, data: JsonValue) => void;
167
184
  /** A call of one of the package's methods, on demand. */
168
185
  type PackageCall = <TData = JsonValue>(method: string, input?: JsonValue) => Promise<TData>;
169
186
  /** How a mutation is made. */
@@ -298,6 +315,32 @@ type PlatformLinkProps = LinkAttributes & {
298
315
  type PageActionsProps = {
299
316
  readonly children?: ReactNode;
300
317
  };
318
+ /** What a toast says. Since 1.4. */
319
+ type ToastMessage = {
320
+ /** A few words, no full stop. */
321
+ readonly title: string;
322
+ /** One or two lines. */
323
+ readonly message?: string;
324
+ /** The one thing to do about it; doing it closes the toast. */
325
+ readonly action?: {
326
+ readonly label: string;
327
+ readonly onSelect: () => void;
328
+ };
329
+ };
330
+ /**
331
+ * Shows one of the interface's toasts, one function per kind: bottom right, three at a time, the
332
+ * rest waiting their turn; the same toast twice is shown once. Since 1.4.
333
+ */
334
+ type Toasts = {
335
+ /** Confirms what was just done; stays 4 s. */
336
+ readonly success: (message: ToastMessage) => void;
337
+ /** Something that needs a look, not right now; stays 8 s. */
338
+ readonly warning: (message: ToastMessage) => void;
339
+ /** Something that failed; stays 10 s. */
340
+ readonly error: (message: ToastMessage) => void;
341
+ /** Something that started or changed; stays 6 s. */
342
+ readonly info: (message: ToastMessage) => void;
343
+ };
301
344
  /** What the interface lends to implement `/ui`. */
302
345
  type UiHost = {
303
346
  readonly usePackage: () => PackageInfo;
@@ -332,6 +375,12 @@ type UiHost = {
332
375
  readonly PageActions: ComponentType<PageActionsProps>;
333
376
  /** Since 1.3. */
334
377
  readonly useAreaActions: () => AreaActions;
378
+ /** Since 1.4. */
379
+ readonly useToast: () => Toasts;
380
+ /** Since 1.5. */
381
+ readonly usePackageLive: <TData = JsonValue>(topic: string, options?: LiveOptions) => LiveState<TData>;
382
+ /** Since 1.5. */
383
+ readonly usePackageSend: () => PackageSend;
335
384
  };
336
385
  //#endregion
337
386
  //#region src/ui/index.d.ts
@@ -454,6 +503,30 @@ export declare function useAreas(): readonly AreaView[];
454
503
  * @returns What changes the areas.
455
504
  */
456
505
  export declare function useAreaActions(): AreaActions;
506
+ /**
507
+ * Shows the interface's toasts: the outcome of something the viewer just did - "Saved", "Couldn't
508
+ * reach the printer" - from a handler, a mutation's `onError`, wherever. Since 1.4.
509
+ *
510
+ * @returns One function per kind: `success`, `warning`, `error`, `info`.
511
+ */
512
+ export declare function useToast(): Toasts;
513
+ /**
514
+ * Follows a live topic of the package's server (`ctx.live.publish`), over the interface's own
515
+ * socket: the retained message at once, then every message as it is sent, with no polling and
516
+ * no HTTP rate limit. Every component following one topic shares one subscription. Since 1.5.
517
+ *
518
+ * @param topic - The topic.
519
+ * @param options - Whether to follow it.
520
+ * @returns The last message, and whether the socket is open.
521
+ */
522
+ export declare function usePackageLive<TData = JsonValue>(topic: string, options?: LiveOptions): LiveState<TData>;
523
+ /**
524
+ * Sends the package's server messages over the interface's socket (`ctx.live.onMessage`): for
525
+ * what goes often and wants no answer. A call that wants one stays `usePackageCall`. Since 1.5.
526
+ *
527
+ * @returns What sends one.
528
+ */
529
+ export declare function usePackageSend(): PackageSend;
457
530
  /**
458
531
  * A camera's latest still, fetched again every so often, or an image entity's picture, fetched
459
532
  * again when it changes: an address an `<img>` shows, let go of when a newer one replaces it.
@@ -516,4 +589,4 @@ export declare function PlatformLink(props: PlatformLinkProps): ReactElement;
516
589
  */
517
590
  export declare function PageActions(props: PageActionsProps): ReactElement;
518
591
  //#endregion
519
- export type { AreaActions, AreaDraft, AreaKind, AreaView, CardProps, ChangeCause, CustomEntityState, EntitiesFilter, EntityPicture, EntityView, EntityViewProps, History, HistoryQuery, HistorySeries, HistoryWindow, Locale, Mutation, MutationOptions, PackageCall, PackageInfo, PackageLinkProps, PackageNavigate, PackageRoute, PackageSettings, PageActionsProps, PageProps, PictureOptions, PlatformLinkProps, PlatformNavigate, PlatformTarget, PluralEnglish, RpcOptions, RpcState, SendCommand, SettingsPatch, StateHistoryPoint, Telemetry, TelemetryPoint, TelemetryQuery, TelemetryResolution, Theme, Translate, Translation, TranslationValues, UiHost, Viewer, VisibleSettings };
592
+ export type { AreaActions, AreaDraft, AreaKind, AreaView, CardProps, ChangeCause, CustomEntityState, EntitiesFilter, EntityPicture, EntityView, EntityViewProps, History, HistoryQuery, HistorySeries, HistoryWindow, LiveOptions, LiveState, Locale, Mutation, MutationOptions, PackageCall, PackageInfo, PackageLinkProps, PackageNavigate, PackageRoute, PackageSend, PackageSettings, PageActionsProps, PageProps, PictureOptions, PlatformLinkProps, PlatformNavigate, PlatformTarget, PluralEnglish, RpcOptions, RpcState, SendCommand, SettingsPatch, StateHistoryPoint, Telemetry, TelemetryPoint, TelemetryQuery, TelemetryResolution, Theme, ToastMessage, Toasts, Translate, Translation, TranslationValues, UiHost, Viewer, VisibleSettings };
package/dist/ui.js CHANGED
@@ -212,6 +212,36 @@ function useAreaActions() {
212
212
  return ui("useAreaActions", "1.3")();
213
213
  }
214
214
  /**
215
+ * Shows the interface's toasts: the outcome of something the viewer just did - "Saved", "Couldn't
216
+ * reach the printer" - from a handler, a mutation's `onError`, wherever. Since 1.4.
217
+ *
218
+ * @returns One function per kind: `success`, `warning`, `error`, `info`.
219
+ */
220
+ function useToast() {
221
+ return ui("useToast", "1.4")();
222
+ }
223
+ /**
224
+ * Follows a live topic of the package's server (`ctx.live.publish`), over the interface's own
225
+ * socket: the retained message at once, then every message as it is sent, with no polling and
226
+ * no HTTP rate limit. Every component following one topic shares one subscription. Since 1.5.
227
+ *
228
+ * @param topic - The topic.
229
+ * @param options - Whether to follow it.
230
+ * @returns The last message, and whether the socket is open.
231
+ */
232
+ function usePackageLive(topic, options) {
233
+ return ui("usePackageLive", "1.5")(topic, options);
234
+ }
235
+ /**
236
+ * Sends the package's server messages over the interface's socket (`ctx.live.onMessage`): for
237
+ * what goes often and wants no answer. A call that wants one stays `usePackageCall`. Since 1.5.
238
+ *
239
+ * @returns What sends one.
240
+ */
241
+ function usePackageSend() {
242
+ return ui("usePackageSend", "1.5")();
243
+ }
244
+ /**
215
245
  * A camera's latest still, fetched again every so often, or an image entity's picture, fetched
216
246
  * again when it changes: an address an `<img>` shows, let go of when a newer one replaces it.
217
247
  *
@@ -291,4 +321,4 @@ function PageActions(props) {
291
321
  return createElement(ui("PageActions", "1.3"), props);
292
322
  }
293
323
  //#endregion
294
- export { PackageLink, PageActions, PlatformLink, useAreaActions, useAreas, useCommand, useCustomEntity, useEntities, useEntity, useEntityPicture, useLocale, usePackage, usePackageCall, usePackageMutation, usePackageNavigate, usePackageRoute, usePackageRpc, usePackageSettings, usePlatformNavigate, usePortalContainer, useStateHistory, useTelemetry, useTheme, useTranslation, useViewer };
324
+ export { PackageLink, PageActions, PlatformLink, useAreaActions, useAreas, useCommand, useCustomEntity, useEntities, useEntity, useEntityPicture, useLocale, usePackage, usePackageCall, usePackageLive, usePackageMutation, usePackageNavigate, usePackageRoute, usePackageRpc, usePackageSend, usePackageSettings, usePlatformNavigate, usePortalContainer, useStateHistory, useTelemetry, useTheme, useToast, useTranslation, useViewer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulahome/package-sdk",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Write a NebulaHome package: declare it, build it, and run it on the platform.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -100,10 +100,10 @@
100
100
  "vitest": "^4.1.11",
101
101
  "yaml": "^2.9.0",
102
102
  "zod": "^4.5.4",
103
- "@nebulahome/contracts": "0.0.0",
104
- "@nebulahome/eslint-config": "0.0.0",
105
- "@nebulahome/tsconfig": "0.0.0",
106
- "@nebulahome/ui": "0.0.0"
103
+ "@nebulahome/contracts": "2026.10.0-alpha.23",
104
+ "@nebulahome/eslint-config": "2026.10.0-alpha.23",
105
+ "@nebulahome/ui": "2026.10.0-alpha.23",
106
+ "@nebulahome/tsconfig": "2026.10.0-alpha.23"
107
107
  },
108
108
  "scripts": {
109
109
  "build": "tsdown && node scripts/build-styles.js",