@camstack/ui-library 1.2.372 → 1.2.373

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.373] - 2026-11-02
11
+
12
+ ### Changed
13
+
14
+ - Device export table: selectors on the left with the search on the right, searchable Role and Integration filters showing display names, a bulk bar with select all / none, and a multi export that now lands every selected device
15
+
10
16
  ## [1.2.372] - 2026-11-02
11
17
 
12
18
  ### Changed
@@ -94,6 +94,19 @@ export interface DeviceListGenericProps<T> {
94
94
  readonly toolbar?: ReactNode | ((ctx: {
95
95
  readonly filtered: readonly T[];
96
96
  }) => ReactNode);
97
+ /**
98
+ * Extra selectors rendered on the LEFT, before the type filter, so every
99
+ * selector sits in one group and the search box stays alone on the right.
100
+ * Wraps onto their own row on a narrow screen.
101
+ */
102
+ readonly filters?: ReactNode;
103
+ /**
104
+ * Full-width row between the filter row and the table (a bulk-action bar).
105
+ * A function receives the rows the search and filters currently leave.
106
+ */
107
+ readonly belowFilters?: ReactNode | ((ctx: {
108
+ readonly filtered: readonly T[];
109
+ }) => ReactNode);
97
110
  }
98
111
  /**
99
112
  * Generic render-prop list: search + pagination chrome shared with the device
@@ -7,12 +7,25 @@
7
7
  * the only filter, D869).
8
8
  */
9
9
  import type { MultiSelectOption } from "./device-list/multi-select";
10
+ import { type DeviceIntegrationRef } from "./device-list/device-integration";
10
11
  /** The slice of a device row the bulk filters read. */
11
12
  export interface ExportFilterable {
12
13
  readonly id: number;
13
14
  readonly addonId: string;
14
15
  readonly role?: string | null;
16
+ /** The integration that created the device (`integrations.list` `id`). */
17
+ readonly integrationId?: string | null;
15
18
  }
19
+ /** The slice of an `integrations.list` row the provider filter reads. */
20
+ export interface ExportIntegration extends DeviceIntegrationRef {
21
+ readonly name: string;
22
+ }
23
+ /**
24
+ * The provider filter value of a row: the id of the integration it belongs to,
25
+ * or `addon:<addonId>` when no registered integration claims it (or the list
26
+ * has not answered). The raw id is never shown — `providerFilterOptions` names it.
27
+ */
28
+ export declare function providerFilterId(device: ExportFilterable, integrations: readonly ExportIntegration[] | null | undefined): string;
16
29
  export interface ExportBulkFilters {
17
30
  /** Keep rows whose role is one of these; empty = no role filter. */
18
31
  readonly roles: readonly string[];
@@ -24,11 +37,17 @@ export declare const NO_ROLE_FILTER_ID = "(none)";
24
37
  export declare const NO_EXPORT_FILTERS: ExportBulkFilters;
25
38
  export declare function roleFilterId(device: ExportFilterable): string;
26
39
  /** The rows the role / provider filters leave. */
27
- export declare function applyExportFilters<T extends ExportFilterable>(devices: readonly T[], filters: ExportBulkFilters): readonly T[];
28
- /** Distinct roles present among the rows (a roleless row counts as "(none)"). */
29
- export declare function roleFilterOptions(devices: readonly ExportFilterable[]): readonly MultiSelectOption[];
30
- /** Distinct provider addons present among the rows. */
31
- export declare function providerFilterOptions(devices: readonly ExportFilterable[]): readonly MultiSelectOption[];
40
+ export declare function applyExportFilters<T extends ExportFilterable>(devices: readonly T[], filters: ExportBulkFilters, integrations?: readonly ExportIntegration[] | null): readonly T[];
41
+ /**
42
+ * Distinct roles present among the rows, named by `labelOf` (a roleless row is
43
+ * `NO_ROLE_FILTER_ID`).
44
+ */
45
+ export declare function roleFilterOptions(devices: readonly ExportFilterable[], labelOf?: (roleId: string) => string): readonly MultiSelectOption[];
46
+ /**
47
+ * Distinct integrations present among the rows, by NAME. A device no
48
+ * registered integration claims is listed under its addon id.
49
+ */
50
+ export declare function providerFilterOptions(devices: readonly ExportFilterable[], integrations?: readonly ExportIntegration[] | null): readonly MultiSelectOption[];
32
51
  /** `current` plus every shown row. */
33
52
  export declare function selectShown(current: ReadonlySet<string>, shown: readonly ExportFilterable[]): ReadonlySet<string>;
34
53
  /** `current` minus every shown row; selections outside the filter are kept. */
package/dist/index.cjs CHANGED
@@ -62173,7 +62173,7 @@ var FIXED_PAGE_SIZE = 100;
62173
62173
  */
62174
62174
  function GenericModeList(props) {
62175
62175
  const t = useUiText();
62176
- const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar } = props;
62176
+ const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar, filters, belowFilters } = props;
62177
62177
  const [chosenPageSize, setChosenPageSize] = usePersistedPageSize(pageSizeSelector ?? {
62178
62178
  storageKey: null,
62179
62179
  options: [fixedPageSize],
@@ -62241,16 +62241,22 @@ function GenericModeList(props) {
62241
62241
  className: "flex flex-col gap-3 h-full min-h-0",
62242
62242
  children: [
62243
62243
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
62244
- className: "flex items-center justify-between gap-3 flex-wrap shrink-0",
62245
- children: [getFilterType && typeOptions.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResponsiveTypeFilter, {
62246
- label: filterLabel,
62247
- options: typeOptions,
62248
- selected: typeFilters,
62249
- onChange: setTypeFilters
62250
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
62251
- className: "flex items-center gap-2",
62244
+ className: "flex items-start justify-between gap-3 flex-wrap shrink-0",
62245
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
62246
+ className: "flex min-w-0 flex-1 flex-wrap items-start gap-2",
62247
+ children: [filters, getFilterType && typeOptions.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
62248
+ className: "flex min-w-40 flex-1",
62249
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResponsiveTypeFilter, {
62250
+ label: filterLabel,
62251
+ options: typeOptions,
62252
+ selected: typeFilters,
62253
+ onChange: setTypeFilters
62254
+ })
62255
+ }) : null]
62256
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
62257
+ className: "flex items-center gap-2 max-sm:w-full",
62252
62258
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
62253
- className: "relative",
62259
+ className: "relative max-sm:flex-1",
62254
62260
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-foreground-subtle pointer-events-none" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
62255
62261
  ref: searchRef,
62256
62262
  type: "text",
@@ -62258,11 +62264,12 @@ function GenericModeList(props) {
62258
62264
  onChange: (e) => setSearchInput(e.target.value),
62259
62265
  placeholder: searchPlaceholder,
62260
62266
  "aria-label": searchPlaceholder,
62261
- className: "h-7 pl-7 pr-2.5 rounded-md border border-border bg-surface text-[11px] text-foreground placeholder:text-foreground-subtle/60 focus:outline-none focus:ring-1 focus:ring-primary/40 w-56"
62267
+ className: "h-7 pl-7 pr-2.5 rounded-md border border-border bg-surface text-[11px] text-foreground placeholder:text-foreground-subtle/60 focus:outline-none focus:ring-1 focus:ring-primary/40 w-56 max-sm:w-full"
62262
62268
  })]
62263
62269
  }), typeof toolbar === "function" ? toolbar({ filtered }) : toolbar]
62264
62270
  })]
62265
62271
  }),
62272
+ typeof belowFilters === "function" ? belowFilters({ filtered }) : belowFilters,
62266
62273
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
62267
62274
  className: "flex-1 min-h-0 overflow-y-auto",
62268
62275
  children: isLoading && totalItems === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -62393,6 +62400,17 @@ function DeviceList(props) {
62393
62400
  }
62394
62401
  //#endregion
62395
62402
  //#region src/composites/export-bulk-selection.ts
62403
+ /** The provider filter value of a device no registered integration claims. */
62404
+ var UNREGISTERED_PREFIX = "addon:";
62405
+ /**
62406
+ * The provider filter value of a row: the id of the integration it belongs to,
62407
+ * or `addon:<addonId>` when no registered integration claims it (or the list
62408
+ * has not answered). The raw id is never shown — `providerFilterOptions` names it.
62409
+ */
62410
+ function providerFilterId(device, integrations) {
62411
+ const resolved = deviceIntegrationResolver(integrations)(device);
62412
+ return resolved.kind === "resolved" ? resolved.integration.id : `${UNREGISTERED_PREFIX}${device.addonId}`;
62413
+ }
62396
62414
  /** The role filter value of a row that has no role. */
62397
62415
  var NO_ROLE_FILTER_ID = "(none)";
62398
62416
  var NO_EXPORT_FILTERS = {
@@ -62404,24 +62422,40 @@ function roleFilterId(device) {
62404
62422
  return role === null || role === void 0 || role === "" ? NO_ROLE_FILTER_ID : role;
62405
62423
  }
62406
62424
  /** The rows the role / provider filters leave. */
62407
- function applyExportFilters(devices, filters) {
62425
+ function applyExportFilters(devices, filters, integrations) {
62408
62426
  const roles = new Set(filters.roles);
62409
62427
  const providers = new Set(filters.providers);
62410
- return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(d.addonId)));
62428
+ return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(providerFilterId(d, integrations))));
62411
62429
  }
62412
- function distinctOptions(values) {
62413
- return [...new Set(values)].toSorted().map((id) => ({
62414
- id,
62415
- label: id
62416
- }));
62430
+ function sortedByLabel(options) {
62431
+ return options.toSorted((a, b) => a.label.localeCompare(b.label));
62417
62432
  }
62418
- /** Distinct roles present among the rows (a roleless row counts as "(none)"). */
62419
- function roleFilterOptions(devices) {
62420
- return distinctOptions(devices.map(roleFilterId));
62433
+ /**
62434
+ * Distinct roles present among the rows, named by `labelOf` (a roleless row is
62435
+ * `NO_ROLE_FILTER_ID`).
62436
+ */
62437
+ function roleFilterOptions(devices, labelOf = (id) => id) {
62438
+ return sortedByLabel([...new Set(devices.map(roleFilterId))].map((id) => ({
62439
+ id,
62440
+ label: labelOf(id)
62441
+ })));
62421
62442
  }
62422
- /** Distinct provider addons present among the rows. */
62423
- function providerFilterOptions(devices) {
62424
- return distinctOptions(devices.map((d) => d.addonId));
62443
+ /**
62444
+ * Distinct integrations present among the rows, by NAME. A device no
62445
+ * registered integration claims is listed under its addon id.
62446
+ */
62447
+ function providerFilterOptions(devices, integrations) {
62448
+ const nameById = new Map((integrations ?? []).map((i) => [i.id, i.name]));
62449
+ const options = /* @__PURE__ */ new Map();
62450
+ for (const d of devices) {
62451
+ const id = providerFilterId(d, integrations);
62452
+ if (options.has(id)) continue;
62453
+ options.set(id, nameById.get(id) ?? d.addonId);
62454
+ }
62455
+ return sortedByLabel([...options].map(([id, label]) => ({
62456
+ id,
62457
+ label
62458
+ })));
62425
62459
  }
62426
62460
  /** `current` plus every shown row. */
62427
62461
  function selectShown(current, shown) {
@@ -62577,9 +62611,15 @@ var DeviceItemRowSchema = zod.z.object({
62577
62611
  disabled: zod.z.boolean(),
62578
62612
  parentDeviceId: zod.z.number().nullable(),
62579
62613
  role: zod.z.string().nullable().optional(),
62614
+ integrationId: zod.z.string().nullable().optional(),
62580
62615
  features: zod.z.array(zod.z.string()).optional(),
62581
62616
  isCamera: zod.z.boolean().optional()
62582
62617
  });
62618
+ var IntegrationRowsSchema = zod.z.array(zod.z.object({
62619
+ id: zod.z.string(),
62620
+ addonId: zod.z.string(),
62621
+ name: zod.z.string()
62622
+ }));
62583
62623
  var SupportedKindsSchema = zod.z.array(zod.z.string());
62584
62624
  var STATUS_POLL_INTERVAL_MS = 1e4;
62585
62625
  /** Where the viewer's "rows per page" choice for this table is remembered. */
@@ -62836,6 +62876,11 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
62836
62876
  addonId
62837
62877
  }, { retry: false });
62838
62878
  const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
62879
+ const integrationsQuery = useIntegrationsList(void 0);
62880
+ const integrations = (0, react$1.useMemo)(() => {
62881
+ const parsed = IntegrationRowsSchema.safeParse(integrationsQuery.data);
62882
+ return parsed.success ? parsed.data : null;
62883
+ }, [integrationsQuery.data]);
62839
62884
  const entityKindsQuery = useDeviceExportListSupportedEntityKinds({
62840
62885
  nodeId: "hub",
62841
62886
  addonId
@@ -62953,28 +62998,28 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
62953
62998
  const succeeded = [];
62954
62999
  const failed = [];
62955
63000
  const mutation = action === "expose" ? exposeMutation : unexposeMutation;
62956
- (await Promise.allSettled(deviceIds.map((deviceId) => mutation.mutateAsync({
62957
- deviceId,
62958
- nodeId: "hub",
62959
- addonId
62960
- })))).forEach((result, index) => {
62961
- const deviceId = deviceIds[index] ?? "";
63001
+ for (const deviceId of deviceIds) {
62962
63002
  const label = labelFor(deviceId);
62963
- if (result.status === "fulfilled") {
63003
+ try {
63004
+ await mutation.mutateAsync({
63005
+ deviceId,
63006
+ nodeId: "hub",
63007
+ addonId
63008
+ });
62964
63009
  succeeded.push(label);
62965
- return;
63010
+ } catch (reason) {
63011
+ const message = reason instanceof Error ? reason.message : String(reason);
63012
+ console.error(`[device-export] ${action} failed`, {
63013
+ tags: { deviceId: Number.parseInt(deviceId, 10) },
63014
+ addonId,
63015
+ error: message
63016
+ });
63017
+ failed.push({
63018
+ label,
63019
+ message
63020
+ });
62966
63021
  }
62967
- const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
62968
- console.error(`[device-export] ${action} failed`, {
62969
- tags: { deviceId: Number.parseInt(deviceId, 10) },
62970
- addonId,
62971
- error: message
62972
- });
62973
- failed.push({
62974
- label,
62975
- message
62976
- });
62977
- });
63022
+ }
62978
63023
  setOutcome({
62979
63024
  action,
62980
63025
  total: deviceIds.length,
@@ -63035,9 +63080,19 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63035
63080
  running
63036
63081
  ]);
63037
63082
  const [bulkFilters, setBulkFilters] = (0, react$1.useState)(NO_EXPORT_FILTERS);
63038
- const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices), [candidateDevices]);
63039
- const providerOptions = (0, react$1.useMemo)(() => providerFilterOptions(candidateDevices), [candidateDevices]);
63040
- const shownDevices = (0, react$1.useMemo)(() => applyExportFilters(candidateDevices, bulkFilters), [candidateDevices, bulkFilters]);
63083
+ const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices, (id) => id === "(none)" ? t("uiLibrary.deviceExport.noRole", "No role") : deviceRoleMetaOf(id, t).label).map((o) => ({
63084
+ value: o.id,
63085
+ label: o.label
63086
+ })), [candidateDevices, t]);
63087
+ const providerOptions = (0, react$1.useMemo)(() => providerFilterOptions(candidateDevices, integrations).map((o) => ({
63088
+ value: o.id,
63089
+ label: o.label
63090
+ })), [candidateDevices, integrations]);
63091
+ const shownDevices = (0, react$1.useMemo)(() => applyExportFilters(candidateDevices, bulkFilters, integrations), [
63092
+ candidateDevices,
63093
+ bulkFilters,
63094
+ integrations
63095
+ ]);
63041
63096
  const shownRows = (0, react$1.useMemo)(() => {
63042
63097
  const deviceRows = shownDevices.map((device) => ({
63043
63098
  kind: "device",
@@ -63150,72 +63205,86 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63150
63205
  toggleSelection,
63151
63206
  t
63152
63207
  ]);
63153
- const renderToolbar = (0, react$1.useCallback)(({ filtered: filteredRows }) => {
63208
+ const filterSelectors = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [roleOptions.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
63209
+ className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
63210
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
63211
+ mode: "multi",
63212
+ searchable: true,
63213
+ options: roleOptions,
63214
+ value: bulkFilters.roles,
63215
+ onChange: setRoleFilter,
63216
+ placeholder: t("uiLibrary.deviceExport.filterRole", "Role"),
63217
+ emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
63218
+ testId: "export-filter-role"
63219
+ })
63220
+ }), providerOptions.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
63221
+ className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
63222
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionSelect, {
63223
+ mode: "multi",
63224
+ searchable: true,
63225
+ options: providerOptions,
63226
+ value: bulkFilters.providers,
63227
+ onChange: setProviderFilter,
63228
+ placeholder: t("uiLibrary.deviceExport.filterIntegration", "Integration"),
63229
+ emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
63230
+ testId: "export-filter-integration"
63231
+ })
63232
+ })] });
63233
+ const renderBulkBar = (0, react$1.useCallback)(({ filtered: filteredRows }) => {
63154
63234
  const filtered = filteredRows.flatMap((row) => row.kind === "device" ? [row.device] : []);
63155
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
63156
- className: "inline-flex flex-wrap items-center gap-1.5",
63235
+ if (filtered.length === 0 && selectedIds.size === 0) return null;
63236
+ const hasSelection = selectedIds.size > 0;
63237
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
63238
+ role: "toolbar",
63239
+ "aria-label": t("uiLibrary.deviceExport.bulkBar", "Bulk export"),
63240
+ className: `flex flex-wrap items-center gap-2 rounded-md border px-2.5 py-1.5 ${hasSelection ? "border-primary/40 bg-primary/10" : "border-border bg-surface"}`,
63157
63241
  children: [
63158
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
63159
- label: t("uiLibrary.deviceExport.filterRole", "Role"),
63160
- options: roleOptions,
63161
- selected: bulkFilters.roles,
63162
- onChange: setRoleFilter
63163
- }),
63164
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
63165
- label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
63166
- options: providerOptions,
63167
- selected: bulkFilters.providers,
63168
- onChange: setProviderFilter
63242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
63243
+ ...hasSelection ? { "data-testid": "export-selection-count" } : {},
63244
+ className: `text-[11px] font-medium ${hasSelection ? "text-foreground" : "text-foreground-subtle"}`,
63245
+ children: hasSelection ? t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size }) : t("uiLibrary.deviceExport.noneSelected", "None selected")
63169
63246
  }),
63170
63247
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
63171
63248
  size: "sm",
63172
- variant: "secondary",
63249
+ variant: "ghost",
63173
63250
  disabled: filtered.length === 0,
63174
63251
  onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
63175
- children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
63252
+ children: t("uiLibrary.deviceExport.selectShown", "Select all {{count}} shown", { count: filtered.length })
63176
63253
  }),
63177
- selectedIds.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
63254
+ hasSelection && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
63178
63255
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
63179
63256
  size: "sm",
63180
63257
  variant: "ghost",
63181
63258
  onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
63182
63259
  children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
63183
63260
  }),
63184
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
63185
- "data-testid": "export-selection-count",
63186
- className: "text-[11px] text-foreground-subtle",
63187
- children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
63188
- }),
63189
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
63190
- size: "sm",
63191
- variant: "primary",
63192
- disabled: busy,
63193
- onClick: exposeSelected,
63194
- children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
63195
- }),
63196
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
63197
- size: "sm",
63198
- variant: "secondary",
63199
- disabled: busy,
63200
- onClick: unexposeSelected,
63201
- children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
63202
- }),
63203
63261
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
63204
63262
  size: "sm",
63205
63263
  variant: "ghost",
63206
63264
  onClick: clearSelection,
63207
63265
  children: t("uiLibrary.deviceExport.clear", "Clear")
63266
+ }),
63267
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
63268
+ className: "ml-auto inline-flex flex-wrap items-center gap-1.5",
63269
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
63270
+ size: "sm",
63271
+ variant: "primary",
63272
+ disabled: busy,
63273
+ onClick: exposeSelected,
63274
+ children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
63275
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
63276
+ size: "sm",
63277
+ variant: "secondary",
63278
+ disabled: busy,
63279
+ onClick: unexposeSelected,
63280
+ children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
63281
+ })]
63208
63282
  })
63209
63283
  ] })
63210
63284
  ]
63211
63285
  });
63212
63286
  }, [
63213
63287
  t,
63214
- roleOptions,
63215
- providerOptions,
63216
- bulkFilters,
63217
- setRoleFilter,
63218
- setProviderFilter,
63219
63288
  selectedIds.size,
63220
63289
  busy,
63221
63290
  exposeSelected,
@@ -63270,7 +63339,8 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63270
63339
  filterOptions: typeFilterOptions,
63271
63340
  columns,
63272
63341
  onRowClick: onOpenDevice ? onRowClick : void 0,
63273
- toolbar: renderToolbar,
63342
+ filters: filterSelectors,
63343
+ belowFilters: renderBulkBar,
63274
63344
  pageSizeSelector: DEVICE_EXPORT_PAGE_SIZE,
63275
63345
  searchPlaceholder: t("uiLibrary.deviceExport.search", "Search by name or stableId"),
63276
63346
  emptyLabel: t("uiLibrary.deviceExport.noMatch", "No device matches this search.")
@@ -72899,7 +72969,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
72899
72969
  }
72900
72970
  //#endregion
72901
72971
  //#region src/ui-library-version.ts
72902
- var uiLibraryVersion = "1.2.372";
72972
+ var uiLibraryVersion = "1.2.373";
72903
72973
  //#endregion
72904
72974
  //#region src/hooks/use-account-unit-preferences.ts
72905
72975
  /**
package/dist/index.js CHANGED
@@ -62149,7 +62149,7 @@ var FIXED_PAGE_SIZE = 100;
62149
62149
  */
62150
62150
  function GenericModeList(props) {
62151
62151
  const t = useUiText();
62152
- const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar } = props;
62152
+ const { items, getKey, getSearchText, renderItem, getFilterType, mapFilterOption, filterOptions, filterLabel = "Type", initialTypeFilters = [], columns, renderExpanded, isExpanded, rowClassName, onRowClick, isLoading = false, searchPlaceholder = "Search", emptyLabel = "No items found.", pageSize: fixedPageSize = FIXED_PAGE_SIZE, pageSizeSelector, toolbar, filters, belowFilters } = props;
62153
62153
  const [chosenPageSize, setChosenPageSize] = usePersistedPageSize(pageSizeSelector ?? {
62154
62154
  storageKey: null,
62155
62155
  options: [fixedPageSize],
@@ -62217,16 +62217,22 @@ function GenericModeList(props) {
62217
62217
  className: "flex flex-col gap-3 h-full min-h-0",
62218
62218
  children: [
62219
62219
  /* @__PURE__ */ jsxs("div", {
62220
- className: "flex items-center justify-between gap-3 flex-wrap shrink-0",
62221
- children: [getFilterType && typeOptions.length > 0 ? /* @__PURE__ */ jsx(ResponsiveTypeFilter, {
62222
- label: filterLabel,
62223
- options: typeOptions,
62224
- selected: typeFilters,
62225
- onChange: setTypeFilters
62226
- }) : /* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsxs("div", {
62227
- className: "flex items-center gap-2",
62220
+ className: "flex items-start justify-between gap-3 flex-wrap shrink-0",
62221
+ children: [/* @__PURE__ */ jsxs("div", {
62222
+ className: "flex min-w-0 flex-1 flex-wrap items-start gap-2",
62223
+ children: [filters, getFilterType && typeOptions.length > 0 ? /* @__PURE__ */ jsx("div", {
62224
+ className: "flex min-w-40 flex-1",
62225
+ children: /* @__PURE__ */ jsx(ResponsiveTypeFilter, {
62226
+ label: filterLabel,
62227
+ options: typeOptions,
62228
+ selected: typeFilters,
62229
+ onChange: setTypeFilters
62230
+ })
62231
+ }) : null]
62232
+ }), /* @__PURE__ */ jsxs("div", {
62233
+ className: "flex items-center gap-2 max-sm:w-full",
62228
62234
  children: [/* @__PURE__ */ jsxs("div", {
62229
- className: "relative",
62235
+ className: "relative max-sm:flex-1",
62230
62236
  children: [/* @__PURE__ */ jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-foreground-subtle pointer-events-none" }), /* @__PURE__ */ jsx("input", {
62231
62237
  ref: searchRef,
62232
62238
  type: "text",
@@ -62234,11 +62240,12 @@ function GenericModeList(props) {
62234
62240
  onChange: (e) => setSearchInput(e.target.value),
62235
62241
  placeholder: searchPlaceholder,
62236
62242
  "aria-label": searchPlaceholder,
62237
- className: "h-7 pl-7 pr-2.5 rounded-md border border-border bg-surface text-[11px] text-foreground placeholder:text-foreground-subtle/60 focus:outline-none focus:ring-1 focus:ring-primary/40 w-56"
62243
+ className: "h-7 pl-7 pr-2.5 rounded-md border border-border bg-surface text-[11px] text-foreground placeholder:text-foreground-subtle/60 focus:outline-none focus:ring-1 focus:ring-primary/40 w-56 max-sm:w-full"
62238
62244
  })]
62239
62245
  }), typeof toolbar === "function" ? toolbar({ filtered }) : toolbar]
62240
62246
  })]
62241
62247
  }),
62248
+ typeof belowFilters === "function" ? belowFilters({ filtered }) : belowFilters,
62242
62249
  /* @__PURE__ */ jsx("div", {
62243
62250
  className: "flex-1 min-h-0 overflow-y-auto",
62244
62251
  children: isLoading && totalItems === 0 ? /* @__PURE__ */ jsx("div", {
@@ -62369,6 +62376,17 @@ function DeviceList(props) {
62369
62376
  }
62370
62377
  //#endregion
62371
62378
  //#region src/composites/export-bulk-selection.ts
62379
+ /** The provider filter value of a device no registered integration claims. */
62380
+ var UNREGISTERED_PREFIX = "addon:";
62381
+ /**
62382
+ * The provider filter value of a row: the id of the integration it belongs to,
62383
+ * or `addon:<addonId>` when no registered integration claims it (or the list
62384
+ * has not answered). The raw id is never shown — `providerFilterOptions` names it.
62385
+ */
62386
+ function providerFilterId(device, integrations) {
62387
+ const resolved = deviceIntegrationResolver(integrations)(device);
62388
+ return resolved.kind === "resolved" ? resolved.integration.id : `${UNREGISTERED_PREFIX}${device.addonId}`;
62389
+ }
62372
62390
  /** The role filter value of a row that has no role. */
62373
62391
  var NO_ROLE_FILTER_ID = "(none)";
62374
62392
  var NO_EXPORT_FILTERS = {
@@ -62380,24 +62398,40 @@ function roleFilterId(device) {
62380
62398
  return role === null || role === void 0 || role === "" ? NO_ROLE_FILTER_ID : role;
62381
62399
  }
62382
62400
  /** The rows the role / provider filters leave. */
62383
- function applyExportFilters(devices, filters) {
62401
+ function applyExportFilters(devices, filters, integrations) {
62384
62402
  const roles = new Set(filters.roles);
62385
62403
  const providers = new Set(filters.providers);
62386
- return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(d.addonId)));
62404
+ return devices.filter((d) => (roles.size === 0 || roles.has(roleFilterId(d))) && (providers.size === 0 || providers.has(providerFilterId(d, integrations))));
62387
62405
  }
62388
- function distinctOptions(values) {
62389
- return [...new Set(values)].toSorted().map((id) => ({
62390
- id,
62391
- label: id
62392
- }));
62406
+ function sortedByLabel(options) {
62407
+ return options.toSorted((a, b) => a.label.localeCompare(b.label));
62393
62408
  }
62394
- /** Distinct roles present among the rows (a roleless row counts as "(none)"). */
62395
- function roleFilterOptions(devices) {
62396
- return distinctOptions(devices.map(roleFilterId));
62409
+ /**
62410
+ * Distinct roles present among the rows, named by `labelOf` (a roleless row is
62411
+ * `NO_ROLE_FILTER_ID`).
62412
+ */
62413
+ function roleFilterOptions(devices, labelOf = (id) => id) {
62414
+ return sortedByLabel([...new Set(devices.map(roleFilterId))].map((id) => ({
62415
+ id,
62416
+ label: labelOf(id)
62417
+ })));
62397
62418
  }
62398
- /** Distinct provider addons present among the rows. */
62399
- function providerFilterOptions(devices) {
62400
- return distinctOptions(devices.map((d) => d.addonId));
62419
+ /**
62420
+ * Distinct integrations present among the rows, by NAME. A device no
62421
+ * registered integration claims is listed under its addon id.
62422
+ */
62423
+ function providerFilterOptions(devices, integrations) {
62424
+ const nameById = new Map((integrations ?? []).map((i) => [i.id, i.name]));
62425
+ const options = /* @__PURE__ */ new Map();
62426
+ for (const d of devices) {
62427
+ const id = providerFilterId(d, integrations);
62428
+ if (options.has(id)) continue;
62429
+ options.set(id, nameById.get(id) ?? d.addonId);
62430
+ }
62431
+ return sortedByLabel([...options].map(([id, label]) => ({
62432
+ id,
62433
+ label
62434
+ })));
62401
62435
  }
62402
62436
  /** `current` plus every shown row. */
62403
62437
  function selectShown(current, shown) {
@@ -62553,9 +62587,15 @@ var DeviceItemRowSchema = z.object({
62553
62587
  disabled: z.boolean(),
62554
62588
  parentDeviceId: z.number().nullable(),
62555
62589
  role: z.string().nullable().optional(),
62590
+ integrationId: z.string().nullable().optional(),
62556
62591
  features: z.array(z.string()).optional(),
62557
62592
  isCamera: z.boolean().optional()
62558
62593
  });
62594
+ var IntegrationRowsSchema = z.array(z.object({
62595
+ id: z.string(),
62596
+ addonId: z.string(),
62597
+ name: z.string()
62598
+ }));
62559
62599
  var SupportedKindsSchema = z.array(z.string());
62560
62600
  var STATUS_POLL_INTERVAL_MS = 1e4;
62561
62601
  /** Where the viewer's "rows per page" choice for this table is remembered. */
@@ -62812,6 +62852,11 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
62812
62852
  addonId
62813
62853
  }, { retry: false });
62814
62854
  const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
62855
+ const integrationsQuery = useIntegrationsList(void 0);
62856
+ const integrations = useMemo(() => {
62857
+ const parsed = IntegrationRowsSchema.safeParse(integrationsQuery.data);
62858
+ return parsed.success ? parsed.data : null;
62859
+ }, [integrationsQuery.data]);
62815
62860
  const entityKindsQuery = useDeviceExportListSupportedEntityKinds({
62816
62861
  nodeId: "hub",
62817
62862
  addonId
@@ -62929,28 +62974,28 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
62929
62974
  const succeeded = [];
62930
62975
  const failed = [];
62931
62976
  const mutation = action === "expose" ? exposeMutation : unexposeMutation;
62932
- (await Promise.allSettled(deviceIds.map((deviceId) => mutation.mutateAsync({
62933
- deviceId,
62934
- nodeId: "hub",
62935
- addonId
62936
- })))).forEach((result, index) => {
62937
- const deviceId = deviceIds[index] ?? "";
62977
+ for (const deviceId of deviceIds) {
62938
62978
  const label = labelFor(deviceId);
62939
- if (result.status === "fulfilled") {
62979
+ try {
62980
+ await mutation.mutateAsync({
62981
+ deviceId,
62982
+ nodeId: "hub",
62983
+ addonId
62984
+ });
62940
62985
  succeeded.push(label);
62941
- return;
62986
+ } catch (reason) {
62987
+ const message = reason instanceof Error ? reason.message : String(reason);
62988
+ console.error(`[device-export] ${action} failed`, {
62989
+ tags: { deviceId: Number.parseInt(deviceId, 10) },
62990
+ addonId,
62991
+ error: message
62992
+ });
62993
+ failed.push({
62994
+ label,
62995
+ message
62996
+ });
62942
62997
  }
62943
- const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
62944
- console.error(`[device-export] ${action} failed`, {
62945
- tags: { deviceId: Number.parseInt(deviceId, 10) },
62946
- addonId,
62947
- error: message
62948
- });
62949
- failed.push({
62950
- label,
62951
- message
62952
- });
62953
- });
62998
+ }
62954
62999
  setOutcome({
62955
63000
  action,
62956
63001
  total: deviceIds.length,
@@ -63011,9 +63056,19 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63011
63056
  running
63012
63057
  ]);
63013
63058
  const [bulkFilters, setBulkFilters] = useState(NO_EXPORT_FILTERS);
63014
- const roleOptions = useMemo(() => roleFilterOptions(candidateDevices), [candidateDevices]);
63015
- const providerOptions = useMemo(() => providerFilterOptions(candidateDevices), [candidateDevices]);
63016
- const shownDevices = useMemo(() => applyExportFilters(candidateDevices, bulkFilters), [candidateDevices, bulkFilters]);
63059
+ const roleOptions = useMemo(() => roleFilterOptions(candidateDevices, (id) => id === "(none)" ? t("uiLibrary.deviceExport.noRole", "No role") : deviceRoleMetaOf(id, t).label).map((o) => ({
63060
+ value: o.id,
63061
+ label: o.label
63062
+ })), [candidateDevices, t]);
63063
+ const providerOptions = useMemo(() => providerFilterOptions(candidateDevices, integrations).map((o) => ({
63064
+ value: o.id,
63065
+ label: o.label
63066
+ })), [candidateDevices, integrations]);
63067
+ const shownDevices = useMemo(() => applyExportFilters(candidateDevices, bulkFilters, integrations), [
63068
+ candidateDevices,
63069
+ bulkFilters,
63070
+ integrations
63071
+ ]);
63017
63072
  const shownRows = useMemo(() => {
63018
63073
  const deviceRows = shownDevices.map((device) => ({
63019
63074
  kind: "device",
@@ -63126,72 +63181,86 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63126
63181
  toggleSelection,
63127
63182
  t
63128
63183
  ]);
63129
- const renderToolbar = useCallback(({ filtered: filteredRows }) => {
63184
+ const filterSelectors = /* @__PURE__ */ jsxs(Fragment$1, { children: [roleOptions.length > 1 && /* @__PURE__ */ jsx("div", {
63185
+ className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
63186
+ children: /* @__PURE__ */ jsx(OptionSelect, {
63187
+ mode: "multi",
63188
+ searchable: true,
63189
+ options: roleOptions,
63190
+ value: bulkFilters.roles,
63191
+ onChange: setRoleFilter,
63192
+ placeholder: t("uiLibrary.deviceExport.filterRole", "Role"),
63193
+ emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
63194
+ testId: "export-filter-role"
63195
+ })
63196
+ }), providerOptions.length > 1 && /* @__PURE__ */ jsx("div", {
63197
+ className: "min-w-36 flex-1 sm:w-44 sm:flex-none",
63198
+ children: /* @__PURE__ */ jsx(OptionSelect, {
63199
+ mode: "multi",
63200
+ searchable: true,
63201
+ options: providerOptions,
63202
+ value: bulkFilters.providers,
63203
+ onChange: setProviderFilter,
63204
+ placeholder: t("uiLibrary.deviceExport.filterIntegration", "Integration"),
63205
+ emptyMessage: t("uiLibrary.deviceExport.filterNoOption", "No match."),
63206
+ testId: "export-filter-integration"
63207
+ })
63208
+ })] });
63209
+ const renderBulkBar = useCallback(({ filtered: filteredRows }) => {
63130
63210
  const filtered = filteredRows.flatMap((row) => row.kind === "device" ? [row.device] : []);
63131
- return /* @__PURE__ */ jsxs("span", {
63132
- className: "inline-flex flex-wrap items-center gap-1.5",
63211
+ if (filtered.length === 0 && selectedIds.size === 0) return null;
63212
+ const hasSelection = selectedIds.size > 0;
63213
+ return /* @__PURE__ */ jsxs("div", {
63214
+ role: "toolbar",
63215
+ "aria-label": t("uiLibrary.deviceExport.bulkBar", "Bulk export"),
63216
+ className: `flex flex-wrap items-center gap-2 rounded-md border px-2.5 py-1.5 ${hasSelection ? "border-primary/40 bg-primary/10" : "border-border bg-surface"}`,
63133
63217
  children: [
63134
- /* @__PURE__ */ jsx(MultiSelectDropdown, {
63135
- label: t("uiLibrary.deviceExport.filterRole", "Role"),
63136
- options: roleOptions,
63137
- selected: bulkFilters.roles,
63138
- onChange: setRoleFilter
63139
- }),
63140
- /* @__PURE__ */ jsx(MultiSelectDropdown, {
63141
- label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
63142
- options: providerOptions,
63143
- selected: bulkFilters.providers,
63144
- onChange: setProviderFilter
63218
+ /* @__PURE__ */ jsx("span", {
63219
+ ...hasSelection ? { "data-testid": "export-selection-count" } : {},
63220
+ className: `text-[11px] font-medium ${hasSelection ? "text-foreground" : "text-foreground-subtle"}`,
63221
+ children: hasSelection ? t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size }) : t("uiLibrary.deviceExport.noneSelected", "None selected")
63145
63222
  }),
63146
63223
  /* @__PURE__ */ jsx(Button, {
63147
63224
  size: "sm",
63148
- variant: "secondary",
63225
+ variant: "ghost",
63149
63226
  disabled: filtered.length === 0,
63150
63227
  onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
63151
- children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
63228
+ children: t("uiLibrary.deviceExport.selectShown", "Select all {{count}} shown", { count: filtered.length })
63152
63229
  }),
63153
- selectedIds.size > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [
63230
+ hasSelection && /* @__PURE__ */ jsxs(Fragment$1, { children: [
63154
63231
  /* @__PURE__ */ jsx(Button, {
63155
63232
  size: "sm",
63156
63233
  variant: "ghost",
63157
63234
  onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
63158
63235
  children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
63159
63236
  }),
63160
- /* @__PURE__ */ jsx("span", {
63161
- "data-testid": "export-selection-count",
63162
- className: "text-[11px] text-foreground-subtle",
63163
- children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
63164
- }),
63165
- /* @__PURE__ */ jsx(Button, {
63166
- size: "sm",
63167
- variant: "primary",
63168
- disabled: busy,
63169
- onClick: exposeSelected,
63170
- children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
63171
- }),
63172
- /* @__PURE__ */ jsx(Button, {
63173
- size: "sm",
63174
- variant: "secondary",
63175
- disabled: busy,
63176
- onClick: unexposeSelected,
63177
- children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
63178
- }),
63179
63237
  /* @__PURE__ */ jsx(Button, {
63180
63238
  size: "sm",
63181
63239
  variant: "ghost",
63182
63240
  onClick: clearSelection,
63183
63241
  children: t("uiLibrary.deviceExport.clear", "Clear")
63242
+ }),
63243
+ /* @__PURE__ */ jsxs("span", {
63244
+ className: "ml-auto inline-flex flex-wrap items-center gap-1.5",
63245
+ children: [/* @__PURE__ */ jsx(Button, {
63246
+ size: "sm",
63247
+ variant: "primary",
63248
+ disabled: busy,
63249
+ onClick: exposeSelected,
63250
+ children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
63251
+ }), /* @__PURE__ */ jsx(Button, {
63252
+ size: "sm",
63253
+ variant: "secondary",
63254
+ disabled: busy,
63255
+ onClick: unexposeSelected,
63256
+ children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
63257
+ })]
63184
63258
  })
63185
63259
  ] })
63186
63260
  ]
63187
63261
  });
63188
63262
  }, [
63189
63263
  t,
63190
- roleOptions,
63191
- providerOptions,
63192
- bulkFilters,
63193
- setRoleFilter,
63194
- setProviderFilter,
63195
63264
  selectedIds.size,
63196
63265
  busy,
63197
63266
  exposeSelected,
@@ -63246,7 +63315,8 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
63246
63315
  filterOptions: typeFilterOptions,
63247
63316
  columns,
63248
63317
  onRowClick: onOpenDevice ? onRowClick : void 0,
63249
- toolbar: renderToolbar,
63318
+ filters: filterSelectors,
63319
+ belowFilters: renderBulkBar,
63250
63320
  pageSizeSelector: DEVICE_EXPORT_PAGE_SIZE,
63251
63321
  searchPlaceholder: t("uiLibrary.deviceExport.search", "Search by name or stableId"),
63252
63322
  emptyLabel: t("uiLibrary.deviceExport.noMatch", "No device matches this search.")
@@ -72875,7 +72945,7 @@ function TimelineSlider({ markers, bands = [], selectedId = null, step = 5, aria
72875
72945
  }
72876
72946
  //#endregion
72877
72947
  //#region src/ui-library-version.ts
72878
- var uiLibraryVersion = "1.2.372";
72948
+ var uiLibraryVersion = "1.2.373";
72879
72949
  //#endregion
72880
72950
  //#region src/hooks/use-account-unit-preferences.ts
72881
72951
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.372",
3
+ "version": "1.2.373",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/camstack/server.git",