@camstack/ui-library 1.2.380 → 1.2.381

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
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.381] - 2026-11-04
11
+
12
+ ### Changed
13
+
14
+ - The device export panel shows each export instance on its own (one section per Home Assistant broker), writes to the instance you act on, and reads a disabled instance as disabled
15
+
10
16
  ## [1.2.380] - 2026-11-04
11
17
 
12
18
  ### Changed
@@ -28,15 +28,20 @@ export interface DeviceExportPanelProps {
28
28
  }
29
29
  /**
30
30
  * The exporter's error and Setup block on their own — for a page that shows
31
- * them above its settings while the devices table sits at the bottom. Renders
32
- * nothing when the exporter reports neither. Shares `getStatus`'s query key
33
- * with the panel, so mounting both costs one request.
31
+ * them above its settings while the devices table sits at the bottom. One block
32
+ * per ENABLED instance (named when there are several); renders nothing while
33
+ * the instances are unanswered or when none reports an error or a setup. Shares
34
+ * `getStatus`'s query key with the panel, so mounting both costs one request.
34
35
  */
35
36
  export declare function DeviceExportSetup({ addonId }: {
36
37
  readonly addonId: string;
37
38
  }): ReactElement | null;
38
39
  /**
39
- * Generic device-export panel. Addon-agnostic — only the `device-export`
40
- * cap drives it.
40
+ * The export surface of one exporter addon, per export INSTANCE. The instances
41
+ * are asked of the provider (`listInstances`), never assumed: one instance
42
+ * renders the table alone, several render one named section each (one per
43
+ * Home Assistant broker), and every read and write names the instance it acts
44
+ * on. A disabled instance reads as disabled; an unanswered list reads as
45
+ * loading or unknown, never as "nothing exposed".
41
46
  */
42
47
  export declare function DeviceExportPanel({ addonId, onOpenDevice, showSetup }: DeviceExportPanelProps): ReactElement;
package/dist/index.cjs CHANGED
@@ -63460,16 +63460,26 @@ function parseStatus(data) {
63460
63460
  const parsed = _camstack_types.DeviceExportStatusSchema.safeParse(data);
63461
63461
  return parsed.success ? parsed.data : null;
63462
63462
  }
63463
- /**
63464
- * The exporter's error and Setup block on their own — for a page that shows
63465
- * them above its settings while the devices table sits at the bottom. Renders
63466
- * nothing when the exporter reports neither. Shares `getStatus`'s query key
63467
- * with the panel, so mounting both costs one request.
63468
- */
63469
- function DeviceExportSetup({ addonId }) {
63470
- const statusQuery = useDeviceExportGetStatus({
63463
+ function useExportInstances(addonId) {
63464
+ const query = useDeviceExportListInstances({
63471
63465
  nodeId: "hub",
63472
63466
  addonId
63467
+ }, { retry: false });
63468
+ return (0, react$1.useMemo)(() => {
63469
+ if (query.data === void 0) return query.isError ? { kind: "unknown" } : { kind: "pending" };
63470
+ const parsed = ExportInstanceArraySchema.safeParse(query.data);
63471
+ return parsed.success ? {
63472
+ kind: "answered",
63473
+ instances: parsed.data
63474
+ } : { kind: "unknown" };
63475
+ }, [query.data, query.isError]);
63476
+ }
63477
+ /** One instance's error and Setup block. Renders nothing when it reports neither. */
63478
+ function InstanceSetup({ addonId, instance, named }) {
63479
+ const statusQuery = useDeviceExportGetStatus({
63480
+ nodeId: "hub",
63481
+ addonId,
63482
+ instanceId: instance.instanceId
63473
63483
  }, {
63474
63484
  refetchInterval: STATUS_POLL_INTERVAL_MS,
63475
63485
  retry: false
@@ -63478,12 +63488,37 @@ function DeviceExportSetup({ addonId }) {
63478
63488
  if (!status?.error && !status?.setup) return null;
63479
63489
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
63480
63490
  className: "rounded-lg border border-border bg-surface overflow-hidden",
63481
- children: [status.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
63482
- className: "px-2 sm:px-4 py-3 border-b border-border",
63483
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: status.error })
63484
- }), status.setup && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupSection, { setup: status.setup })]
63491
+ children: [
63492
+ named && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
63493
+ className: "px-2 sm:px-4 py-2 border-b border-border text-xs font-medium text-foreground",
63494
+ children: instance.name
63495
+ }),
63496
+ status.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
63497
+ className: "px-2 sm:px-4 py-3 border-b border-border",
63498
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: status.error })
63499
+ }),
63500
+ status.setup && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupSection, { setup: status.setup })
63501
+ ]
63485
63502
  });
63486
63503
  }
63504
+ /**
63505
+ * The exporter's error and Setup block on their own — for a page that shows
63506
+ * them above its settings while the devices table sits at the bottom. One block
63507
+ * per ENABLED instance (named when there are several); renders nothing while
63508
+ * the instances are unanswered or when none reports an error or a setup. Shares
63509
+ * `getStatus`'s query key with the panel, so mounting both costs one request.
63510
+ */
63511
+ function DeviceExportSetup({ addonId }) {
63512
+ const state = useExportInstances(addonId);
63513
+ if (state.kind !== "answered") return null;
63514
+ const enabled = state.instances.filter((i) => i.enabled);
63515
+ if (enabled.length === 0) return null;
63516
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: enabled.map((instance) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstanceSetup, {
63517
+ addonId,
63518
+ instance,
63519
+ named: state.instances.length > 1
63520
+ }, instance.instanceId)) });
63521
+ }
63487
63522
  function ExportSelectCell({ device, checked, onToggle }) {
63488
63523
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
63489
63524
  type: "checkbox",
@@ -63579,30 +63614,30 @@ function BulkSummary({ outcome, onDismiss }) {
63579
63614
  ]
63580
63615
  });
63581
63616
  }
63582
- /**
63583
- * Generic device-export panel. Addon-agnostic — only the `device-export`
63584
- * cap drives it.
63585
- */
63586
- function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63617
+ function DeviceExportInstancePanel({ addonId, instance, named, onOpenDevice, showSetup = true }) {
63618
+ const instanceId = instance.instanceId;
63587
63619
  const t = useUiText();
63588
63620
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
63589
63621
  const statusQuery = useDeviceExportGetStatus({
63590
63622
  nodeId: "hub",
63591
- addonId
63623
+ addonId,
63624
+ instanceId
63592
63625
  }, {
63593
63626
  refetchInterval: STATUS_POLL_INTERVAL_MS,
63594
63627
  retry: false
63595
63628
  });
63596
63629
  const exposedQuery = useDeviceExportListExposedDevices({
63597
63630
  nodeId: "hub",
63598
- addonId
63631
+ addonId,
63632
+ instanceId
63599
63633
  }, {
63600
63634
  refetchInterval: STATUS_POLL_INTERVAL_MS,
63601
63635
  retry: false
63602
63636
  });
63603
63637
  const kindsQuery = useDeviceExportListSupportedDeviceKinds({
63604
63638
  nodeId: "hub",
63605
- addonId
63639
+ addonId,
63640
+ instanceId
63606
63641
  }, { retry: false });
63607
63642
  const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
63608
63643
  const integrationsQuery = useIntegrationsList(void 0);
@@ -63617,7 +63652,8 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63617
63652
  const entityKinds = (0, react$1.useMemo)(() => supportedEntityKinds(entityKindsQuery.data), [entityKindsQuery.data]);
63618
63653
  const exposedEntitiesQuery = useDeviceExportListExposedEntities({
63619
63654
  nodeId: "hub",
63620
- addonId
63655
+ addonId,
63656
+ instanceId
63621
63657
  }, {
63622
63658
  refetchInterval: STATUS_POLL_INTERVAL_MS,
63623
63659
  retry: false,
@@ -63634,15 +63670,6 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63634
63670
  const invalidate = (0, react$1.useCallback)(() => {
63635
63671
  queryClient.invalidateQueries({ queryKey: [["deviceExport"]] });
63636
63672
  }, [queryClient]);
63637
- const instancesQuery = useDeviceExportListInstances({
63638
- nodeId: "hub",
63639
- addonId
63640
- }, { retry: false });
63641
- const targetInstanceIds = (0, react$1.useMemo)(() => {
63642
- const parsed = ExportInstanceArraySchema.safeParse(instancesQuery.data);
63643
- if (!parsed.success) return null;
63644
- return parsed.data.filter((i) => i.enabled).map((i) => i.instanceId);
63645
- }, [instancesQuery.data]);
63646
63673
  const exposeMutation = useDeviceExportExposeDevice({ onSuccess: invalidate });
63647
63674
  const unexposeMutation = useDeviceExportUnexposeDevice({ onSuccess: invalidate });
63648
63675
  const exposeEntityMutation = useDeviceExportExposeEntity({ onSuccess: invalidate });
@@ -63739,8 +63766,7 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63739
63766
  for (const deviceId of deviceIds) {
63740
63767
  const label = labelFor(deviceId);
63741
63768
  try {
63742
- if (targetInstanceIds === null || targetInstanceIds.length === 0) throw new Error("no export instance is available to act on");
63743
- for (const instanceId of targetInstanceIds) await mutation.mutateAsync({
63769
+ await mutation.mutateAsync({
63744
63770
  instanceId,
63745
63771
  deviceId,
63746
63772
  nodeId: "hub",
@@ -63774,7 +63800,7 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63774
63800
  unexposeMutation,
63775
63801
  labelFor,
63776
63802
  running,
63777
- targetInstanceIds
63803
+ instanceId
63778
63804
  ]);
63779
63805
  /**
63780
63806
  * Expose / unexpose ONE action or notification target. Same contract as the
@@ -63789,8 +63815,7 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63789
63815
  const succeeded = [];
63790
63816
  const failed = [];
63791
63817
  try {
63792
- if (targetInstanceIds === null || targetInstanceIds.length === 0) throw new Error("no export instance is available to act on");
63793
- for (const instanceId of targetInstanceIds) await mutation.mutateAsync({
63818
+ await mutation.mutateAsync({
63794
63819
  instanceId,
63795
63820
  entity: row.ref,
63796
63821
  nodeId: "hub",
@@ -63821,7 +63846,7 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63821
63846
  exposeEntityMutation,
63822
63847
  unexposeEntityMutation,
63823
63848
  running,
63824
- targetInstanceIds
63849
+ instanceId
63825
63850
  ]);
63826
63851
  const [bulkFilters, setBulkFilters] = (0, react$1.useState)(NO_EXPORT_FILTERS);
63827
63852
  const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices, (id) => id === "(none)" ? t("uiLibrary.deviceExport.noRole", "No role") : deviceRoleMetaOf(id, t).label).map((o) => ({
@@ -64049,6 +64074,10 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
64049
64074
  className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wide",
64050
64075
  children: t("uiLibrary.deviceExport.title", "Exported devices")
64051
64076
  }),
64077
+ named && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
64078
+ className: "text-xs font-medium text-foreground truncate",
64079
+ children: instance.name
64080
+ }),
64052
64081
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
64053
64082
  variant: statusVariant(linkState),
64054
64083
  children: capitaliseLinkState(t, linkState)
@@ -64103,6 +64132,63 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
64103
64132
  ]
64104
64133
  });
64105
64134
  }
64135
+ /** A switched-off instance: named, marked disabled, and asked nothing. */
64136
+ function DisabledInstanceCard({ instance }) {
64137
+ const t = useUiText();
64138
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
64139
+ className: "rounded-lg border border-border bg-surface overflow-hidden",
64140
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
64141
+ className: "flex items-center gap-2 px-2 sm:px-4 py-2.5 bg-surface-hover/20",
64142
+ children: [
64143
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Share2, { className: "h-3.5 w-3.5 text-foreground-subtle shrink-0" }),
64144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
64145
+ className: "text-xs font-medium text-foreground truncate",
64146
+ children: instance.name
64147
+ }),
64148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
64149
+ variant: "neutral",
64150
+ children: t("uiLibrary.deviceExport.instanceDisabled", "Disabled")
64151
+ })
64152
+ ]
64153
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
64154
+ className: "px-2 sm:px-4 py-3 text-xs text-foreground-subtle border-t border-border",
64155
+ children: t("uiLibrary.deviceExport.instanceDisabledHint", "This instance is switched off. Enable it to expose devices through it.")
64156
+ })]
64157
+ });
64158
+ }
64159
+ /**
64160
+ * The export surface of one exporter addon, per export INSTANCE. The instances
64161
+ * are asked of the provider (`listInstances`), never assumed: one instance
64162
+ * renders the table alone, several render one named section each (one per
64163
+ * Home Assistant broker), and every read and write names the instance it acts
64164
+ * on. A disabled instance reads as disabled; an unanswered list reads as
64165
+ * loading or unknown, never as "nothing exposed".
64166
+ */
64167
+ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
64168
+ const t = useUiText();
64169
+ const state = useExportInstances(addonId);
64170
+ if (state.kind === "pending") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
64171
+ className: "flex items-center gap-2 px-1 py-6 text-xs text-foreground-subtle",
64172
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), t("uiLibrary.deviceExport.instancesLoading", "Loading export instances…")]
64173
+ });
64174
+ if (state.kind === "unknown") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: t("uiLibrary.deviceExport.instancesUnknown", "The export instances could not be read. Try again in a moment.") });
64175
+ if (state.instances.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
64176
+ icon: Share2,
64177
+ title: t("uiLibrary.deviceExport.emptyTitle", "No exportable devices"),
64178
+ description: t("uiLibrary.deviceExport.instancesNone", "\"{{addon}}\" has no export instance.", { addon: addonId })
64179
+ });
64180
+ const named = state.instances.length > 1;
64181
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
64182
+ className: "flex flex-col gap-3",
64183
+ children: state.instances.map((instance) => instance.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceExportInstancePanel, {
64184
+ addonId,
64185
+ instance,
64186
+ named,
64187
+ onOpenDevice,
64188
+ showSetup
64189
+ }, instance.instanceId) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledInstanceCard, { instance }, instance.instanceId))
64190
+ });
64191
+ }
64106
64192
  //#endregion
64107
64193
  //#region src/composites/device-grid.tsx
64108
64194
  function DeviceGrid({ children, minCardWidth = 220, gap = 3, className }) {
@@ -73716,7 +73802,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
73716
73802
  }
73717
73803
  //#endregion
73718
73804
  //#region src/ui-library-version.ts
73719
- var uiLibraryVersion = "1.2.380";
73805
+ var uiLibraryVersion = "1.2.381";
73720
73806
  //#endregion
73721
73807
  //#region src/hooks/use-account-unit-preferences.ts
73722
73808
  /**
package/dist/index.js CHANGED
@@ -63436,16 +63436,26 @@ function parseStatus(data) {
63436
63436
  const parsed = DeviceExportStatusSchema.safeParse(data);
63437
63437
  return parsed.success ? parsed.data : null;
63438
63438
  }
63439
- /**
63440
- * The exporter's error and Setup block on their own — for a page that shows
63441
- * them above its settings while the devices table sits at the bottom. Renders
63442
- * nothing when the exporter reports neither. Shares `getStatus`'s query key
63443
- * with the panel, so mounting both costs one request.
63444
- */
63445
- function DeviceExportSetup({ addonId }) {
63446
- const statusQuery = useDeviceExportGetStatus({
63439
+ function useExportInstances(addonId) {
63440
+ const query = useDeviceExportListInstances({
63447
63441
  nodeId: "hub",
63448
63442
  addonId
63443
+ }, { retry: false });
63444
+ return useMemo(() => {
63445
+ if (query.data === void 0) return query.isError ? { kind: "unknown" } : { kind: "pending" };
63446
+ const parsed = ExportInstanceArraySchema.safeParse(query.data);
63447
+ return parsed.success ? {
63448
+ kind: "answered",
63449
+ instances: parsed.data
63450
+ } : { kind: "unknown" };
63451
+ }, [query.data, query.isError]);
63452
+ }
63453
+ /** One instance's error and Setup block. Renders nothing when it reports neither. */
63454
+ function InstanceSetup({ addonId, instance, named }) {
63455
+ const statusQuery = useDeviceExportGetStatus({
63456
+ nodeId: "hub",
63457
+ addonId,
63458
+ instanceId: instance.instanceId
63449
63459
  }, {
63450
63460
  refetchInterval: STATUS_POLL_INTERVAL_MS,
63451
63461
  retry: false
@@ -63454,12 +63464,37 @@ function DeviceExportSetup({ addonId }) {
63454
63464
  if (!status?.error && !status?.setup) return null;
63455
63465
  return /* @__PURE__ */ jsxs("div", {
63456
63466
  className: "rounded-lg border border-border bg-surface overflow-hidden",
63457
- children: [status.error && /* @__PURE__ */ jsx("div", {
63458
- className: "px-2 sm:px-4 py-3 border-b border-border",
63459
- children: /* @__PURE__ */ jsx(ErrorBox, { message: status.error })
63460
- }), status.setup && /* @__PURE__ */ jsx(SetupSection, { setup: status.setup })]
63467
+ children: [
63468
+ named && /* @__PURE__ */ jsx("div", {
63469
+ className: "px-2 sm:px-4 py-2 border-b border-border text-xs font-medium text-foreground",
63470
+ children: instance.name
63471
+ }),
63472
+ status.error && /* @__PURE__ */ jsx("div", {
63473
+ className: "px-2 sm:px-4 py-3 border-b border-border",
63474
+ children: /* @__PURE__ */ jsx(ErrorBox, { message: status.error })
63475
+ }),
63476
+ status.setup && /* @__PURE__ */ jsx(SetupSection, { setup: status.setup })
63477
+ ]
63461
63478
  });
63462
63479
  }
63480
+ /**
63481
+ * The exporter's error and Setup block on their own — for a page that shows
63482
+ * them above its settings while the devices table sits at the bottom. One block
63483
+ * per ENABLED instance (named when there are several); renders nothing while
63484
+ * the instances are unanswered or when none reports an error or a setup. Shares
63485
+ * `getStatus`'s query key with the panel, so mounting both costs one request.
63486
+ */
63487
+ function DeviceExportSetup({ addonId }) {
63488
+ const state = useExportInstances(addonId);
63489
+ if (state.kind !== "answered") return null;
63490
+ const enabled = state.instances.filter((i) => i.enabled);
63491
+ if (enabled.length === 0) return null;
63492
+ return /* @__PURE__ */ jsx(Fragment$1, { children: enabled.map((instance) => /* @__PURE__ */ jsx(InstanceSetup, {
63493
+ addonId,
63494
+ instance,
63495
+ named: state.instances.length > 1
63496
+ }, instance.instanceId)) });
63497
+ }
63463
63498
  function ExportSelectCell({ device, checked, onToggle }) {
63464
63499
  return /* @__PURE__ */ jsx("input", {
63465
63500
  type: "checkbox",
@@ -63555,30 +63590,30 @@ function BulkSummary({ outcome, onDismiss }) {
63555
63590
  ]
63556
63591
  });
63557
63592
  }
63558
- /**
63559
- * Generic device-export panel. Addon-agnostic — only the `device-export`
63560
- * cap drives it.
63561
- */
63562
- function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63593
+ function DeviceExportInstancePanel({ addonId, instance, named, onOpenDevice, showSetup = true }) {
63594
+ const instanceId = instance.instanceId;
63563
63595
  const t = useUiText();
63564
63596
  const queryClient = useQueryClient();
63565
63597
  const statusQuery = useDeviceExportGetStatus({
63566
63598
  nodeId: "hub",
63567
- addonId
63599
+ addonId,
63600
+ instanceId
63568
63601
  }, {
63569
63602
  refetchInterval: STATUS_POLL_INTERVAL_MS,
63570
63603
  retry: false
63571
63604
  });
63572
63605
  const exposedQuery = useDeviceExportListExposedDevices({
63573
63606
  nodeId: "hub",
63574
- addonId
63607
+ addonId,
63608
+ instanceId
63575
63609
  }, {
63576
63610
  refetchInterval: STATUS_POLL_INTERVAL_MS,
63577
63611
  retry: false
63578
63612
  });
63579
63613
  const kindsQuery = useDeviceExportListSupportedDeviceKinds({
63580
63614
  nodeId: "hub",
63581
- addonId
63615
+ addonId,
63616
+ instanceId
63582
63617
  }, { retry: false });
63583
63618
  const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
63584
63619
  const integrationsQuery = useIntegrationsList(void 0);
@@ -63593,7 +63628,8 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63593
63628
  const entityKinds = useMemo(() => supportedEntityKinds(entityKindsQuery.data), [entityKindsQuery.data]);
63594
63629
  const exposedEntitiesQuery = useDeviceExportListExposedEntities({
63595
63630
  nodeId: "hub",
63596
- addonId
63631
+ addonId,
63632
+ instanceId
63597
63633
  }, {
63598
63634
  refetchInterval: STATUS_POLL_INTERVAL_MS,
63599
63635
  retry: false,
@@ -63610,15 +63646,6 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63610
63646
  const invalidate = useCallback(() => {
63611
63647
  queryClient.invalidateQueries({ queryKey: [["deviceExport"]] });
63612
63648
  }, [queryClient]);
63613
- const instancesQuery = useDeviceExportListInstances({
63614
- nodeId: "hub",
63615
- addonId
63616
- }, { retry: false });
63617
- const targetInstanceIds = useMemo(() => {
63618
- const parsed = ExportInstanceArraySchema.safeParse(instancesQuery.data);
63619
- if (!parsed.success) return null;
63620
- return parsed.data.filter((i) => i.enabled).map((i) => i.instanceId);
63621
- }, [instancesQuery.data]);
63622
63649
  const exposeMutation = useDeviceExportExposeDevice({ onSuccess: invalidate });
63623
63650
  const unexposeMutation = useDeviceExportUnexposeDevice({ onSuccess: invalidate });
63624
63651
  const exposeEntityMutation = useDeviceExportExposeEntity({ onSuccess: invalidate });
@@ -63715,8 +63742,7 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63715
63742
  for (const deviceId of deviceIds) {
63716
63743
  const label = labelFor(deviceId);
63717
63744
  try {
63718
- if (targetInstanceIds === null || targetInstanceIds.length === 0) throw new Error("no export instance is available to act on");
63719
- for (const instanceId of targetInstanceIds) await mutation.mutateAsync({
63745
+ await mutation.mutateAsync({
63720
63746
  instanceId,
63721
63747
  deviceId,
63722
63748
  nodeId: "hub",
@@ -63750,7 +63776,7 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63750
63776
  unexposeMutation,
63751
63777
  labelFor,
63752
63778
  running,
63753
- targetInstanceIds
63779
+ instanceId
63754
63780
  ]);
63755
63781
  /**
63756
63782
  * Expose / unexpose ONE action or notification target. Same contract as the
@@ -63765,8 +63791,7 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63765
63791
  const succeeded = [];
63766
63792
  const failed = [];
63767
63793
  try {
63768
- if (targetInstanceIds === null || targetInstanceIds.length === 0) throw new Error("no export instance is available to act on");
63769
- for (const instanceId of targetInstanceIds) await mutation.mutateAsync({
63794
+ await mutation.mutateAsync({
63770
63795
  instanceId,
63771
63796
  entity: row.ref,
63772
63797
  nodeId: "hub",
@@ -63797,7 +63822,7 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63797
63822
  exposeEntityMutation,
63798
63823
  unexposeEntityMutation,
63799
63824
  running,
63800
- targetInstanceIds
63825
+ instanceId
63801
63826
  ]);
63802
63827
  const [bulkFilters, setBulkFilters] = useState(NO_EXPORT_FILTERS);
63803
63828
  const roleOptions = useMemo(() => roleFilterOptions(candidateDevices, (id) => id === "(none)" ? t("uiLibrary.deviceExport.noRole", "No role") : deviceRoleMetaOf(id, t).label).map((o) => ({
@@ -64025,6 +64050,10 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
64025
64050
  className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wide",
64026
64051
  children: t("uiLibrary.deviceExport.title", "Exported devices")
64027
64052
  }),
64053
+ named && /* @__PURE__ */ jsx("span", {
64054
+ className: "text-xs font-medium text-foreground truncate",
64055
+ children: instance.name
64056
+ }),
64028
64057
  /* @__PURE__ */ jsx(Badge, {
64029
64058
  variant: statusVariant(linkState),
64030
64059
  children: capitaliseLinkState(t, linkState)
@@ -64079,6 +64108,63 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
64079
64108
  ]
64080
64109
  });
64081
64110
  }
64111
+ /** A switched-off instance: named, marked disabled, and asked nothing. */
64112
+ function DisabledInstanceCard({ instance }) {
64113
+ const t = useUiText();
64114
+ return /* @__PURE__ */ jsxs("div", {
64115
+ className: "rounded-lg border border-border bg-surface overflow-hidden",
64116
+ children: [/* @__PURE__ */ jsxs("div", {
64117
+ className: "flex items-center gap-2 px-2 sm:px-4 py-2.5 bg-surface-hover/20",
64118
+ children: [
64119
+ /* @__PURE__ */ jsx(Share2, { className: "h-3.5 w-3.5 text-foreground-subtle shrink-0" }),
64120
+ /* @__PURE__ */ jsx("span", {
64121
+ className: "text-xs font-medium text-foreground truncate",
64122
+ children: instance.name
64123
+ }),
64124
+ /* @__PURE__ */ jsx(Badge, {
64125
+ variant: "neutral",
64126
+ children: t("uiLibrary.deviceExport.instanceDisabled", "Disabled")
64127
+ })
64128
+ ]
64129
+ }), /* @__PURE__ */ jsx("p", {
64130
+ className: "px-2 sm:px-4 py-3 text-xs text-foreground-subtle border-t border-border",
64131
+ children: t("uiLibrary.deviceExport.instanceDisabledHint", "This instance is switched off. Enable it to expose devices through it.")
64132
+ })]
64133
+ });
64134
+ }
64135
+ /**
64136
+ * The export surface of one exporter addon, per export INSTANCE. The instances
64137
+ * are asked of the provider (`listInstances`), never assumed: one instance
64138
+ * renders the table alone, several render one named section each (one per
64139
+ * Home Assistant broker), and every read and write names the instance it acts
64140
+ * on. A disabled instance reads as disabled; an unanswered list reads as
64141
+ * loading or unknown, never as "nothing exposed".
64142
+ */
64143
+ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
64144
+ const t = useUiText();
64145
+ const state = useExportInstances(addonId);
64146
+ if (state.kind === "pending") return /* @__PURE__ */ jsxs("div", {
64147
+ className: "flex items-center gap-2 px-1 py-6 text-xs text-foreground-subtle",
64148
+ children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), t("uiLibrary.deviceExport.instancesLoading", "Loading export instances…")]
64149
+ });
64150
+ if (state.kind === "unknown") return /* @__PURE__ */ jsx(ErrorBox, { message: t("uiLibrary.deviceExport.instancesUnknown", "The export instances could not be read. Try again in a moment.") });
64151
+ if (state.instances.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
64152
+ icon: Share2,
64153
+ title: t("uiLibrary.deviceExport.emptyTitle", "No exportable devices"),
64154
+ description: t("uiLibrary.deviceExport.instancesNone", "\"{{addon}}\" has no export instance.", { addon: addonId })
64155
+ });
64156
+ const named = state.instances.length > 1;
64157
+ return /* @__PURE__ */ jsx("div", {
64158
+ className: "flex flex-col gap-3",
64159
+ children: state.instances.map((instance) => instance.enabled ? /* @__PURE__ */ jsx(DeviceExportInstancePanel, {
64160
+ addonId,
64161
+ instance,
64162
+ named,
64163
+ onOpenDevice,
64164
+ showSetup
64165
+ }, instance.instanceId) : /* @__PURE__ */ jsx(DisabledInstanceCard, { instance }, instance.instanceId))
64166
+ });
64167
+ }
64082
64168
  //#endregion
64083
64169
  //#region src/composites/device-grid.tsx
64084
64170
  function DeviceGrid({ children, minCardWidth = 220, gap = 3, className }) {
@@ -73692,7 +73778,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
73692
73778
  }
73693
73779
  //#endregion
73694
73780
  //#region src/ui-library-version.ts
73695
- var uiLibraryVersion = "1.2.380";
73781
+ var uiLibraryVersion = "1.2.381";
73696
73782
  //#endregion
73697
73783
  //#region src/hooks/use-account-unit-preferences.ts
73698
73784
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.380",
3
+ "version": "1.2.381",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/camstack/server.git",