@camstack/ui-library 1.2.300 → 1.2.302

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/dist/index.cjs CHANGED
@@ -11016,6 +11016,8 @@ var useDeviceStatusGetStatus = trpc.deviceStatus.getStatus.useQuery;
11016
11016
  var useDishwasherStart = trpc.dishwasher.start.useMutation;
11017
11017
  /** Generated alias around `trpc.dishwasher.pause.useMutation`. */
11018
11018
  var useDishwasherPause = trpc.dishwasher.pause.useMutation;
11019
+ /** Generated alias around `trpc.dishwasher.resume.useMutation`. */
11020
+ var useDishwasherResume = trpc.dishwasher.resume.useMutation;
11019
11021
  /** Generated alias around `trpc.dishwasher.stop.useMutation`. */
11020
11022
  var useDishwasherStop = trpc.dishwasher.stop.useMutation;
11021
11023
  /** Generated alias around `trpc.dishwasher.setProgram.useMutation`. */
@@ -11026,8 +11028,18 @@ var useDishwasherSetPower = trpc.dishwasher.setPower.useMutation;
11026
11028
  var useDishwasherSetHalfLoad = trpc.dishwasher.setHalfLoad.useMutation;
11027
11029
  /** Generated alias around `trpc.dishwasher.setHygienePlus.useMutation`. */
11028
11030
  var useDishwasherSetHygienePlus = trpc.dishwasher.setHygienePlus.useMutation;
11031
+ /** Generated alias around `trpc.dishwasher.setExtraDry.useMutation`. */
11032
+ var useDishwasherSetExtraDry = trpc.dishwasher.setExtraDry.useMutation;
11033
+ /** Generated alias around `trpc.dishwasher.setIntensiveZone.useMutation`. */
11034
+ var useDishwasherSetIntensiveZone = trpc.dishwasher.setIntensiveZone.useMutation;
11035
+ /** Generated alias around `trpc.dishwasher.setVarioSpeedPlus.useMutation`. */
11036
+ var useDishwasherSetVarioSpeedPlus = trpc.dishwasher.setVarioSpeedPlus.useMutation;
11037
+ /** Generated alias around `trpc.dishwasher.setChildLock.useMutation`. */
11038
+ var useDishwasherSetChildLock = trpc.dishwasher.setChildLock.useMutation;
11029
11039
  /** Generated alias around `trpc.dishwasher.setOption.useMutation`. */
11030
11040
  var useDishwasherSetOption = trpc.dishwasher.setOption.useMutation;
11041
+ /** Generated alias around `trpc.dishwasher.getOptions.useQuery`. */
11042
+ var useDishwasherGetOptions = trpc.dishwasher.getOptions.useQuery;
11031
11043
  /** Generated alias around `trpc.dishwasher.getStatus.useQuery`. */
11032
11044
  var useDishwasherGetStatus = trpc.dishwasher.getStatus.useQuery;
11033
11045
  /** Generated alias around `trpc.doorbell.getStatus.useQuery`. */
@@ -12354,6 +12366,8 @@ var useStorageMigrationStatus = trpc.storageMigration.status.useQuery;
12354
12366
  var useStorageMigrationCancel = trpc.storageMigration.cancel.useMutation;
12355
12367
  /** Generated alias around `trpc.storageMigration.movers.useQuery`. */
12356
12368
  var useStorageMigrationMovers = trpc.storageMigration.movers.useQuery;
12369
+ /** Generated alias around `trpc.storageMigration.moverClearance.useQuery`. */
12370
+ var useStorageMigrationMoverClearance = trpc.storageMigration.moverClearance.useQuery;
12357
12371
  /** Generated alias around `trpc.storageMigration.residue.useQuery`. */
12358
12372
  var useStorageMigrationResidue = trpc.storageMigration.residue.useQuery;
12359
12373
  /** Generated alias around `trpc.storageMigration.drain.useMutation`. */
@@ -28580,7 +28594,7 @@ var STATUS_DOT_STYLES = {
28580
28594
  idle: "bg-foreground-subtle/40",
28581
28595
  unknown: "bg-foreground-subtle/40"
28582
28596
  };
28583
- function formatDuration(ms) {
28597
+ function formatDuration$1(ms) {
28584
28598
  if (!Number.isFinite(ms) || ms < 0) return "—";
28585
28599
  if (ms < 1e3) return `${ms}ms`;
28586
28600
  const totalSec = Math.round(ms / 1e3);
@@ -28608,7 +28622,7 @@ function ObjectArrayCell({ kind, value }) {
28608
28622
  });
28609
28623
  if (kind === "duration" && typeof value === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28610
28624
  className: "font-mono text-[11px]",
28611
- children: formatDuration(value)
28625
+ children: formatDuration$1(value)
28612
28626
  });
28613
28627
  if (kind === "timestamp" && typeof value === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28614
28628
  className: "font-mono text-[11px]",
@@ -35108,7 +35122,7 @@ var ALIGN_CLASS = {
35108
35122
  right: "text-right",
35109
35123
  center: "text-center"
35110
35124
  };
35111
- function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyMessage, className, bordered = true, rowClassName, maxBodyHeight, fill = false, defaultSort, sort: sortProp, onSortChange, pageSize, page: pageProp, onPageChange, paginationLabels = DEFAULT_PAGINATION_LABELS, selection }) {
35125
+ function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyMessage, className, bordered = true, rowClassName, maxBodyHeight, fill = false, defaultSort, sort: sortProp, onSortChange, pageSize, page: pageProp, onPageChange, paginationLabels = DEFAULT_PAGINATION_LABELS, selection, renderExpanded, isExpanded }) {
35112
35126
  const [innerSort, setInnerSort] = (0, react$1.useState)(defaultSort ?? null);
35113
35127
  const [innerPage, setInnerPage] = (0, react$1.useState)(0);
35114
35128
  const sort = sortProp !== void 0 ? sortProp : innerSort;
@@ -35185,7 +35199,8 @@ function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyM
35185
35199
  const key = rowKey ? rowKey(row, rowIndex) : rowIndex;
35186
35200
  const extra = rowClassName?.(row, rowIndex) ?? "";
35187
35201
  const interactive = onRowClick !== void 0;
35188
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", {
35202
+ const detail = renderExpanded !== void 0 && isExpanded?.(row) === true ? renderExpanded(row, rowIndex) : null;
35203
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react$1.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", {
35189
35204
  onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
35190
35205
  className: [interactive ? "cursor-pointer hover:bg-primary/5" : "hover:bg-primary/5", extra].filter(Boolean).join(" "),
35191
35206
  children: shownColumns.map((col) => {
@@ -35201,7 +35216,15 @@ function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyM
35201
35216
  children: col.render(row, rowIndex)
35202
35217
  }, col.key);
35203
35218
  })
35204
- }, key);
35219
+ }), detail !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", {
35220
+ "data-expanded-row": "",
35221
+ className: "bg-surface",
35222
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
35223
+ colSpan: shownColumns.length,
35224
+ className: "border-b border-border px-3 py-2",
35225
+ children: detail
35226
+ })
35227
+ })] }, key);
35205
35228
  }) })]
35206
35229
  })
35207
35230
  });
@@ -37765,6 +37788,88 @@ function filterDataTableRows(rows, state, config) {
37765
37788
  });
37766
37789
  }
37767
37790
  //#endregion
37791
+ //#region src/composites/device-list/multi-select.tsx
37792
+ /**
37793
+ * Compact multi-select dropdown — a single chip-sized trigger ("Label · N")
37794
+ * that opens a checklist. Self-contained (outside-click / Escape dismiss), so
37795
+ * it stays compressed regardless of how many options exist or how little
37796
+ * horizontal room the container has. Shared by the device list's generic-mode
37797
+ * type filter and any other multi-select needs.
37798
+ */
37799
+ function MultiSelectDropdown({ label, options, selected, onChange }) {
37800
+ const t = useUiText();
37801
+ const [open, setOpen] = (0, react$1.useState)(false);
37802
+ const rootRef = (0, react$1.useRef)(null);
37803
+ (0, react$1.useEffect)(() => {
37804
+ if (!open) return;
37805
+ const onOutside = (e) => {
37806
+ const target = e.target instanceof Node ? e.target : null;
37807
+ if (target && rootRef.current && !rootRef.current.contains(target)) setOpen(false);
37808
+ };
37809
+ const onKey = (e) => {
37810
+ if (e.key === "Escape") setOpen(false);
37811
+ };
37812
+ document.addEventListener("mousedown", onOutside);
37813
+ document.addEventListener("touchstart", onOutside);
37814
+ document.addEventListener("keydown", onKey);
37815
+ return () => {
37816
+ document.removeEventListener("mousedown", onOutside);
37817
+ document.removeEventListener("touchstart", onOutside);
37818
+ document.removeEventListener("keydown", onKey);
37819
+ };
37820
+ }, [open]);
37821
+ const selectedSet = new Set(selected);
37822
+ const count = selected.length;
37823
+ const toggle = (id) => {
37824
+ const next = new Set(selectedSet);
37825
+ if (next.has(id)) next.delete(id);
37826
+ else next.add(id);
37827
+ onChange(options.filter((o) => next.has(o.id)).map((o) => o.id));
37828
+ };
37829
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
37830
+ ref: rootRef,
37831
+ className: "relative",
37832
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
37833
+ type: "button",
37834
+ onClick: () => setOpen((v) => !v),
37835
+ "aria-haspopup": "listbox",
37836
+ "aria-expanded": open,
37837
+ className: `inline-flex items-center gap-1.5 h-7 px-2.5 rounded-full border text-[10.5px] font-medium transition-colors ${count > 0 ? "border-primary/40 bg-primary/15 text-primary" : "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface-hover"}`,
37838
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, count > 0 ? ` · ${String(count)}` : ""] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: `h-3 w-3 transition-transform ${open ? "rotate-180" : ""}` })]
37839
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
37840
+ role: "listbox",
37841
+ className: "absolute left-0 top-full mt-1 z-30 min-w-[180px] max-h-72 overflow-y-auto rounded-lg border border-border bg-surface shadow-xl py-1",
37842
+ children: [count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
37843
+ type: "button",
37844
+ onClick: () => onChange([]),
37845
+ className: "flex w-full items-center px-3 py-1.5 text-left text-[10.5px] text-foreground-subtle hover:bg-surface-hover hover:text-foreground",
37846
+ children: t("uiLibrary.multiSelect.clear", "Clear selection")
37847
+ }), options.map((o) => {
37848
+ const active = selectedSet.has(o.id);
37849
+ const Icon = o.icon;
37850
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
37851
+ type: "button",
37852
+ role: "option",
37853
+ "aria-selected": active,
37854
+ onClick: () => toggle(o.id),
37855
+ className: "flex w-full items-center gap-2 px-3 py-1.5 text-left text-[10.5px] text-foreground hover:bg-surface-hover",
37856
+ children: [
37857
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
37858
+ className: `flex h-3.5 w-3.5 items-center justify-center rounded border shrink-0 ${active ? "border-primary bg-primary" : "border-border"}`,
37859
+ children: active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-2.5 w-2.5 text-primary-foreground" })
37860
+ }),
37861
+ Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" }),
37862
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
37863
+ className: "truncate",
37864
+ children: o.label
37865
+ })
37866
+ ]
37867
+ }, o.id);
37868
+ })]
37869
+ })]
37870
+ });
37871
+ }
37872
+ //#endregion
37768
37873
  //#region src/composites/data-table-filter-bar.tsx
37769
37874
  /**
37770
37875
  * The controls of one table's filter: a search box and the facet chips.
@@ -37817,7 +37922,18 @@ function DataTableFilterBar({ state, facets, labels, onSearch, onToggleFacet, on
37817
37922
  }, facet.value);
37818
37923
  })
37819
37924
  }),
37820
- groups.filter((group) => group.facets.length > 1).map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
37925
+ groups.filter((group) => group.facets.length > 1).map((group) => group.display === "menu" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
37926
+ label: group.label,
37927
+ options: group.facets.map((facet) => ({
37928
+ id: facet.value,
37929
+ label: `${group.facetLabel ? group.facetLabel(facet.value) : facet.value} (${String(facet.count)})`
37930
+ })),
37931
+ selected: group.selected,
37932
+ onChange: (next) => {
37933
+ const chosen = new Set(next);
37934
+ for (const facet of group.facets) if (chosen.has(facet.value) !== group.selected.includes(facet.value)) onToggleGroupFacet?.(group.id, facet.value);
37935
+ }
37936
+ }, group.id) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
37821
37937
  role: "group",
37822
37938
  "aria-label": group.label,
37823
37939
  className: "flex flex-wrap items-center gap-1",
@@ -48681,47 +48797,95 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
48681
48797
  *
48682
48798
  * Reads the `dishwasher` cap status slice and renders inline + hero surfaces
48683
48799
  * mirroring vacuum-control / pet-feeder patterns.
48800
+ *
48801
+ * UNKNOWN IS NOT OFF. The slice carries `null` for everything the appliance has not
48802
+ * reported, and this view renders that as "—" / a disabled control, never as an unchecked
48803
+ * box or a zero. It also does not echo a command before the appliance confirms it: the
48804
+ * slice moves when the appliance says so.
48684
48805
  */
48685
- function dishwasherStateMeta(state, t = defaultUiText) {
48686
- switch (state) {
48687
- case "running": return {
48688
- label: t("uiLibrary.dishwasher.running", "Running"),
48806
+ /** What the operation state is called to a person, and how it is toned. */
48807
+ function dishwasherOperationMeta(operationState, t = defaultUiText) {
48808
+ switch (operationState) {
48809
+ case "Run": return {
48810
+ label: t("uiLibrary.dishwasher.op.run", "Running"),
48689
48811
  tone: "text-primary"
48690
48812
  };
48691
- case "paused": return {
48692
- label: t("uiLibrary.dishwasher.paused", "Paused"),
48813
+ case "Pause": return {
48814
+ label: t("uiLibrary.dishwasher.op.pause", "Paused"),
48693
48815
  tone: "text-warning"
48694
48816
  };
48695
- case "finished": return {
48696
- label: t("uiLibrary.dishwasher.finished", "Finished"),
48817
+ case "Finished": return {
48818
+ label: t("uiLibrary.dishwasher.op.finished", "Finished"),
48697
48819
  tone: "text-success"
48698
48820
  };
48699
- case "error": return {
48700
- label: t("uiLibrary.dishwasher.error", "Error"),
48701
- tone: "text-destructive"
48821
+ case "Error": return {
48822
+ label: t("uiLibrary.dishwasher.op.error", "Error"),
48823
+ tone: "text-danger"
48702
48824
  };
48703
- case "unavailable": return {
48704
- label: t("uiLibrary.dishwasher.unavailable", "Unavailable"),
48705
- tone: "text-foreground-subtle"
48825
+ case "Aborting": return {
48826
+ label: t("uiLibrary.dishwasher.op.aborting", "Stopping"),
48827
+ tone: "text-warning"
48706
48828
  };
48707
- case "idle": return {
48708
- label: t("uiLibrary.dishwasher.idle", "Idle"),
48829
+ case "ActionRequired": return {
48830
+ label: t("uiLibrary.dishwasher.op.actionRequired", "Action required"),
48831
+ tone: "text-warning"
48832
+ };
48833
+ case "DelayedStart": return {
48834
+ label: t("uiLibrary.dishwasher.op.delayedStart", "Delayed start"),
48835
+ tone: "text-primary"
48836
+ };
48837
+ case "Ready": return {
48838
+ label: t("uiLibrary.dishwasher.op.ready", "Ready"),
48839
+ tone: "text-foreground"
48840
+ };
48841
+ case "Inactive": return {
48842
+ label: t("uiLibrary.dishwasher.op.inactive", "Inactive"),
48709
48843
  tone: "text-foreground-subtle"
48710
48844
  };
48711
- case void 0: return {
48845
+ case null: return {
48712
48846
  label: t("uiLibrary.dishwasher.unknown", "Unknown"),
48713
48847
  tone: "text-foreground-subtle"
48714
48848
  };
48715
48849
  }
48716
48850
  }
48851
+ /** The headline for the whole slice: the appliance's state, or why there is none. */
48852
+ function dishwasherHeadline(status, t = defaultUiText) {
48853
+ switch (status.availability) {
48854
+ case "offline": return {
48855
+ label: t("uiLibrary.dishwasher.offline", "Offline"),
48856
+ tone: "text-foreground-subtle"
48857
+ };
48858
+ case "description-missing": return {
48859
+ label: t("uiLibrary.dishwasher.descriptionMissing", "Waiting for the appliance profile"),
48860
+ tone: "text-warning"
48861
+ };
48862
+ case "available": return dishwasherOperationMeta(status.operationState, t);
48863
+ }
48864
+ }
48717
48865
  function consumableTone(level) {
48718
48866
  switch (level) {
48719
48867
  case "ok": return "text-success";
48720
48868
  case "low": return "text-warning";
48721
- case "empty": return "text-destructive";
48869
+ case "empty": return "text-danger";
48722
48870
  case null: return "text-foreground-subtle";
48723
48871
  }
48724
48872
  }
48873
+ /** Last segment of a feature key, spaced out: `Dishcare.Dishwasher.Program.Eco50` -> `Eco50`. */
48874
+ function featureLabel(key) {
48875
+ return (key.split(".").pop() ?? key).replace(/([a-z0-9])([A-Z])/g, "$1 $2");
48876
+ }
48877
+ /** Whole seconds as a short duration: `1 h 24 min`, `42 min`, `30 s`. */
48878
+ function formatDuration(seconds, t = defaultUiText) {
48879
+ if (seconds < 60) return t("uiLibrary.dishwasher.durationSeconds", "{{n}} s", { n: seconds });
48880
+ const minutes = Math.round(seconds / 60);
48881
+ if (minutes < 60) return t("uiLibrary.dishwasher.durationMinutes", "{{n}} min", { n: minutes });
48882
+ const hours = Math.floor(minutes / 60);
48883
+ const rest = minutes % 60;
48884
+ return rest === 0 ? t("uiLibrary.dishwasher.durationHours", "{{n}} h", { n: hours }) : t("uiLibrary.dishwasher.durationHoursMinutes", "{{h}} h {{m}} min", {
48885
+ h: hours,
48886
+ m: rest
48887
+ });
48888
+ }
48725
48889
  function useDishwasher(trpc, deviceId) {
48726
48890
  return useDeviceCapability({
48727
48891
  trpc,
@@ -48755,13 +48919,11 @@ function ConsumableBadge({ label, level }) {
48755
48919
  ]
48756
48920
  });
48757
48921
  }
48758
- function DishwasherGraphic({ state, progressPercent }) {
48759
- const running = state === "running";
48922
+ function DishwasherGraphic({ running, progressPercent }) {
48760
48923
  const pct = progressPercent != null ? Math.max(0, Math.min(100, progressPercent)) : running ? 40 : 0;
48761
48924
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48762
48925
  "aria-hidden": "true",
48763
48926
  className: "relative w-28 h-36 rounded border-2 border-border bg-gradient-to-b from-surface-hover to-surface overflow-hidden flex-shrink-0",
48764
- style: { boxShadow: "inset 0 0 0 2px rgba(255,255,255,.06)" },
48765
48927
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48766
48928
  className: cn("absolute inset-x-0 bottom-0 bg-primary/25 transition-[height] duration-500", running && "animate-pulse"),
48767
48929
  style: { height: `${pct}%` }
@@ -48771,61 +48933,73 @@ function DishwasherGraphic({ state, progressPercent }) {
48771
48933
  })]
48772
48934
  });
48773
48935
  }
48774
- function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48936
+ function DishwasherPanel({ trpc, deviceId, layout }) {
48775
48937
  const t = useUiText();
48776
48938
  const { cap, slice, available } = useDishwasher(trpc, deviceId);
48777
- const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
48778
48939
  if (cap === void 0) return null;
48779
- if (display === void 0) return null;
48780
- const busy = !available;
48781
- const meta = dishwasherStateMeta(display.state, t);
48782
- const running = display.state === "running";
48783
- const paused = display.state === "paused";
48784
- const press = (fn, patch) => {
48940
+ if (slice === void 0) return null;
48941
+ const display = slice;
48942
+ const link = display.availability === "available";
48943
+ const busy = !available || !link;
48944
+ const optionTogglesLocked = display.childLock === true;
48945
+ const meta = dishwasherHeadline(display, t);
48946
+ const running = display.operationState === "Run";
48947
+ const paused = display.operationState === "Pause";
48948
+ const startable = display.remoteStartAllowed === true && display.selectedProgram !== null;
48949
+ const press = (fn) => {
48785
48950
  if (busy) return;
48786
- applyOptimistic(patch);
48787
48951
  fn();
48788
48952
  };
48789
- if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48790
- className: "inline-flex items-center gap-1",
48791
- children: [
48792
- display.doorOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48793
- className: "inline-flex text-warning",
48794
- title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
48795
- "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
48796
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
48797
- }),
48798
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48799
- className: cn("text-xs font-medium tabular-nums", meta.tone),
48800
- children: meta.label
48801
- }),
48802
- display.remainingMinutes != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48803
- className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums",
48804
- children: [
48805
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }),
48806
- display.remainingMinutes,
48807
- "m"
48808
- ]
48809
- }),
48810
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48811
- label: t("uiLibrary.dishwasher.start", "Start"),
48812
- busy: busy || !display.remoteStartAllowed,
48813
- active: running,
48814
- onClick: () => press(() => cap.start({}), {
48815
- state: "running",
48816
- remoteStartAllowed: false
48953
+ if (layout === "compact") {
48954
+ const saltWarn = display.saltLevel === "low" || display.saltLevel === "empty";
48955
+ const rinseWarn = display.rinseAidLevel === "low" || display.rinseAidLevel === "empty";
48956
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48957
+ className: "inline-flex items-center gap-1.5 max-w-full",
48958
+ children: [
48959
+ display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48960
+ className: "inline-flex text-warning shrink-0",
48961
+ title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
48962
+ "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
48963
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" })
48817
48964
  }),
48818
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
48819
- }),
48820
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48821
- label: t("uiLibrary.dishwasher.pause", "Pause"),
48822
- busy: busy || !running,
48823
- active: paused,
48824
- onClick: () => press(() => cap.pause({}), { state: "paused" }),
48825
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
48826
- })
48827
- ]
48828
- });
48965
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48966
+ className: cn("text-xs font-medium tabular-nums shrink-0", meta.tone),
48967
+ children: meta.label
48968
+ }),
48969
+ display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48970
+ className: "text-xs text-foreground-subtle truncate max-w-[7rem]",
48971
+ title: display.program,
48972
+ children: featureLabel(display.program)
48973
+ }),
48974
+ display.remainingSeconds != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48975
+ className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
48976
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remainingSeconds, t)]
48977
+ }),
48978
+ saltWarn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48979
+ className: cn("text-[10px] font-medium shrink-0", consumableTone(display.saltLevel)),
48980
+ children: t("uiLibrary.dishwasher.salt", "Salt")
48981
+ }),
48982
+ rinseWarn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48983
+ className: cn("text-[10px] font-medium shrink-0", consumableTone(display.rinseAidLevel)),
48984
+ children: t("uiLibrary.dishwasher.rinseAid", "Rinse")
48985
+ }),
48986
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48987
+ label: t("uiLibrary.dishwasher.start", "Start"),
48988
+ busy: busy || !startable,
48989
+ active: running,
48990
+ onClick: () => press(() => cap.start({})),
48991
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
48992
+ }),
48993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48994
+ label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
48995
+ busy: busy || !(running || paused),
48996
+ active: paused,
48997
+ onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
48998
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
48999
+ })
49000
+ ]
49001
+ });
49002
+ }
48829
49003
  const programs = display.availablePrograms;
48830
49004
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48831
49005
  className: "flex flex-col gap-3 rounded-lg border border-border bg-surface p-4",
@@ -48833,7 +49007,7 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48833
49007
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48834
49008
  className: "flex items-start gap-4",
48835
49009
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
48836
- state: display.state,
49010
+ running,
48837
49011
  progressPercent: display.progressPercent
48838
49012
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48839
49013
  className: "flex min-w-0 flex-1 flex-col gap-2",
@@ -48843,14 +49017,23 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48843
49017
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48844
49018
  className: cn("text-3xl font-semibold", meta.tone),
48845
49019
  children: meta.label
48846
- }), !display.powerOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49020
+ }), display.powerOn === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48847
49021
  className: "text-xs font-medium text-foreground-subtle",
48848
49022
  children: t("uiLibrary.dishwasher.powerOff", "Power off")
48849
49023
  })]
48850
49024
  }),
48851
49025
  display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48852
49026
  className: "text-sm text-foreground-subtle",
48853
- children: display.program
49027
+ title: display.program,
49028
+ children: featureLabel(display.program)
49029
+ }),
49030
+ display.remainingSeconds != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
49031
+ className: "inline-flex items-center gap-1 text-sm text-foreground-subtle tabular-nums",
49032
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.remaining", "{{time}} left", { time: formatDuration(display.remainingSeconds, t) })]
49033
+ }),
49034
+ display.delayedStartSeconds != null && display.delayedStartSeconds > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49035
+ className: "text-xs text-foreground-subtle tabular-nums",
49036
+ children: t("uiLibrary.dishwasher.startsIn", "Starts in {{time}}", { time: formatDuration(display.delayedStartSeconds, t) })
48854
49037
  }),
48855
49038
  display.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48856
49039
  className: "flex items-center gap-2",
@@ -48876,53 +49059,52 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48876
49059
  label: t("uiLibrary.dishwasher.rinseAid", "Rinse aid"),
48877
49060
  level: display.rinseAidLevel
48878
49061
  }),
48879
- display.doorOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
49062
+ display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48880
49063
  className: "inline-flex items-center gap-1 text-xs text-warning",
48881
49064
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
48882
- })
49065
+ }),
49066
+ display.activeEvents.map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49067
+ className: "text-xs text-warning",
49068
+ title: event,
49069
+ children: featureLabel(event)
49070
+ }, event))
48883
49071
  ]
48884
49072
  }),
48885
- display.errorCode != null && display.errorCode !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48886
- className: "inline-flex items-center gap-1 text-xs text-destructive",
48887
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display.errorLabel ?? t("uiLibrary.dishwasher.errorCode", "Error {{code}}", { code: display.errorCode }) })]
49073
+ display.operationState === "Error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
49074
+ className: "inline-flex items-center gap-1 text-xs text-danger",
49075
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-3.5 w-3.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display.errorLabel != null ? featureLabel(display.errorLabel) : display.errorCode != null ? t("uiLibrary.dishwasher.errorCode", "Error {{code}}", { code: display.errorCode }) : t("uiLibrary.dishwasher.op.error", "Error") })]
48888
49076
  }),
48889
49077
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48890
49078
  className: "flex flex-wrap items-center gap-2",
48891
49079
  children: [
48892
49080
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48893
49081
  label: t("uiLibrary.dishwasher.start", "Start"),
48894
- busy: busy || !display.remoteStartAllowed,
49082
+ busy: busy || !startable,
48895
49083
  active: running,
48896
- onClick: () => press(() => cap.start({}), {
48897
- state: "running",
48898
- remoteStartAllowed: false
48899
- }),
49084
+ onClick: () => press(() => cap.start({})),
48900
49085
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" })
48901
49086
  }),
48902
49087
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48903
- label: t("uiLibrary.dishwasher.pause", "Pause"),
48904
- busy: busy || !running,
49088
+ label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
49089
+ busy: busy || !(running || paused),
48905
49090
  active: paused,
48906
- onClick: () => press(() => cap.pause({}), { state: "paused" }),
49091
+ onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
48907
49092
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" })
48908
49093
  }),
48909
49094
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48910
49095
  label: t("uiLibrary.dishwasher.stop", "Stop"),
48911
- busy,
49096
+ busy: busy || !(running || paused),
48912
49097
  active: false,
48913
- onClick: () => press(() => cap.stop({}), {
48914
- state: "idle",
48915
- remainingMinutes: null
48916
- }),
49098
+ onClick: () => press(() => cap.stop({})),
48917
49099
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-3.5 w-3.5" })
48918
49100
  }),
48919
49101
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48920
49102
  label: t("uiLibrary.dishwasher.power", "Power"),
48921
- busy,
48922
- active: display.powerOn,
49103
+ busy: busy || display.powerOn === null,
49104
+ active: display.powerOn === true,
48923
49105
  onClick: () => {
48924
- const next = !display.powerOn;
48925
- press(() => cap.setPower({ on: next }), { powerOn: next });
49106
+ const next = display.powerOn !== true;
49107
+ press(() => cap.setPower({ on: next }));
48926
49108
  },
48927
49109
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, { className: "h-4 w-4" })
48928
49110
  })
@@ -48938,65 +49120,106 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48938
49120
  children: t("uiLibrary.dishwasher.program", "Program")
48939
49121
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
48940
49122
  "aria-label": t("uiLibrary.dishwasher.program", "Program"),
48941
- value: display.program ?? "",
49123
+ value: display.selectedProgram ?? "",
48942
49124
  disabled: busy,
48943
49125
  onChange: (e) => {
48944
49126
  const program = e.target.value;
48945
49127
  if (!program) return;
48946
- applyOptimistic({ program });
48947
49128
  cap.setProgram({ program });
48948
49129
  },
48949
49130
  onClick: (e) => {
48950
49131
  e.stopPropagation();
48951
49132
  },
48952
49133
  className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
48953
- children: [display.program == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
49134
+ children: [display.selectedProgram == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
48954
49135
  value: "",
48955
49136
  children: "—"
48956
49137
  }), programs.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
48957
49138
  value: p,
48958
- children: p
49139
+ children: featureLabel(p)
48959
49140
  }, p))]
48960
49141
  })]
48961
49142
  }),
48962
49143
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48963
49144
  className: "flex flex-wrap gap-4 text-xs",
48964
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
48965
- label: t("uiLibrary.dishwasher.halfLoad", "Half load"),
48966
- checked: display.halfLoad,
48967
- busy,
48968
- onChange: (on) => {
48969
- applyOptimistic({ halfLoad: on });
48970
- cap.setHalfLoad({ on });
48971
- }
48972
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
48973
- label: t("uiLibrary.dishwasher.hygienePlus", "Hygiene plus"),
48974
- checked: display.hygienePlus,
48975
- busy,
48976
- onChange: (on) => {
48977
- applyOptimistic({ hygienePlus: on });
48978
- cap.setHygienePlus({ on });
48979
- }
48980
- })]
49145
+ children: [
49146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49147
+ label: t("uiLibrary.dishwasher.halfLoad", "Half load"),
49148
+ checked: display.halfLoad,
49149
+ shown: link,
49150
+ busy: busy || optionTogglesLocked,
49151
+ onChange: (on) => void cap.setHalfLoad({ on })
49152
+ }),
49153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49154
+ label: t("uiLibrary.dishwasher.hygienePlus", "Hygiene plus"),
49155
+ checked: display.hygienePlus,
49156
+ shown: link,
49157
+ busy: busy || optionTogglesLocked,
49158
+ onChange: (on) => void cap.setHygienePlus({ on })
49159
+ }),
49160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49161
+ label: t("uiLibrary.dishwasher.extraDry", "Extra dry"),
49162
+ checked: display.extraDry,
49163
+ shown: link,
49164
+ busy: busy || optionTogglesLocked,
49165
+ onChange: (on) => void cap.setExtraDry({ on })
49166
+ }),
49167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49168
+ label: t("uiLibrary.dishwasher.intensiveZone", "Intensive zone"),
49169
+ checked: display.intensiveZone,
49170
+ shown: link,
49171
+ busy: busy || optionTogglesLocked,
49172
+ onChange: (on) => void cap.setIntensiveZone({ on })
49173
+ }),
49174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49175
+ label: t("uiLibrary.dishwasher.varioSpeedPlus", "VarioSpeed+"),
49176
+ checked: display.varioSpeedPlus,
49177
+ shown: link,
49178
+ busy: busy || optionTogglesLocked,
49179
+ onChange: (on) => void cap.setVarioSpeedPlus({ on })
49180
+ }),
49181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49182
+ label: t("uiLibrary.dishwasher.childLock", "Child lock"),
49183
+ checked: display.childLock,
49184
+ shown: link,
49185
+ busy,
49186
+ onChange: (on) => void cap.setChildLock({ on })
49187
+ })
49188
+ ]
48981
49189
  })
48982
49190
  ]
48983
49191
  });
48984
49192
  }
48985
- function ToggleRow({ label, checked, busy, onChange }) {
49193
+ /**
49194
+ * One boolean setting. `checked === null` is "the appliance has not said": on a reachable
49195
+ * appliance that means it does not offer the setting (hidden); on an unreachable one the
49196
+ * setting is shown but disabled and marked unknown — never as an unchecked box.
49197
+ */
49198
+ function ToggleRow({ label, checked, shown, busy, onChange }) {
49199
+ const t = useUiText();
49200
+ if (checked === null && shown) return null;
48986
49201
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
48987
49202
  className: "inline-flex items-center gap-2 cursor-pointer",
48988
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
48989
- type: "checkbox",
48990
- checked,
48991
- disabled: busy,
48992
- onChange: (e) => {
48993
- onChange(e.target.checked);
48994
- },
48995
- onClick: (e) => {
48996
- e.stopPropagation();
48997
- },
48998
- className: "rounded border-border"
48999
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
49203
+ children: [
49204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
49205
+ type: "checkbox",
49206
+ checked: checked === true,
49207
+ "aria-checked": checked === null ? "mixed" : checked,
49208
+ disabled: busy || checked === null,
49209
+ onChange: (e) => {
49210
+ onChange(e.target.checked);
49211
+ },
49212
+ onClick: (e) => {
49213
+ e.stopPropagation();
49214
+ },
49215
+ className: "rounded border-border"
49216
+ }),
49217
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
49218
+ checked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49219
+ className: "text-foreground-subtle",
49220
+ children: t("uiLibrary.dishwasher.unknownValue", "—")
49221
+ })
49222
+ ]
49000
49223
  });
49001
49224
  }
49002
49225
  function DishwasherInlineControl(props) {
@@ -54628,88 +54851,6 @@ function PageSizeSelect({ value, options, onChange, ariaLabel }) {
54628
54851
  });
54629
54852
  }
54630
54853
  //#endregion
54631
- //#region src/composites/device-list/multi-select.tsx
54632
- /**
54633
- * Compact multi-select dropdown — a single chip-sized trigger ("Label · N")
54634
- * that opens a checklist. Self-contained (outside-click / Escape dismiss), so
54635
- * it stays compressed regardless of how many options exist or how little
54636
- * horizontal room the container has. Shared by the device list's generic-mode
54637
- * type filter and any other multi-select needs.
54638
- */
54639
- function MultiSelectDropdown({ label, options, selected, onChange }) {
54640
- const t = useUiText();
54641
- const [open, setOpen] = (0, react$1.useState)(false);
54642
- const rootRef = (0, react$1.useRef)(null);
54643
- (0, react$1.useEffect)(() => {
54644
- if (!open) return;
54645
- const onOutside = (e) => {
54646
- const target = e.target instanceof Node ? e.target : null;
54647
- if (target && rootRef.current && !rootRef.current.contains(target)) setOpen(false);
54648
- };
54649
- const onKey = (e) => {
54650
- if (e.key === "Escape") setOpen(false);
54651
- };
54652
- document.addEventListener("mousedown", onOutside);
54653
- document.addEventListener("touchstart", onOutside);
54654
- document.addEventListener("keydown", onKey);
54655
- return () => {
54656
- document.removeEventListener("mousedown", onOutside);
54657
- document.removeEventListener("touchstart", onOutside);
54658
- document.removeEventListener("keydown", onKey);
54659
- };
54660
- }, [open]);
54661
- const selectedSet = new Set(selected);
54662
- const count = selected.length;
54663
- const toggle = (id) => {
54664
- const next = new Set(selectedSet);
54665
- if (next.has(id)) next.delete(id);
54666
- else next.add(id);
54667
- onChange(options.filter((o) => next.has(o.id)).map((o) => o.id));
54668
- };
54669
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54670
- ref: rootRef,
54671
- className: "relative",
54672
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
54673
- type: "button",
54674
- onClick: () => setOpen((v) => !v),
54675
- "aria-haspopup": "listbox",
54676
- "aria-expanded": open,
54677
- className: `inline-flex items-center gap-1.5 h-7 px-2.5 rounded-full border text-[10.5px] font-medium transition-colors ${count > 0 ? "border-primary/40 bg-primary/15 text-primary" : "border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-surface-hover"}`,
54678
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, count > 0 ? ` · ${String(count)}` : ""] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: `h-3 w-3 transition-transform ${open ? "rotate-180" : ""}` })]
54679
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54680
- role: "listbox",
54681
- className: "absolute left-0 top-full mt-1 z-30 min-w-[180px] max-h-72 overflow-y-auto rounded-lg border border-border bg-surface shadow-xl py-1",
54682
- children: [count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
54683
- type: "button",
54684
- onClick: () => onChange([]),
54685
- className: "flex w-full items-center px-3 py-1.5 text-left text-[10.5px] text-foreground-subtle hover:bg-surface-hover hover:text-foreground",
54686
- children: t("uiLibrary.multiSelect.clear", "Clear selection")
54687
- }), options.map((o) => {
54688
- const active = selectedSet.has(o.id);
54689
- const Icon = o.icon;
54690
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
54691
- type: "button",
54692
- role: "option",
54693
- "aria-selected": active,
54694
- onClick: () => toggle(o.id),
54695
- className: "flex w-full items-center gap-2 px-3 py-1.5 text-left text-[10.5px] text-foreground hover:bg-surface-hover",
54696
- children: [
54697
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54698
- className: `flex h-3.5 w-3.5 items-center justify-center rounded border shrink-0 ${active ? "border-primary bg-primary" : "border-border"}`,
54699
- children: active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-2.5 w-2.5 text-primary-foreground" })
54700
- }),
54701
- Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" }),
54702
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54703
- className: "truncate",
54704
- children: o.label
54705
- })
54706
- ]
54707
- }, o.id);
54708
- })]
54709
- })]
54710
- });
54711
- }
54712
- //#endregion
54713
54854
  //#region src/composites/device-list/type-filter.tsx
54714
54855
  /**
54715
54856
  * Shared, multi-select type filter that adapts to its OWN available width
@@ -59402,19 +59543,35 @@ function objectFromJson(text) {
59402
59543
  return null;
59403
59544
  }
59404
59545
  }
59405
- function RecordEntrySecretField({ label, required, value, disabled, onPatch }) {
59406
- const [shown, setShown] = (0, react$1.useState)(false);
59407
- const stored = isRedactedSecret(value);
59546
+ /**
59547
+ * What the secret control is hydrated with. A read hands a stored secret as a
59548
+ * VIEW (`{ $secret: <row ref>, set: true }`) the control reveals by ref through
59549
+ * the host's audited reveal; the legacy redaction token is "set, no ref" (it
59550
+ * can be replaced but not revealed); anything else is "nothing stored".
59551
+ */
59552
+ function hydratedSecret(value) {
59553
+ if ((0, _camstack_types.isSecretFieldView)(value) || (0, _camstack_types.isSecretClear)(value)) return value;
59554
+ if (isRedactedSecret(value)) return {
59555
+ $secret: null,
59556
+ v: 1,
59557
+ set: true
59558
+ };
59559
+ return null;
59560
+ }
59561
+ function RecordEntrySecretField({ name, label, required, value, disabled, onPatch }) {
59408
59562
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
59409
59563
  label,
59410
59564
  required,
59411
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PasswordInput, {
59412
- value: secretDisplayValue(value),
59413
- onValueChange: (text) => onPatch(mergeSecretForSubmit(text, value)),
59414
- shown,
59415
- onToggleShown: () => setShown((s) => !s),
59416
- disabled,
59417
- placeholder: stored ? SECRET_MASK_PLACEHOLDER : `Enter ${label.toLowerCase()}`
59565
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretFieldControl, {
59566
+ field: {
59567
+ type: "password",
59568
+ key: name,
59569
+ label,
59570
+ required
59571
+ },
59572
+ value: hydratedSecret(value),
59573
+ onChange: onPatch,
59574
+ disabled
59418
59575
  })
59419
59576
  });
59420
59577
  }
@@ -59422,6 +59579,7 @@ function SchemaField(props) {
59422
59579
  const { path, name, node, value, secretPaths, required, disabled, onPatch } = props;
59423
59580
  const label = labelFor(name, node);
59424
59581
  if (isSecretField(path, secretPaths, node)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordEntrySecretField, {
59582
+ name,
59425
59583
  label,
59426
59584
  required,
59427
59585
  value,
@@ -59571,7 +59729,7 @@ function defaultBody(schema) {
59571
59729
  }
59572
59730
  return out;
59573
59731
  }
59574
- function RecordEntriesPanel({ kind }) {
59732
+ function RecordEntriesPanel({ kind, rowActions, extraColumns = [], showUpdated = true, minWidthPx }) {
59575
59733
  const t = useUiText();
59576
59734
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
59577
59735
  const confirm = useConfirm();
@@ -59644,41 +59802,51 @@ function RecordEntriesPanel({ kind }) {
59644
59802
  })
59645
59803
  })
59646
59804
  },
59647
- {
59805
+ ...showUpdated ? [{
59648
59806
  key: "updated",
59649
59807
  header: t("uiLibrary.recordEntries.colUpdated", "Updated"),
59650
59808
  nowrap: true,
59651
59809
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59652
59810
  className: "text-[11px] text-foreground-subtle",
59653
- children: new Date(row.updatedAt).toLocaleString()
59811
+ title: new Date(row.updatedAt).toLocaleString(),
59812
+ children: new Date(row.updatedAt).toLocaleDateString()
59654
59813
  })
59655
- },
59814
+ }] : [],
59815
+ ...extraColumns,
59656
59816
  {
59657
59817
  key: "actions",
59658
59818
  header: "",
59659
59819
  align: "right",
59660
59820
  nowrap: true,
59821
+ width: "w-px",
59661
59822
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
59662
59823
  className: "flex justify-end gap-1",
59663
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59664
- size: "sm",
59665
- variant: "ghost",
59666
- type: "button",
59667
- onClick: () => openEdit(row),
59668
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pencil, { className: "h-3.5 w-3.5" })
59669
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59670
- size: "sm",
59671
- variant: "ghost",
59672
- type: "button",
59673
- onClick: () => void onDelete(row),
59674
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5 text-danger" })
59675
- })]
59824
+ children: [
59825
+ rowActions?.(row),
59826
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59827
+ size: "sm",
59828
+ variant: "ghost",
59829
+ type: "button",
59830
+ onClick: () => openEdit(row),
59831
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pencil, { className: "h-3.5 w-3.5" })
59832
+ }),
59833
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59834
+ size: "sm",
59835
+ variant: "ghost",
59836
+ type: "button",
59837
+ onClick: () => void onDelete(row),
59838
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5 text-danger" })
59839
+ })
59840
+ ]
59676
59841
  })
59677
59842
  }
59678
59843
  ];
59679
59844
  }, [
59845
+ extraColumns,
59680
59846
  onDelete,
59681
59847
  openEdit,
59848
+ rowActions,
59849
+ showUpdated,
59682
59850
  t
59683
59851
  ]);
59684
59852
  const dialogOpen = formUrl.state.mode !== "closed";
@@ -59726,6 +59894,7 @@ function RecordEntriesPanel({ kind }) {
59726
59894
  title: t("uiLibrary.recordEntries.emptyTitle", "No entries yet"),
59727
59895
  description: t("uiLibrary.recordEntries.emptyDescription", "Add the first row for this kind.")
59728
59896
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTable, {
59897
+ ...minWidthPx !== void 0 ? { minWidthPx } : {},
59729
59898
  columns,
59730
59899
  rows: entries,
59731
59900
  rowKey: (r) => r.id,
@@ -59756,73 +59925,6 @@ function RecordEntriesPanel({ kind }) {
59756
59925
  });
59757
59926
  }
59758
59927
  //#endregion
59759
- //#region src/composites/record-entries/record-entries-hub.tsx
59760
- var RECORD_ENTRY_OWNER_PARAM = "entryOwner";
59761
- var RECORD_ENTRY_KIND_PARAM = "entryKind";
59762
- function kindKey$1(k) {
59763
- return `${k.owner}/${k.kind}`;
59764
- }
59765
- function RecordEntriesHub({ filter }) {
59766
- const t = useUiText();
59767
- const kindsQuery = useRecordEntriesListEntryKinds({}, { staleTime: 3e4 });
59768
- const [ownerParam, setOwner] = useUrlQueryParam(RECORD_ENTRY_OWNER_PARAM);
59769
- const [kindParam, setKind] = useUrlQueryParam(RECORD_ENTRY_KIND_PARAM);
59770
- const kinds = (0, react$1.useMemo)(() => {
59771
- return (kindsQuery.data?.kinds ?? []).filter((k) => {
59772
- if (filter?.owner !== void 0 && k.owner !== filter.owner) return false;
59773
- if (filter?.kind !== void 0 && k.kind !== filter.kind) return false;
59774
- return true;
59775
- });
59776
- }, [
59777
- filter?.kind,
59778
- filter?.owner,
59779
- kindsQuery.data?.kinds
59780
- ]);
59781
- const active = (0, react$1.useMemo)(() => {
59782
- if (kinds.length === 0) return null;
59783
- return kinds.find((k) => k.owner === ownerParam && k.kind === kindParam) ?? kinds[0] ?? null;
59784
- }, [
59785
- kindParam,
59786
- kinds,
59787
- ownerParam
59788
- ]);
59789
- const selectKind = (k) => {
59790
- setOwner(k.owner);
59791
- setKind(k.kind);
59792
- };
59793
- if (kindsQuery.isError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: kindsQuery.error instanceof Error ? kindsQuery.error.message : t("uiLibrary.recordEntries.kindsFailed", "Could not load entry kinds") });
59794
- if (kindsQuery.isPending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
59795
- className: "flex justify-center py-10 text-foreground-subtle",
59796
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
59797
- });
59798
- if (kinds.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
59799
- title: t("uiLibrary.recordEntries.noKindsTitle", "No entry kinds"),
59800
- description: t("uiLibrary.recordEntries.noKindsDescription", "No addon has declared an entry record kind yet.")
59801
- });
59802
- const activeKey = active === null ? "" : kindKey$1(active);
59803
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
59804
- className: "space-y-4",
59805
- children: [kinds.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tabs, {
59806
- appearance: "pills",
59807
- value: activeKey,
59808
- onValueChange: (key) => {
59809
- const found = kinds.find((k) => kindKey$1(k) === key);
59810
- if (found !== void 0) selectKind(found);
59811
- },
59812
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabsList, {
59813
- className: "flex flex-wrap gap-1",
59814
- children: kinds.map((k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TabsTrigger, {
59815
- value: kindKey$1(k),
59816
- children: [k.title, k.rows > 0 ? ` (${k.rows})` : ""]
59817
- }, kindKey$1(k)))
59818
- })
59819
- }), active !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [kinds.length === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
59820
- className: "mb-2 text-sm font-semibold text-foreground",
59821
- children: active.title
59822
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordEntriesPanel, { kind: active })] })]
59823
- });
59824
- }
59825
- //#endregion
59826
59928
  //#region src/composites/storage-records/storage-records-panel.tsx
59827
59929
  function StorageRecordsPanel({ kind }) {
59828
59930
  const t = useUiText();
@@ -59946,47 +60048,143 @@ function StorageRecordsPanel({ kind }) {
59946
60048
  });
59947
60049
  }
59948
60050
  //#endregion
59949
- //#region src/composites/storage-records/storage-records-hub.tsx
59950
- var STORAGE_RECORDS_OWNER_PARAM = "recordsOwner";
59951
- var STORAGE_RECORDS_KIND_PARAM = "recordsKind";
59952
- function kindKey(k) {
59953
- return `${k.owner}/${k.kind}`;
60051
+ //#region src/composites/data-records/data-records-model.ts
60052
+ function dataRecordKey(kind) {
60053
+ return `${kind.owner}/${kind.kind}`;
60054
+ }
60055
+ function dataRecordType(kind) {
60056
+ return kind.entry ? "entry" : "record";
59954
60057
  }
60058
+ /** `unknown` is a kind whose count was not asked for or could not be read — never "empty". */
60059
+ function dataRecordContents(kind) {
60060
+ if (kind.rows === null) return "unknown";
60061
+ return kind.rows > 0 ? "has-rows" : "empty";
60062
+ }
60063
+ /**
60064
+ * Join the declared kinds with the entry catalog. A pending/failed catalog
60065
+ * (`undefined`) leaves titles `null`; it never changes which kinds are listed.
60066
+ */
60067
+ function buildDataRecordRows(kinds, entryKinds) {
60068
+ const titles = /* @__PURE__ */ new Map();
60069
+ for (const entry of entryKinds ?? []) titles.set(dataRecordKey(entry), entry.title);
60070
+ return kinds.map((kind) => ({
60071
+ kind,
60072
+ entryTitle: kind.entry ? titles.get(dataRecordKey(kind)) ?? null : null
60073
+ }));
60074
+ }
60075
+ var DATA_RECORD_FACET_GROUPS = [
60076
+ {
60077
+ id: "kind",
60078
+ value: (row) => dataRecordKey(row.kind)
60079
+ },
60080
+ {
60081
+ id: "owner",
60082
+ value: (row) => row.kind.owner
60083
+ },
60084
+ {
60085
+ id: "type",
60086
+ value: (row) => dataRecordType(row.kind)
60087
+ },
60088
+ {
60089
+ id: "database",
60090
+ value: (row) => row.kind.database
60091
+ },
60092
+ {
60093
+ id: "class",
60094
+ value: (row) => row.kind.class
60095
+ },
60096
+ {
60097
+ id: "state",
60098
+ value: (row) => row.kind.state
60099
+ },
60100
+ {
60101
+ id: "contents",
60102
+ value: (row) => dataRecordContents(row.kind)
60103
+ }
60104
+ ];
60105
+ function dataRecordSearchText(row) {
60106
+ const { kind } = row;
60107
+ return [
60108
+ kind.owner,
60109
+ kind.kind,
60110
+ kind.class,
60111
+ kind.database,
60112
+ kind.state,
60113
+ row.entryTitle ?? ""
60114
+ ];
60115
+ }
60116
+ //#endregion
60117
+ //#region src/composites/data-records/data-records-hub.tsx
60118
+ var DATA_RECORDS_OWNER_PARAM = "recordsOwner";
60119
+ var DATA_RECORDS_KIND_PARAM = "recordsKind";
59955
60120
  function formatBytes(n) {
59956
60121
  if (n === null) return "—";
59957
60122
  if (n < 1024) return `${n} B`;
59958
60123
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
59959
60124
  return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
59960
60125
  }
59961
- function StorageRecordsHub() {
60126
+ function groupLabel(id, t) {
60127
+ switch (id) {
60128
+ case "kind": return t("uiLibrary.dataRecords.facetKind", "Kind");
60129
+ case "owner": return t("uiLibrary.dataRecords.facetOwner", "Owner");
60130
+ case "type": return t("uiLibrary.dataRecords.facetType", "Type");
60131
+ case "database": return t("uiLibrary.dataRecords.facetDatabase", "Database");
60132
+ case "class": return t("uiLibrary.dataRecords.facetClass", "Class");
60133
+ case "state": return t("uiLibrary.dataRecords.facetState", "State");
60134
+ default: return t("uiLibrary.dataRecords.facetContents", "Contents");
60135
+ }
60136
+ }
60137
+ function facetCaption(groupId, value, t) {
60138
+ if (groupId === "type") return value === "entry" ? t("uiLibrary.dataRecords.typeEntry", "Entries (editable)") : t("uiLibrary.dataRecords.typeRecord", "Records (read-only)");
60139
+ if (groupId === "contents") {
60140
+ if (value === "has-rows") return t("uiLibrary.dataRecords.contentsHasRows", "Has rows");
60141
+ if (value === "empty") return t("uiLibrary.dataRecords.contentsEmpty", "Empty");
60142
+ return t("uiLibrary.dataRecords.contentsUnknown", "Count unknown");
60143
+ }
60144
+ return value;
60145
+ }
60146
+ /** Entry kinds open the editable list; every other kind opens the read-only preview. */
60147
+ function KindDetail({ kind, entryKinds, entryKindsPending, entryKindsError }) {
60148
+ const t = useUiText();
60149
+ if (!kind.entry) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StorageRecordsPanel, { kind });
60150
+ if (entryKindsPending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
60151
+ className: "flex justify-center py-8 text-foreground-subtle",
60152
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
60153
+ });
60154
+ if (entryKindsError !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: entryKindsError });
60155
+ const info = entryKinds?.find((e) => dataRecordKey(e) === dataRecordKey(kind));
60156
+ if (info === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: t("uiLibrary.dataRecords.entryKindMissing", "This entry kind is not served by the entries service.") });
60157
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
60158
+ className: "space-y-2",
60159
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
60160
+ className: "text-sm font-semibold text-foreground",
60161
+ children: info.title
60162
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordEntriesPanel, { kind: info })]
60163
+ });
60164
+ }
60165
+ /**
60166
+ * System → Data → Records: every declared record kind in ONE table with
60167
+ * compact filters; an entry kind opens its editable list, any other kind its
60168
+ * read-only row preview. Selection is in `?recordsOwner=&recordsKind=`.
60169
+ */
60170
+ function DataRecordsHub() {
59962
60171
  const t = useUiText();
59963
60172
  const kindsQuery = trpc.storageAdmin.listKinds.useQuery({}, { staleTime: 3e4 });
59964
- const [ownerParam, setOwner] = useUrlQueryParam(STORAGE_RECORDS_OWNER_PARAM);
59965
- const [kindParam, setKind] = useUrlQueryParam(STORAGE_RECORDS_KIND_PARAM);
59966
- const kinds = kindsQuery.data?.kinds ?? [];
59967
- const filter = useDataTableFilter(kinds, {
59968
- searchText: (row) => [
59969
- row.owner,
59970
- row.kind,
59971
- row.class,
59972
- row.database,
59973
- row.state
59974
- ],
59975
- facetGroups: [{
59976
- id: "owner",
59977
- value: (row) => row.owner
59978
- }, {
59979
- id: "database",
59980
- value: (row) => row.database
59981
- }]
60173
+ const entryKindsQuery = useRecordEntriesListEntryKinds({}, { staleTime: 3e4 });
60174
+ const [ownerParam, setOwner] = useUrlQueryParam(DATA_RECORDS_OWNER_PARAM);
60175
+ const [kindParam, setKind] = useUrlQueryParam(DATA_RECORDS_KIND_PARAM);
60176
+ const entryKinds = entryKindsQuery.data?.kinds;
60177
+ const rows = (0, react$1.useMemo)(() => buildDataRecordRows(kindsQuery.data?.kinds ?? [], entryKinds), [entryKinds, kindsQuery.data?.kinds]);
60178
+ const filter = useDataTableFilter(rows, {
60179
+ searchText: dataRecordSearchText,
60180
+ facetGroups: DATA_RECORD_FACET_GROUPS
59982
60181
  });
59983
60182
  const active = (0, react$1.useMemo)(() => {
59984
- if (kinds.length === 0) return null;
59985
- return kinds.find((k) => k.owner === ownerParam && k.kind === kindParam) ?? null;
60183
+ return rows.find((r) => r.kind.owner === ownerParam && r.kind.kind === kindParam)?.kind ?? null;
59986
60184
  }, [
59987
60185
  kindParam,
59988
- kinds,
59989
- ownerParam
60186
+ ownerParam,
60187
+ rows
59990
60188
  ]);
59991
60189
  const selectKind = (0, react$1.useCallback)((k) => {
59992
60190
  setOwner(k.owner);
@@ -59996,72 +60194,87 @@ function StorageRecordsHub() {
59996
60194
  return [
59997
60195
  {
59998
60196
  key: "kind",
59999
- header: t("uiLibrary.storageRecords.colKind", "Kind"),
60197
+ header: t("uiLibrary.dataRecords.colKind", "Kind"),
60000
60198
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
60001
- className: "font-mono text-xs",
60002
- children: [
60003
- row.owner,
60004
- "/",
60005
- row.kind
60006
- ]
60199
+ className: "flex flex-col",
60200
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60201
+ className: "font-mono text-xs",
60202
+ children: dataRecordKey(row.kind)
60203
+ }), row.entryTitle !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60204
+ className: "text-[11px] text-foreground-subtle",
60205
+ children: row.entryTitle
60206
+ }) : null]
60207
+ }),
60208
+ sortValue: (row) => dataRecordKey(row.kind)
60209
+ },
60210
+ {
60211
+ key: "type",
60212
+ header: t("uiLibrary.dataRecords.colType", "Type"),
60213
+ nowrap: true,
60214
+ render: (row) => row.kind.entry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
60215
+ variant: "info",
60216
+ children: t("uiLibrary.dataRecords.badgeEntry", "Entry")
60217
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
60218
+ variant: "neutral",
60219
+ children: t("uiLibrary.dataRecords.badgeRecord", "Record")
60007
60220
  }),
60008
- sortValue: (row) => kindKey(row)
60221
+ sortValue: (row) => row.kind.entry ? "entry" : "record"
60009
60222
  },
60010
60223
  {
60011
60224
  key: "database",
60012
- header: t("uiLibrary.storageRecords.colDatabase", "DB"),
60225
+ header: t("uiLibrary.dataRecords.colDatabase", "DB"),
60013
60226
  nowrap: true,
60014
60227
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
60015
60228
  variant: "neutral",
60016
60229
  style: "outline",
60017
- children: row.database
60230
+ children: row.kind.database
60018
60231
  }),
60019
- sortValue: (row) => row.database
60232
+ sortValue: (row) => row.kind.database
60020
60233
  },
60021
60234
  {
60022
60235
  key: "class",
60023
- header: t("uiLibrary.storageRecords.colClass", "Class"),
60236
+ header: t("uiLibrary.dataRecords.colClass", "Class"),
60024
60237
  nowrap: true,
60025
60238
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60026
60239
  className: "text-xs text-foreground-muted",
60027
- children: row.class
60240
+ children: row.kind.class
60028
60241
  }),
60029
- sortValue: (row) => row.class
60242
+ sortValue: (row) => row.kind.class
60030
60243
  },
60031
60244
  {
60032
60245
  key: "state",
60033
- header: t("uiLibrary.storageRecords.colState", "State"),
60246
+ header: t("uiLibrary.dataRecords.colState", "State"),
60034
60247
  nowrap: true,
60035
60248
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
60036
- variant: row.state === "declared" ? "default" : "neutral",
60037
- children: row.state
60249
+ variant: row.kind.state === "declared" ? "default" : "neutral",
60250
+ children: row.kind.state
60038
60251
  }),
60039
- sortValue: (row) => row.state
60252
+ sortValue: (row) => row.kind.state
60040
60253
  },
60041
60254
  {
60042
60255
  key: "rows",
60043
- header: t("uiLibrary.storageRecords.colRows", "Rows"),
60256
+ header: t("uiLibrary.dataRecords.colRows", "Rows"),
60044
60257
  align: "right",
60045
60258
  nowrap: true,
60046
60259
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60047
60260
  className: "font-mono text-xs tabular-nums",
60048
- children: row.rows === null ? "—" : row.rows
60261
+ children: row.kind.rows === null ? "—" : row.kind.rows
60049
60262
  }),
60050
- sortValue: (row) => row.rows ?? -1
60263
+ sortValue: (row) => row.kind.rows ?? -1
60051
60264
  },
60052
60265
  {
60053
60266
  key: "bytes",
60054
- header: t("uiLibrary.storageRecords.colSize", "Est. size"),
60267
+ header: t("uiLibrary.dataRecords.colSize", "Est. size"),
60055
60268
  align: "right",
60056
60269
  nowrap: true,
60057
60270
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60058
60271
  className: "text-xs text-foreground-subtle",
60059
- children: formatBytes(row.bytesEstimate)
60272
+ children: formatBytes(row.kind.bytesEstimate)
60060
60273
  }),
60061
- sortValue: (row) => row.bytesEstimate ?? -1
60274
+ sortValue: (row) => row.kind.bytesEstimate ?? -1
60062
60275
  },
60063
60276
  {
60064
- key: "inspect",
60277
+ key: "open",
60065
60278
  header: "",
60066
60279
  align: "right",
60067
60280
  nowrap: true,
@@ -60069,12 +60282,12 @@ function StorageRecordsHub() {
60069
60282
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
60070
60283
  type: "button",
60071
60284
  size: "sm",
60072
- variant: active !== null && kindKey(active) === kindKey(row) ? "primary" : "outline",
60073
- onClick: () => selectKind(row),
60074
- children: t("uiLibrary.storageRecords.viewRows", "View rows")
60285
+ variant: active !== null && dataRecordKey(active) === dataRecordKey(row.kind) ? "primary" : "outline",
60286
+ onClick: () => selectKind(row.kind),
60287
+ children: row.kind.entry ? t("uiLibrary.dataRecords.manage", "Manage") : t("uiLibrary.dataRecords.viewRows", "View rows")
60075
60288
  });
60076
60289
  },
60077
- notSortableReason: t("uiLibrary.storageRecords.actionColumn", "Actions")
60290
+ notSortableReason: t("uiLibrary.dataRecords.actionColumn", "Actions")
60078
60291
  }
60079
60292
  ];
60080
60293
  }, [
@@ -60082,14 +60295,14 @@ function StorageRecordsHub() {
60082
60295
  selectKind,
60083
60296
  t
60084
60297
  ]);
60085
- if (kindsQuery.isError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: kindsQuery.error instanceof Error ? kindsQuery.error.message : t("uiLibrary.storageRecords.kindsFailed", "Could not load record kinds") });
60298
+ if (kindsQuery.isError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBox, { message: kindsQuery.error instanceof Error ? kindsQuery.error.message : t("uiLibrary.dataRecords.kindsFailed", "Could not load record kinds") });
60086
60299
  if (kindsQuery.isPending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
60087
60300
  className: "flex justify-center py-10 text-foreground-subtle",
60088
60301
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
60089
60302
  });
60090
- if (kinds.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
60091
- title: t("uiLibrary.storageRecords.noKindsTitle", "No record kinds"),
60092
- description: t("uiLibrary.storageRecords.noKindsDescription", "No addon has declared a record kind on this hub yet.")
60303
+ if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
60304
+ title: t("uiLibrary.dataRecords.noKindsTitle", "No record kinds"),
60305
+ description: t("uiLibrary.dataRecords.noKindsDescription", "No addon has declared a record kind on this hub yet.")
60093
60306
  });
60094
60307
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
60095
60308
  className: "space-y-4",
@@ -60099,13 +60312,15 @@ function StorageRecordsHub() {
60099
60312
  facets: filter.facets,
60100
60313
  groups: filter.groups.map((g) => ({
60101
60314
  ...g,
60102
- label: g.id === "owner" ? t("uiLibrary.storageRecords.facetOwner", "Owner") : t("uiLibrary.storageRecords.facetDatabase", "Database")
60315
+ label: groupLabel(g.id, t),
60316
+ display: "menu",
60317
+ facetLabel: (value) => facetCaption(g.id, value, t)
60103
60318
  })),
60104
60319
  labels: {
60105
- searchPlaceholder: t("uiLibrary.storageRecords.search", "Search kinds…"),
60106
- searchLabel: t("uiLibrary.storageRecords.searchLabel", "Search record kinds"),
60107
- facetsLabel: t("uiLibrary.storageRecords.facetsLabel", "Filter record kinds"),
60108
- clearLabel: t("uiLibrary.storageRecords.clearFilters", "Clear filters")
60320
+ searchPlaceholder: t("uiLibrary.dataRecords.search", "Search kinds…"),
60321
+ searchLabel: t("uiLibrary.dataRecords.searchLabel", "Search record kinds"),
60322
+ facetsLabel: t("uiLibrary.dataRecords.facetsLabel", "Filter record kinds"),
60323
+ clearLabel: t("uiLibrary.dataRecords.clearFilters", "Clear filters")
60109
60324
  },
60110
60325
  onSearch: filter.setSearch,
60111
60326
  onToggleFacet: filter.toggleFacet,
@@ -60115,15 +60330,20 @@ function StorageRecordsHub() {
60115
60330
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTable, {
60116
60331
  columns,
60117
60332
  rows: filter.rows,
60118
- rowKey: (k) => kindKey(k),
60333
+ rowKey: (r) => dataRecordKey(r.kind),
60119
60334
  maxBodyHeight: TABLE_SCROLL_MAX_H,
60120
60335
  defaultSort: {
60121
60336
  columnKey: "kind",
60122
60337
  dir: "asc"
60123
60338
  },
60124
- emptyMessage: t("uiLibrary.storageRecords.noMatch", "No kinds match the filter")
60339
+ emptyMessage: t("uiLibrary.dataRecords.noMatch", "No kinds match the filter")
60125
60340
  }),
60126
- active !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StorageRecordsPanel, { kind: active }) : null
60341
+ active !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KindDetail, {
60342
+ kind: active,
60343
+ entryKinds,
60344
+ entryKindsPending: entryKindsQuery.isPending,
60345
+ entryKindsError: entryKindsQuery.isError ? entryKindsQuery.error instanceof Error ? entryKindsQuery.error.message : t("uiLibrary.dataRecords.entryKindsFailed", "Could not load entry kinds") : null
60346
+ }) : null
60127
60347
  ]
60128
60348
  });
60129
60349
  }
@@ -64532,6 +64752,9 @@ exports.CoverPanel = CoverPanel;
64532
64752
  exports.Cpu = Cpu;
64533
64753
  exports.Crosshair = Crosshair;
64534
64754
  exports.CustomFieldRenderersProvider = CustomFieldRenderersProvider;
64755
+ exports.DATA_RECORDS_KIND_PARAM = DATA_RECORDS_KIND_PARAM;
64756
+ exports.DATA_RECORDS_OWNER_PARAM = DATA_RECORDS_OWNER_PARAM;
64757
+ exports.DATA_RECORD_FACET_GROUPS = DATA_RECORD_FACET_GROUPS;
64535
64758
  exports.DEFAULT_COLOR = DEFAULT_COLOR;
64536
64759
  exports.DEFAULT_PAGINATION_LABELS = DEFAULT_PAGINATION_LABELS;
64537
64760
  exports.DEVICE_COLUMNS = DEVICE_COLUMNS;
@@ -64542,6 +64765,7 @@ exports.DEVICE_PICKER_GROUP_STORAGE_KEY = DEVICE_PICKER_GROUP_STORAGE_KEY;
64542
64765
  exports.DEVICE_ROLE_META = DEVICE_ROLE_META;
64543
64766
  exports.DEVICE_TYPE_CONTROL = DEVICE_TYPE_CONTROL;
64544
64767
  exports.DEVICE_TYPE_META = DEVICE_TYPE_META;
64768
+ exports.DataRecordsHub = DataRecordsHub;
64545
64769
  exports.DataTable = DataTable;
64546
64770
  exports.DataTableFilterBar = DataTableFilterBar;
64547
64771
  exports.DataTablePager = DataTablePager;
@@ -64786,8 +65010,6 @@ exports.QrCode = QrCode;
64786
65010
  exports.QrCode$1 = QrCode$1;
64787
65011
  exports.RECONNECT_POLICY = RECONNECT_POLICY;
64788
65012
  exports.RECORDED_PLAYBACK_MODES = RECORDED_PLAYBACK_MODES;
64789
- exports.RECORD_ENTRY_KIND_PARAM = RECORD_ENTRY_KIND_PARAM;
64790
- exports.RECORD_ENTRY_OWNER_PARAM = RECORD_ENTRY_OWNER_PARAM;
64791
65013
  exports.RIGHT = RIGHT;
64792
65014
  exports.ROI_AREA_WARN_FRACTION = ROI_AREA_WARN_FRACTION;
64793
65015
  exports.ROLE_DESCRIPTOR = ROLE_DESCRIPTOR;
@@ -64796,7 +65018,6 @@ exports.Radar = Radar;
64796
65018
  exports.RadialGauge = RadialGauge;
64797
65019
  exports.Radio = Radio;
64798
65020
  exports.RadioTower = RadioTower;
64799
- exports.RecordEntriesHub = RecordEntriesHub;
64800
65021
  exports.RecordEntriesPanel = RecordEntriesPanel;
64801
65022
  exports.RecordedPlaybackProvider = RecordedPlaybackProvider;
64802
65023
  exports.RecordingPanel = RecordingPanel;
@@ -64830,8 +65051,6 @@ exports.SPLIT_PANEL_SIDE = SPLIT_PANEL_SIDE;
64830
65051
  exports.STACK_GAP = STACK_GAP;
64831
65052
  exports.STATE_COLOR = STATE_COLOR;
64832
65053
  exports.STEP_DOCK_WIDTH_PX = STEP_DOCK_WIDTH_PX;
64833
- exports.STORAGE_RECORDS_KIND_PARAM = STORAGE_RECORDS_KIND_PARAM;
64834
- exports.STORAGE_RECORDS_OWNER_PARAM = STORAGE_RECORDS_OWNER_PARAM;
64835
65054
  exports.Sailboat = Sailboat;
64836
65055
  exports.Save = Save;
64837
65056
  exports.Scan = Scan;
@@ -64890,7 +65109,6 @@ exports.StepInspectorPanel = StepInspectorPanel;
64890
65109
  exports.StepModelRow = StepModelRow;
64891
65110
  exports.StepTimings = StepTimings;
64892
65111
  exports.Stepper = Stepper;
64893
- exports.StorageRecordsHub = StorageRecordsHub;
64894
65112
  exports.StorageRecordsPanel = StorageRecordsPanel;
64895
65113
  exports.StreamBrokerSelector = StreamBrokerSelector;
64896
65114
  exports.StreamDebugOverlay = StreamDebugOverlay;
@@ -64992,6 +65210,7 @@ exports.badgeFor = badgeFor;
64992
65210
  exports.badgeFor$1 = badgeFor;
64993
65211
  exports.bindsToModel = bindsToModel;
64994
65212
  exports.buildColorByNodeId = buildColorByNodeId;
65213
+ exports.buildDataRecordRows = buildDataRecordRows;
64995
65214
  exports.buildPeerConnectionConfig = buildPeerConnectionConfig;
64996
65215
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
64997
65216
  exports.catalogHasDraft = catalogHasDraft;
@@ -65019,6 +65238,10 @@ exports.createTheme = require_theme_index.createTheme$1;
65019
65238
  exports.currentAddonLanguage = currentAddonLanguage;
65020
65239
  exports.cursorFractionFor = cursorFractionFor;
65021
65240
  exports.darkColors = require_theme_to_root_css.darkColors;
65241
+ exports.dataRecordContents = dataRecordContents;
65242
+ exports.dataRecordKey = dataRecordKey;
65243
+ exports.dataRecordSearchText = dataRecordSearchText;
65244
+ exports.dataRecordType = dataRecordType;
65022
65245
  exports.dataTableFacetGroups = dataTableFacetGroups;
65023
65246
  exports.dataTableFacets = dataTableFacets;
65024
65247
  exports.defaultOfField = defaultOfField;
@@ -65602,13 +65825,19 @@ exports.useDeviceStateSlice$1 = useDeviceStateSlice;
65602
65825
  exports.useDeviceStatusGetStatus = useDeviceStatusGetStatus;
65603
65826
  exports.useDeviceWebrtc = useDeviceWebrtc;
65604
65827
  exports.useDevices = useDevices;
65828
+ exports.useDishwasherGetOptions = useDishwasherGetOptions;
65605
65829
  exports.useDishwasherGetStatus = useDishwasherGetStatus;
65606
65830
  exports.useDishwasherPause = useDishwasherPause;
65831
+ exports.useDishwasherResume = useDishwasherResume;
65832
+ exports.useDishwasherSetChildLock = useDishwasherSetChildLock;
65833
+ exports.useDishwasherSetExtraDry = useDishwasherSetExtraDry;
65607
65834
  exports.useDishwasherSetHalfLoad = useDishwasherSetHalfLoad;
65608
65835
  exports.useDishwasherSetHygienePlus = useDishwasherSetHygienePlus;
65836
+ exports.useDishwasherSetIntensiveZone = useDishwasherSetIntensiveZone;
65609
65837
  exports.useDishwasherSetOption = useDishwasherSetOption;
65610
65838
  exports.useDishwasherSetPower = useDishwasherSetPower;
65611
65839
  exports.useDishwasherSetProgram = useDishwasherSetProgram;
65840
+ exports.useDishwasherSetVarioSpeedPlus = useDishwasherSetVarioSpeedPlus;
65612
65841
  exports.useDishwasherStart = useDishwasherStart;
65613
65842
  exports.useDishwasherStop = useDishwasherStop;
65614
65843
  exports.useDisplayIconResolver = useDisplayIconResolver;
@@ -66294,6 +66523,7 @@ exports.useStorageMigrationCleanupStart = useStorageMigrationCleanupStart;
66294
66523
  exports.useStorageMigrationCleanupStatus = useStorageMigrationCleanupStatus;
66295
66524
  exports.useStorageMigrationDrain = useStorageMigrationDrain;
66296
66525
  exports.useStorageMigrationHistory = useStorageMigrationHistory;
66526
+ exports.useStorageMigrationMoverClearance = useStorageMigrationMoverClearance;
66297
66527
  exports.useStorageMigrationMovers = useStorageMigrationMovers;
66298
66528
  exports.useStorageMigrationPlan = useStorageMigrationPlan;
66299
66529
  exports.useStorageMigrationResidue = useStorageMigrationResidue;