@camstack/ui-library 1.2.301 → 1.2.303

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`. */
@@ -11036,6 +11038,8 @@ var useDishwasherSetVarioSpeedPlus = trpc.dishwasher.setVarioSpeedPlus.useMutati
11036
11038
  var useDishwasherSetChildLock = trpc.dishwasher.setChildLock.useMutation;
11037
11039
  /** Generated alias around `trpc.dishwasher.setOption.useMutation`. */
11038
11040
  var useDishwasherSetOption = trpc.dishwasher.setOption.useMutation;
11041
+ /** Generated alias around `trpc.dishwasher.getOptions.useQuery`. */
11042
+ var useDishwasherGetOptions = trpc.dishwasher.getOptions.useQuery;
11039
11043
  /** Generated alias around `trpc.dishwasher.getStatus.useQuery`. */
11040
11044
  var useDishwasherGetStatus = trpc.dishwasher.getStatus.useQuery;
11041
11045
  /** Generated alias around `trpc.doorbell.getStatus.useQuery`. */
@@ -12362,6 +12366,8 @@ var useStorageMigrationStatus = trpc.storageMigration.status.useQuery;
12362
12366
  var useStorageMigrationCancel = trpc.storageMigration.cancel.useMutation;
12363
12367
  /** Generated alias around `trpc.storageMigration.movers.useQuery`. */
12364
12368
  var useStorageMigrationMovers = trpc.storageMigration.movers.useQuery;
12369
+ /** Generated alias around `trpc.storageMigration.moverClearance.useQuery`. */
12370
+ var useStorageMigrationMoverClearance = trpc.storageMigration.moverClearance.useQuery;
12365
12371
  /** Generated alias around `trpc.storageMigration.residue.useQuery`. */
12366
12372
  var useStorageMigrationResidue = trpc.storageMigration.residue.useQuery;
12367
12373
  /** Generated alias around `trpc.storageMigration.drain.useMutation`. */
@@ -28588,7 +28594,7 @@ var STATUS_DOT_STYLES = {
28588
28594
  idle: "bg-foreground-subtle/40",
28589
28595
  unknown: "bg-foreground-subtle/40"
28590
28596
  };
28591
- function formatDuration(ms) {
28597
+ function formatDuration$1(ms) {
28592
28598
  if (!Number.isFinite(ms) || ms < 0) return "—";
28593
28599
  if (ms < 1e3) return `${ms}ms`;
28594
28600
  const totalSec = Math.round(ms / 1e3);
@@ -28616,7 +28622,7 @@ function ObjectArrayCell({ kind, value }) {
28616
28622
  });
28617
28623
  if (kind === "duration" && typeof value === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28618
28624
  className: "font-mono text-[11px]",
28619
- children: formatDuration(value)
28625
+ children: formatDuration$1(value)
28620
28626
  });
28621
28627
  if (kind === "timestamp" && typeof value === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28622
28628
  className: "font-mono text-[11px]",
@@ -35116,7 +35122,7 @@ var ALIGN_CLASS = {
35116
35122
  right: "text-right",
35117
35123
  center: "text-center"
35118
35124
  };
35119
- 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 }) {
35120
35126
  const [innerSort, setInnerSort] = (0, react$1.useState)(defaultSort ?? null);
35121
35127
  const [innerPage, setInnerPage] = (0, react$1.useState)(0);
35122
35128
  const sort = sortProp !== void 0 ? sortProp : innerSort;
@@ -35193,7 +35199,8 @@ function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyM
35193
35199
  const key = rowKey ? rowKey(row, rowIndex) : rowIndex;
35194
35200
  const extra = rowClassName?.(row, rowIndex) ?? "";
35195
35201
  const interactive = onRowClick !== void 0;
35196
- 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", {
35197
35204
  onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
35198
35205
  className: [interactive ? "cursor-pointer hover:bg-primary/5" : "hover:bg-primary/5", extra].filter(Boolean).join(" "),
35199
35206
  children: shownColumns.map((col) => {
@@ -35209,7 +35216,15 @@ function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyM
35209
35216
  children: col.render(row, rowIndex)
35210
35217
  }, col.key);
35211
35218
  })
35212
- }, 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);
35213
35228
  }) })]
35214
35229
  })
35215
35230
  });
@@ -37773,6 +37788,88 @@ function filterDataTableRows(rows, state, config) {
37773
37788
  });
37774
37789
  }
37775
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
37776
37873
  //#region src/composites/data-table-filter-bar.tsx
37777
37874
  /**
37778
37875
  * The controls of one table's filter: a search box and the facet chips.
@@ -37825,7 +37922,18 @@ function DataTableFilterBar({ state, facets, labels, onSearch, onToggleFacet, on
37825
37922
  }, facet.value);
37826
37923
  })
37827
37924
  }),
37828
- 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", {
37829
37937
  role: "group",
37830
37938
  "aria-label": group.label,
37831
37939
  className: "flex flex-wrap items-center gap-1",
@@ -48689,47 +48797,95 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
48689
48797
  *
48690
48798
  * Reads the `dishwasher` cap status slice and renders inline + hero surfaces
48691
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.
48692
48805
  */
48693
- function dishwasherStateMeta(state, t = defaultUiText) {
48694
- switch (state) {
48695
- case "running": return {
48696
- 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"),
48697
48811
  tone: "text-primary"
48698
48812
  };
48699
- case "paused": return {
48700
- label: t("uiLibrary.dishwasher.paused", "Paused"),
48813
+ case "Pause": return {
48814
+ label: t("uiLibrary.dishwasher.op.pause", "Paused"),
48701
48815
  tone: "text-warning"
48702
48816
  };
48703
- case "finished": return {
48704
- label: t("uiLibrary.dishwasher.finished", "Finished"),
48817
+ case "Finished": return {
48818
+ label: t("uiLibrary.dishwasher.op.finished", "Finished"),
48705
48819
  tone: "text-success"
48706
48820
  };
48707
- case "error": return {
48708
- label: t("uiLibrary.dishwasher.error", "Error"),
48709
- tone: "text-destructive"
48821
+ case "Error": return {
48822
+ label: t("uiLibrary.dishwasher.op.error", "Error"),
48823
+ tone: "text-danger"
48710
48824
  };
48711
- case "unavailable": return {
48712
- label: t("uiLibrary.dishwasher.unavailable", "Unavailable"),
48713
- tone: "text-foreground-subtle"
48825
+ case "Aborting": return {
48826
+ label: t("uiLibrary.dishwasher.op.aborting", "Stopping"),
48827
+ tone: "text-warning"
48714
48828
  };
48715
- case "idle": return {
48716
- 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"),
48717
48843
  tone: "text-foreground-subtle"
48718
48844
  };
48719
- case void 0: return {
48845
+ case null: return {
48720
48846
  label: t("uiLibrary.dishwasher.unknown", "Unknown"),
48721
48847
  tone: "text-foreground-subtle"
48722
48848
  };
48723
48849
  }
48724
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
+ }
48725
48865
  function consumableTone(level) {
48726
48866
  switch (level) {
48727
48867
  case "ok": return "text-success";
48728
48868
  case "low": return "text-warning";
48729
- case "empty": return "text-destructive";
48869
+ case "empty": return "text-danger";
48730
48870
  case null: return "text-foreground-subtle";
48731
48871
  }
48732
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
+ }
48733
48889
  function useDishwasher(trpc, deviceId) {
48734
48890
  return useDeviceCapability({
48735
48891
  trpc,
@@ -48763,13 +48919,11 @@ function ConsumableBadge({ label, level }) {
48763
48919
  ]
48764
48920
  });
48765
48921
  }
48766
- function DishwasherGraphic({ state, progressPercent }) {
48767
- const running = state === "running";
48922
+ function DishwasherGraphic({ running, progressPercent }) {
48768
48923
  const pct = progressPercent != null ? Math.max(0, Math.min(100, progressPercent)) : running ? 40 : 0;
48769
48924
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48770
48925
  "aria-hidden": "true",
48771
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",
48772
- style: { boxShadow: "inset 0 0 0 2px rgba(255,255,255,.06)" },
48773
48927
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48774
48928
  className: cn("absolute inset-x-0 bottom-0 bg-primary/25 transition-[height] duration-500", running && "animate-pulse"),
48775
48929
  style: { height: `${pct}%` }
@@ -48779,20 +48933,21 @@ function DishwasherGraphic({ state, progressPercent }) {
48779
48933
  })]
48780
48934
  });
48781
48935
  }
48782
- function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48936
+ function DishwasherPanel({ trpc, deviceId, layout }) {
48783
48937
  const t = useUiText();
48784
48938
  const { cap, slice, available } = useDishwasher(trpc, deviceId);
48785
- const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
48786
48939
  if (cap === void 0) return null;
48787
- if (display === void 0) return null;
48788
- const busy = !available;
48940
+ if (slice === void 0) return null;
48941
+ const display = slice;
48942
+ const link = display.availability === "available";
48943
+ const busy = !available || !link;
48789
48944
  const optionTogglesLocked = display.childLock === true;
48790
- const meta = dishwasherStateMeta(display.state, t);
48791
- const running = display.state === "running";
48792
- const paused = display.state === "paused";
48793
- const press = (fn, patch) => {
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) => {
48794
48950
  if (busy) return;
48795
- applyOptimistic(patch);
48796
48951
  fn();
48797
48952
  };
48798
48953
  if (layout === "compact") {
@@ -48801,7 +48956,7 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48801
48956
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48802
48957
  className: "inline-flex items-center gap-1.5 max-w-full",
48803
48958
  children: [
48804
- display.doorOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48959
+ display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48805
48960
  className: "inline-flex text-warning shrink-0",
48806
48961
  title: t("uiLibrary.dishwasher.doorOpen", "Door open"),
48807
48962
  "aria-label": t("uiLibrary.dishwasher.doorOpen", "Door open"),
@@ -48814,15 +48969,11 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48814
48969
  display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48815
48970
  className: "text-xs text-foreground-subtle truncate max-w-[7rem]",
48816
48971
  title: display.program,
48817
- children: display.program
48972
+ children: featureLabel(display.program)
48818
48973
  }),
48819
- display.remainingMinutes != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48974
+ display.remainingSeconds != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48820
48975
  className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle tabular-nums shrink-0",
48821
- children: [
48822
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }),
48823
- display.remainingMinutes,
48824
- "m"
48825
- ]
48976
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-3 w-3" }), formatDuration(display.remainingSeconds, t)]
48826
48977
  }),
48827
48978
  saltWarn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48828
48979
  className: cn("text-[10px] font-medium shrink-0", consumableTone(display.saltLevel)),
@@ -48834,19 +48985,16 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48834
48985
  }),
48835
48986
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48836
48987
  label: t("uiLibrary.dishwasher.start", "Start"),
48837
- busy: busy || !display.remoteStartAllowed,
48988
+ busy: busy || !startable,
48838
48989
  active: running,
48839
- onClick: () => press(() => cap.start({}), {
48840
- state: "running",
48841
- remoteStartAllowed: false
48842
- }),
48990
+ onClick: () => press(() => cap.start({})),
48843
48991
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" })
48844
48992
  }),
48845
48993
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48846
- label: t("uiLibrary.dishwasher.pause", "Pause"),
48847
- busy: busy || !running,
48994
+ label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
48995
+ busy: busy || !(running || paused),
48848
48996
  active: paused,
48849
- onClick: () => press(() => cap.pause({}), { state: "paused" }),
48997
+ onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
48850
48998
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" })
48851
48999
  })
48852
49000
  ]
@@ -48859,7 +49007,7 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48859
49007
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48860
49008
  className: "flex items-start gap-4",
48861
49009
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherGraphic, {
48862
- state: display.state,
49010
+ running,
48863
49011
  progressPercent: display.progressPercent
48864
49012
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48865
49013
  className: "flex min-w-0 flex-1 flex-col gap-2",
@@ -48869,14 +49017,23 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48869
49017
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48870
49018
  className: cn("text-3xl font-semibold", meta.tone),
48871
49019
  children: meta.label
48872
- }), !display.powerOn && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49020
+ }), display.powerOn === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48873
49021
  className: "text-xs font-medium text-foreground-subtle",
48874
49022
  children: t("uiLibrary.dishwasher.powerOff", "Power off")
48875
49023
  })]
48876
49024
  }),
48877
49025
  display.program != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
48878
49026
  className: "text-sm text-foreground-subtle",
48879
- 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) })
48880
49037
  }),
48881
49038
  display.progressPercent != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48882
49039
  className: "flex items-center gap-2",
@@ -48902,53 +49059,52 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48902
49059
  label: t("uiLibrary.dishwasher.rinseAid", "Rinse aid"),
48903
49060
  level: display.rinseAidLevel
48904
49061
  }),
48905
- display.doorOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
49062
+ display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
48906
49063
  className: "inline-flex items-center gap-1 text-xs text-warning",
48907
49064
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
48908
- })
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))
48909
49071
  ]
48910
49072
  }),
48911
- display.errorCode != null && display.errorCode !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48912
- className: "inline-flex items-center gap-1 text-xs text-destructive",
48913
- 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") })]
48914
49076
  }),
48915
49077
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48916
49078
  className: "flex flex-wrap items-center gap-2",
48917
49079
  children: [
48918
49080
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48919
49081
  label: t("uiLibrary.dishwasher.start", "Start"),
48920
- busy: busy || !display.remoteStartAllowed,
49082
+ busy: busy || !startable,
48921
49083
  active: running,
48922
- onClick: () => press(() => cap.start({}), {
48923
- state: "running",
48924
- remoteStartAllowed: false
48925
- }),
49084
+ onClick: () => press(() => cap.start({})),
48926
49085
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" })
48927
49086
  }),
48928
49087
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48929
- label: t("uiLibrary.dishwasher.pause", "Pause"),
48930
- busy: busy || !running,
49088
+ label: paused ? t("uiLibrary.dishwasher.resume", "Resume") : t("uiLibrary.dishwasher.pause", "Pause"),
49089
+ busy: busy || !(running || paused),
48931
49090
  active: paused,
48932
- onClick: () => press(() => cap.pause({}), { state: "paused" }),
49091
+ onClick: () => press(() => paused ? cap.resume({}) : cap.pause({})),
48933
49092
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" })
48934
49093
  }),
48935
49094
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48936
49095
  label: t("uiLibrary.dishwasher.stop", "Stop"),
48937
- busy,
49096
+ busy: busy || !(running || paused),
48938
49097
  active: false,
48939
- onClick: () => press(() => cap.stop({}), {
48940
- state: "idle",
48941
- remainingMinutes: null
48942
- }),
49098
+ onClick: () => press(() => cap.stop({})),
48943
49099
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Square, { className: "h-3.5 w-3.5" })
48944
49100
  }),
48945
49101
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DishwasherIconButton, {
48946
49102
  label: t("uiLibrary.dishwasher.power", "Power"),
48947
- busy,
48948
- active: display.powerOn,
49103
+ busy: busy || display.powerOn === null,
49104
+ active: display.powerOn === true,
48949
49105
  onClick: () => {
48950
- const next = !display.powerOn;
48951
- press(() => cap.setPower({ on: next }), { powerOn: next });
49106
+ const next = display.powerOn !== true;
49107
+ press(() => cap.setPower({ on: next }));
48952
49108
  },
48953
49109
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Power, { className: "h-4 w-4" })
48954
49110
  })
@@ -48964,24 +49120,23 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48964
49120
  children: t("uiLibrary.dishwasher.program", "Program")
48965
49121
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
48966
49122
  "aria-label": t("uiLibrary.dishwasher.program", "Program"),
48967
- value: display.program ?? "",
49123
+ value: display.selectedProgram ?? "",
48968
49124
  disabled: busy,
48969
49125
  onChange: (e) => {
48970
49126
  const program = e.target.value;
48971
49127
  if (!program) return;
48972
- applyOptimistic({ program });
48973
49128
  cap.setProgram({ program });
48974
49129
  },
48975
49130
  onClick: (e) => {
48976
49131
  e.stopPropagation();
48977
49132
  },
48978
49133
  className: "flex-1 rounded border border-border bg-surface px-2 py-1 text-foreground disabled:opacity-50",
48979
- children: [display.program == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
49134
+ children: [display.selectedProgram == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
48980
49135
  value: "",
48981
49136
  children: "—"
48982
49137
  }), programs.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
48983
49138
  value: p,
48984
- children: p
49139
+ children: featureLabel(p)
48985
49140
  }, p))]
48986
49141
  })]
48987
49142
  }),
@@ -48991,77 +49146,80 @@ function DishwasherPanel({ trpc, deviceId, layout, optimistic }) {
48991
49146
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
48992
49147
  label: t("uiLibrary.dishwasher.halfLoad", "Half load"),
48993
49148
  checked: display.halfLoad,
49149
+ shown: link,
48994
49150
  busy: busy || optionTogglesLocked,
48995
- onChange: (on) => {
48996
- applyOptimistic({ halfLoad: on });
48997
- cap.setHalfLoad({ on });
48998
- }
49151
+ onChange: (on) => void cap.setHalfLoad({ on })
48999
49152
  }),
49000
49153
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49001
49154
  label: t("uiLibrary.dishwasher.hygienePlus", "Hygiene plus"),
49002
49155
  checked: display.hygienePlus,
49156
+ shown: link,
49003
49157
  busy: busy || optionTogglesLocked,
49004
- onChange: (on) => {
49005
- applyOptimistic({ hygienePlus: on });
49006
- cap.setHygienePlus({ on });
49007
- }
49158
+ onChange: (on) => void cap.setHygienePlus({ on })
49008
49159
  }),
49009
49160
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49010
49161
  label: t("uiLibrary.dishwasher.extraDry", "Extra dry"),
49011
49162
  checked: display.extraDry,
49163
+ shown: link,
49012
49164
  busy: busy || optionTogglesLocked,
49013
- onChange: (on) => {
49014
- applyOptimistic({ extraDry: on });
49015
- cap.setExtraDry({ on });
49016
- }
49165
+ onChange: (on) => void cap.setExtraDry({ on })
49017
49166
  }),
49018
49167
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49019
49168
  label: t("uiLibrary.dishwasher.intensiveZone", "Intensive zone"),
49020
49169
  checked: display.intensiveZone,
49170
+ shown: link,
49021
49171
  busy: busy || optionTogglesLocked,
49022
- onChange: (on) => {
49023
- applyOptimistic({ intensiveZone: on });
49024
- cap.setIntensiveZone({ on });
49025
- }
49172
+ onChange: (on) => void cap.setIntensiveZone({ on })
49026
49173
  }),
49027
49174
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49028
49175
  label: t("uiLibrary.dishwasher.varioSpeedPlus", "VarioSpeed+"),
49029
49176
  checked: display.varioSpeedPlus,
49177
+ shown: link,
49030
49178
  busy: busy || optionTogglesLocked,
49031
- onChange: (on) => {
49032
- applyOptimistic({ varioSpeedPlus: on });
49033
- cap.setVarioSpeedPlus({ on });
49034
- }
49179
+ onChange: (on) => void cap.setVarioSpeedPlus({ on })
49035
49180
  }),
49036
49181
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
49037
49182
  label: t("uiLibrary.dishwasher.childLock", "Child lock"),
49038
49183
  checked: display.childLock,
49184
+ shown: link,
49039
49185
  busy,
49040
- onChange: (on) => {
49041
- applyOptimistic({ childLock: on });
49042
- cap.setChildLock({ on });
49043
- }
49186
+ onChange: (on) => void cap.setChildLock({ on })
49044
49187
  })
49045
49188
  ]
49046
49189
  })
49047
49190
  ]
49048
49191
  });
49049
49192
  }
49050
- 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;
49051
49201
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
49052
49202
  className: "inline-flex items-center gap-2 cursor-pointer",
49053
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
49054
- type: "checkbox",
49055
- checked,
49056
- disabled: busy,
49057
- onChange: (e) => {
49058
- onChange(e.target.checked);
49059
- },
49060
- onClick: (e) => {
49061
- e.stopPropagation();
49062
- },
49063
- className: "rounded border-border"
49064
- }), /* @__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
+ ]
49065
49223
  });
49066
49224
  }
49067
49225
  function DishwasherInlineControl(props) {
@@ -54693,88 +54851,6 @@ function PageSizeSelect({ value, options, onChange, ariaLabel }) {
54693
54851
  });
54694
54852
  }
54695
54853
  //#endregion
54696
- //#region src/composites/device-list/multi-select.tsx
54697
- /**
54698
- * Compact multi-select dropdown — a single chip-sized trigger ("Label · N")
54699
- * that opens a checklist. Self-contained (outside-click / Escape dismiss), so
54700
- * it stays compressed regardless of how many options exist or how little
54701
- * horizontal room the container has. Shared by the device list's generic-mode
54702
- * type filter and any other multi-select needs.
54703
- */
54704
- function MultiSelectDropdown({ label, options, selected, onChange }) {
54705
- const t = useUiText();
54706
- const [open, setOpen] = (0, react$1.useState)(false);
54707
- const rootRef = (0, react$1.useRef)(null);
54708
- (0, react$1.useEffect)(() => {
54709
- if (!open) return;
54710
- const onOutside = (e) => {
54711
- const target = e.target instanceof Node ? e.target : null;
54712
- if (target && rootRef.current && !rootRef.current.contains(target)) setOpen(false);
54713
- };
54714
- const onKey = (e) => {
54715
- if (e.key === "Escape") setOpen(false);
54716
- };
54717
- document.addEventListener("mousedown", onOutside);
54718
- document.addEventListener("touchstart", onOutside);
54719
- document.addEventListener("keydown", onKey);
54720
- return () => {
54721
- document.removeEventListener("mousedown", onOutside);
54722
- document.removeEventListener("touchstart", onOutside);
54723
- document.removeEventListener("keydown", onKey);
54724
- };
54725
- }, [open]);
54726
- const selectedSet = new Set(selected);
54727
- const count = selected.length;
54728
- const toggle = (id) => {
54729
- const next = new Set(selectedSet);
54730
- if (next.has(id)) next.delete(id);
54731
- else next.add(id);
54732
- onChange(options.filter((o) => next.has(o.id)).map((o) => o.id));
54733
- };
54734
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54735
- ref: rootRef,
54736
- className: "relative",
54737
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
54738
- type: "button",
54739
- onClick: () => setOpen((v) => !v),
54740
- "aria-haspopup": "listbox",
54741
- "aria-expanded": open,
54742
- 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"}`,
54743
- 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" : ""}` })]
54744
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54745
- role: "listbox",
54746
- 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",
54747
- children: [count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
54748
- type: "button",
54749
- onClick: () => onChange([]),
54750
- 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",
54751
- children: t("uiLibrary.multiSelect.clear", "Clear selection")
54752
- }), options.map((o) => {
54753
- const active = selectedSet.has(o.id);
54754
- const Icon = o.icon;
54755
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
54756
- type: "button",
54757
- role: "option",
54758
- "aria-selected": active,
54759
- onClick: () => toggle(o.id),
54760
- className: "flex w-full items-center gap-2 px-3 py-1.5 text-left text-[10.5px] text-foreground hover:bg-surface-hover",
54761
- children: [
54762
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54763
- className: `flex h-3.5 w-3.5 items-center justify-center rounded border shrink-0 ${active ? "border-primary bg-primary" : "border-border"}`,
54764
- children: active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-2.5 w-2.5 text-primary-foreground" })
54765
- }),
54766
- Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" }),
54767
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54768
- className: "truncate",
54769
- children: o.label
54770
- })
54771
- ]
54772
- }, o.id);
54773
- })]
54774
- })]
54775
- });
54776
- }
54777
- //#endregion
54778
54854
  //#region src/composites/device-list/type-filter.tsx
54779
54855
  /**
54780
54856
  * Shared, multi-select type filter that adapts to its OWN available width
@@ -59467,19 +59543,35 @@ function objectFromJson(text) {
59467
59543
  return null;
59468
59544
  }
59469
59545
  }
59470
- function RecordEntrySecretField({ label, required, value, disabled, onPatch }) {
59471
- const [shown, setShown] = (0, react$1.useState)(false);
59472
- 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 }) {
59473
59562
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
59474
59563
  label,
59475
59564
  required,
59476
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PasswordInput, {
59477
- value: secretDisplayValue(value),
59478
- onValueChange: (text) => onPatch(mergeSecretForSubmit(text, value)),
59479
- shown,
59480
- onToggleShown: () => setShown((s) => !s),
59481
- disabled,
59482
- 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
59483
59575
  })
59484
59576
  });
59485
59577
  }
@@ -59487,6 +59579,7 @@ function SchemaField(props) {
59487
59579
  const { path, name, node, value, secretPaths, required, disabled, onPatch } = props;
59488
59580
  const label = labelFor(name, node);
59489
59581
  if (isSecretField(path, secretPaths, node)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordEntrySecretField, {
59582
+ name,
59490
59583
  label,
59491
59584
  required,
59492
59585
  value,
@@ -59636,7 +59729,7 @@ function defaultBody(schema) {
59636
59729
  }
59637
59730
  return out;
59638
59731
  }
59639
- function RecordEntriesPanel({ kind }) {
59732
+ function RecordEntriesPanel({ kind, rowActions, extraColumns = [], showUpdated = true, minWidthPx }) {
59640
59733
  const t = useUiText();
59641
59734
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
59642
59735
  const confirm = useConfirm();
@@ -59709,41 +59802,51 @@ function RecordEntriesPanel({ kind }) {
59709
59802
  })
59710
59803
  })
59711
59804
  },
59712
- {
59805
+ ...showUpdated ? [{
59713
59806
  key: "updated",
59714
59807
  header: t("uiLibrary.recordEntries.colUpdated", "Updated"),
59715
59808
  nowrap: true,
59716
59809
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
59717
59810
  className: "text-[11px] text-foreground-subtle",
59718
- children: new Date(row.updatedAt).toLocaleString()
59811
+ title: new Date(row.updatedAt).toLocaleString(),
59812
+ children: new Date(row.updatedAt).toLocaleDateString()
59719
59813
  })
59720
- },
59814
+ }] : [],
59815
+ ...extraColumns,
59721
59816
  {
59722
59817
  key: "actions",
59723
59818
  header: "",
59724
59819
  align: "right",
59725
59820
  nowrap: true,
59821
+ width: "w-px",
59726
59822
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
59727
59823
  className: "flex justify-end gap-1",
59728
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59729
- size: "sm",
59730
- variant: "ghost",
59731
- type: "button",
59732
- onClick: () => openEdit(row),
59733
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pencil, { className: "h-3.5 w-3.5" })
59734
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
59735
- size: "sm",
59736
- variant: "ghost",
59737
- type: "button",
59738
- onClick: () => void onDelete(row),
59739
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5 text-danger" })
59740
- })]
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
+ ]
59741
59841
  })
59742
59842
  }
59743
59843
  ];
59744
59844
  }, [
59845
+ extraColumns,
59745
59846
  onDelete,
59746
59847
  openEdit,
59848
+ rowActions,
59849
+ showUpdated,
59747
59850
  t
59748
59851
  ]);
59749
59852
  const dialogOpen = formUrl.state.mode !== "closed";
@@ -59791,6 +59894,7 @@ function RecordEntriesPanel({ kind }) {
59791
59894
  title: t("uiLibrary.recordEntries.emptyTitle", "No entries yet"),
59792
59895
  description: t("uiLibrary.recordEntries.emptyDescription", "Add the first row for this kind.")
59793
59896
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTable, {
59897
+ ...minWidthPx !== void 0 ? { minWidthPx } : {},
59794
59898
  columns,
59795
59899
  rows: entries,
59796
59900
  rowKey: (r) => r.id,
@@ -59821,73 +59925,6 @@ function RecordEntriesPanel({ kind }) {
59821
59925
  });
59822
59926
  }
59823
59927
  //#endregion
59824
- //#region src/composites/record-entries/record-entries-hub.tsx
59825
- var RECORD_ENTRY_OWNER_PARAM = "entryOwner";
59826
- var RECORD_ENTRY_KIND_PARAM = "entryKind";
59827
- function kindKey$1(k) {
59828
- return `${k.owner}/${k.kind}`;
59829
- }
59830
- function RecordEntriesHub({ filter }) {
59831
- const t = useUiText();
59832
- const kindsQuery = useRecordEntriesListEntryKinds({}, { staleTime: 3e4 });
59833
- const [ownerParam, setOwner] = useUrlQueryParam(RECORD_ENTRY_OWNER_PARAM);
59834
- const [kindParam, setKind] = useUrlQueryParam(RECORD_ENTRY_KIND_PARAM);
59835
- const kinds = (0, react$1.useMemo)(() => {
59836
- return (kindsQuery.data?.kinds ?? []).filter((k) => {
59837
- if (filter?.owner !== void 0 && k.owner !== filter.owner) return false;
59838
- if (filter?.kind !== void 0 && k.kind !== filter.kind) return false;
59839
- return true;
59840
- });
59841
- }, [
59842
- filter?.kind,
59843
- filter?.owner,
59844
- kindsQuery.data?.kinds
59845
- ]);
59846
- const active = (0, react$1.useMemo)(() => {
59847
- if (kinds.length === 0) return null;
59848
- return kinds.find((k) => k.owner === ownerParam && k.kind === kindParam) ?? kinds[0] ?? null;
59849
- }, [
59850
- kindParam,
59851
- kinds,
59852
- ownerParam
59853
- ]);
59854
- const selectKind = (k) => {
59855
- setOwner(k.owner);
59856
- setKind(k.kind);
59857
- };
59858
- 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") });
59859
- if (kindsQuery.isPending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
59860
- className: "flex justify-center py-10 text-foreground-subtle",
59861
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
59862
- });
59863
- if (kinds.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
59864
- title: t("uiLibrary.recordEntries.noKindsTitle", "No entry kinds"),
59865
- description: t("uiLibrary.recordEntries.noKindsDescription", "No addon has declared an entry record kind yet.")
59866
- });
59867
- const activeKey = active === null ? "" : kindKey$1(active);
59868
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
59869
- className: "space-y-4",
59870
- children: [kinds.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tabs, {
59871
- appearance: "pills",
59872
- value: activeKey,
59873
- onValueChange: (key) => {
59874
- const found = kinds.find((k) => kindKey$1(k) === key);
59875
- if (found !== void 0) selectKind(found);
59876
- },
59877
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabsList, {
59878
- className: "flex flex-wrap gap-1",
59879
- children: kinds.map((k) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TabsTrigger, {
59880
- value: kindKey$1(k),
59881
- children: [k.title, k.rows > 0 ? ` (${k.rows})` : ""]
59882
- }, kindKey$1(k)))
59883
- })
59884
- }), active !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [kinds.length === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
59885
- className: "mb-2 text-sm font-semibold text-foreground",
59886
- children: active.title
59887
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordEntriesPanel, { kind: active })] })]
59888
- });
59889
- }
59890
- //#endregion
59891
59928
  //#region src/composites/storage-records/storage-records-panel.tsx
59892
59929
  function StorageRecordsPanel({ kind }) {
59893
59930
  const t = useUiText();
@@ -60011,47 +60048,143 @@ function StorageRecordsPanel({ kind }) {
60011
60048
  });
60012
60049
  }
60013
60050
  //#endregion
60014
- //#region src/composites/storage-records/storage-records-hub.tsx
60015
- var STORAGE_RECORDS_OWNER_PARAM = "recordsOwner";
60016
- var STORAGE_RECORDS_KIND_PARAM = "recordsKind";
60017
- function kindKey(k) {
60018
- 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}`;
60019
60054
  }
60055
+ function dataRecordType(kind) {
60056
+ return kind.entry ? "entry" : "record";
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";
60020
60120
  function formatBytes(n) {
60021
60121
  if (n === null) return "—";
60022
60122
  if (n < 1024) return `${n} B`;
60023
60123
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
60024
60124
  return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
60025
60125
  }
60026
- 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() {
60027
60171
  const t = useUiText();
60028
60172
  const kindsQuery = trpc.storageAdmin.listKinds.useQuery({}, { staleTime: 3e4 });
60029
- const [ownerParam, setOwner] = useUrlQueryParam(STORAGE_RECORDS_OWNER_PARAM);
60030
- const [kindParam, setKind] = useUrlQueryParam(STORAGE_RECORDS_KIND_PARAM);
60031
- const kinds = kindsQuery.data?.kinds ?? [];
60032
- const filter = useDataTableFilter(kinds, {
60033
- searchText: (row) => [
60034
- row.owner,
60035
- row.kind,
60036
- row.class,
60037
- row.database,
60038
- row.state
60039
- ],
60040
- facetGroups: [{
60041
- id: "owner",
60042
- value: (row) => row.owner
60043
- }, {
60044
- id: "database",
60045
- value: (row) => row.database
60046
- }]
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
60047
60181
  });
60048
60182
  const active = (0, react$1.useMemo)(() => {
60049
- if (kinds.length === 0) return null;
60050
- 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;
60051
60184
  }, [
60052
60185
  kindParam,
60053
- kinds,
60054
- ownerParam
60186
+ ownerParam,
60187
+ rows
60055
60188
  ]);
60056
60189
  const selectKind = (0, react$1.useCallback)((k) => {
60057
60190
  setOwner(k.owner);
@@ -60061,72 +60194,87 @@ function StorageRecordsHub() {
60061
60194
  return [
60062
60195
  {
60063
60196
  key: "kind",
60064
- header: t("uiLibrary.storageRecords.colKind", "Kind"),
60197
+ header: t("uiLibrary.dataRecords.colKind", "Kind"),
60065
60198
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
60066
- className: "font-mono text-xs",
60067
- children: [
60068
- row.owner,
60069
- "/",
60070
- row.kind
60071
- ]
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]
60072
60207
  }),
60073
- sortValue: (row) => kindKey(row)
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")
60220
+ }),
60221
+ sortValue: (row) => row.kind.entry ? "entry" : "record"
60074
60222
  },
60075
60223
  {
60076
60224
  key: "database",
60077
- header: t("uiLibrary.storageRecords.colDatabase", "DB"),
60225
+ header: t("uiLibrary.dataRecords.colDatabase", "DB"),
60078
60226
  nowrap: true,
60079
60227
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
60080
60228
  variant: "neutral",
60081
60229
  style: "outline",
60082
- children: row.database
60230
+ children: row.kind.database
60083
60231
  }),
60084
- sortValue: (row) => row.database
60232
+ sortValue: (row) => row.kind.database
60085
60233
  },
60086
60234
  {
60087
60235
  key: "class",
60088
- header: t("uiLibrary.storageRecords.colClass", "Class"),
60236
+ header: t("uiLibrary.dataRecords.colClass", "Class"),
60089
60237
  nowrap: true,
60090
60238
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60091
60239
  className: "text-xs text-foreground-muted",
60092
- children: row.class
60240
+ children: row.kind.class
60093
60241
  }),
60094
- sortValue: (row) => row.class
60242
+ sortValue: (row) => row.kind.class
60095
60243
  },
60096
60244
  {
60097
60245
  key: "state",
60098
- header: t("uiLibrary.storageRecords.colState", "State"),
60246
+ header: t("uiLibrary.dataRecords.colState", "State"),
60099
60247
  nowrap: true,
60100
60248
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
60101
- variant: row.state === "declared" ? "default" : "neutral",
60102
- children: row.state
60249
+ variant: row.kind.state === "declared" ? "default" : "neutral",
60250
+ children: row.kind.state
60103
60251
  }),
60104
- sortValue: (row) => row.state
60252
+ sortValue: (row) => row.kind.state
60105
60253
  },
60106
60254
  {
60107
60255
  key: "rows",
60108
- header: t("uiLibrary.storageRecords.colRows", "Rows"),
60256
+ header: t("uiLibrary.dataRecords.colRows", "Rows"),
60109
60257
  align: "right",
60110
60258
  nowrap: true,
60111
60259
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60112
60260
  className: "font-mono text-xs tabular-nums",
60113
- children: row.rows === null ? "—" : row.rows
60261
+ children: row.kind.rows === null ? "—" : row.kind.rows
60114
60262
  }),
60115
- sortValue: (row) => row.rows ?? -1
60263
+ sortValue: (row) => row.kind.rows ?? -1
60116
60264
  },
60117
60265
  {
60118
60266
  key: "bytes",
60119
- header: t("uiLibrary.storageRecords.colSize", "Est. size"),
60267
+ header: t("uiLibrary.dataRecords.colSize", "Est. size"),
60120
60268
  align: "right",
60121
60269
  nowrap: true,
60122
60270
  render: (row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
60123
60271
  className: "text-xs text-foreground-subtle",
60124
- children: formatBytes(row.bytesEstimate)
60272
+ children: formatBytes(row.kind.bytesEstimate)
60125
60273
  }),
60126
- sortValue: (row) => row.bytesEstimate ?? -1
60274
+ sortValue: (row) => row.kind.bytesEstimate ?? -1
60127
60275
  },
60128
60276
  {
60129
- key: "inspect",
60277
+ key: "open",
60130
60278
  header: "",
60131
60279
  align: "right",
60132
60280
  nowrap: true,
@@ -60134,12 +60282,12 @@ function StorageRecordsHub() {
60134
60282
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
60135
60283
  type: "button",
60136
60284
  size: "sm",
60137
- variant: active !== null && kindKey(active) === kindKey(row) ? "primary" : "outline",
60138
- onClick: () => selectKind(row),
60139
- 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")
60140
60288
  });
60141
60289
  },
60142
- notSortableReason: t("uiLibrary.storageRecords.actionColumn", "Actions")
60290
+ notSortableReason: t("uiLibrary.dataRecords.actionColumn", "Actions")
60143
60291
  }
60144
60292
  ];
60145
60293
  }, [
@@ -60147,14 +60295,14 @@ function StorageRecordsHub() {
60147
60295
  selectKind,
60148
60296
  t
60149
60297
  ]);
60150
- 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") });
60151
60299
  if (kindsQuery.isPending) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
60152
60300
  className: "flex justify-center py-10 text-foreground-subtle",
60153
60301
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-5 w-5 animate-spin" })
60154
60302
  });
60155
- if (kinds.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {
60156
- title: t("uiLibrary.storageRecords.noKindsTitle", "No record kinds"),
60157
- 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.")
60158
60306
  });
60159
60307
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
60160
60308
  className: "space-y-4",
@@ -60164,13 +60312,15 @@ function StorageRecordsHub() {
60164
60312
  facets: filter.facets,
60165
60313
  groups: filter.groups.map((g) => ({
60166
60314
  ...g,
60167
- 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)
60168
60318
  })),
60169
60319
  labels: {
60170
- searchPlaceholder: t("uiLibrary.storageRecords.search", "Search kinds…"),
60171
- searchLabel: t("uiLibrary.storageRecords.searchLabel", "Search record kinds"),
60172
- facetsLabel: t("uiLibrary.storageRecords.facetsLabel", "Filter record kinds"),
60173
- 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")
60174
60324
  },
60175
60325
  onSearch: filter.setSearch,
60176
60326
  onToggleFacet: filter.toggleFacet,
@@ -60180,15 +60330,20 @@ function StorageRecordsHub() {
60180
60330
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTable, {
60181
60331
  columns,
60182
60332
  rows: filter.rows,
60183
- rowKey: (k) => kindKey(k),
60333
+ rowKey: (r) => dataRecordKey(r.kind),
60184
60334
  maxBodyHeight: TABLE_SCROLL_MAX_H,
60185
60335
  defaultSort: {
60186
60336
  columnKey: "kind",
60187
60337
  dir: "asc"
60188
60338
  },
60189
- emptyMessage: t("uiLibrary.storageRecords.noMatch", "No kinds match the filter")
60339
+ emptyMessage: t("uiLibrary.dataRecords.noMatch", "No kinds match the filter")
60190
60340
  }),
60191
- 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
60192
60347
  ]
60193
60348
  });
60194
60349
  }
@@ -64597,6 +64752,9 @@ exports.CoverPanel = CoverPanel;
64597
64752
  exports.Cpu = Cpu;
64598
64753
  exports.Crosshair = Crosshair;
64599
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;
64600
64758
  exports.DEFAULT_COLOR = DEFAULT_COLOR;
64601
64759
  exports.DEFAULT_PAGINATION_LABELS = DEFAULT_PAGINATION_LABELS;
64602
64760
  exports.DEVICE_COLUMNS = DEVICE_COLUMNS;
@@ -64607,6 +64765,7 @@ exports.DEVICE_PICKER_GROUP_STORAGE_KEY = DEVICE_PICKER_GROUP_STORAGE_KEY;
64607
64765
  exports.DEVICE_ROLE_META = DEVICE_ROLE_META;
64608
64766
  exports.DEVICE_TYPE_CONTROL = DEVICE_TYPE_CONTROL;
64609
64767
  exports.DEVICE_TYPE_META = DEVICE_TYPE_META;
64768
+ exports.DataRecordsHub = DataRecordsHub;
64610
64769
  exports.DataTable = DataTable;
64611
64770
  exports.DataTableFilterBar = DataTableFilterBar;
64612
64771
  exports.DataTablePager = DataTablePager;
@@ -64851,8 +65010,6 @@ exports.QrCode = QrCode;
64851
65010
  exports.QrCode$1 = QrCode$1;
64852
65011
  exports.RECONNECT_POLICY = RECONNECT_POLICY;
64853
65012
  exports.RECORDED_PLAYBACK_MODES = RECORDED_PLAYBACK_MODES;
64854
- exports.RECORD_ENTRY_KIND_PARAM = RECORD_ENTRY_KIND_PARAM;
64855
- exports.RECORD_ENTRY_OWNER_PARAM = RECORD_ENTRY_OWNER_PARAM;
64856
65013
  exports.RIGHT = RIGHT;
64857
65014
  exports.ROI_AREA_WARN_FRACTION = ROI_AREA_WARN_FRACTION;
64858
65015
  exports.ROLE_DESCRIPTOR = ROLE_DESCRIPTOR;
@@ -64861,7 +65018,6 @@ exports.Radar = Radar;
64861
65018
  exports.RadialGauge = RadialGauge;
64862
65019
  exports.Radio = Radio;
64863
65020
  exports.RadioTower = RadioTower;
64864
- exports.RecordEntriesHub = RecordEntriesHub;
64865
65021
  exports.RecordEntriesPanel = RecordEntriesPanel;
64866
65022
  exports.RecordedPlaybackProvider = RecordedPlaybackProvider;
64867
65023
  exports.RecordingPanel = RecordingPanel;
@@ -64895,8 +65051,6 @@ exports.SPLIT_PANEL_SIDE = SPLIT_PANEL_SIDE;
64895
65051
  exports.STACK_GAP = STACK_GAP;
64896
65052
  exports.STATE_COLOR = STATE_COLOR;
64897
65053
  exports.STEP_DOCK_WIDTH_PX = STEP_DOCK_WIDTH_PX;
64898
- exports.STORAGE_RECORDS_KIND_PARAM = STORAGE_RECORDS_KIND_PARAM;
64899
- exports.STORAGE_RECORDS_OWNER_PARAM = STORAGE_RECORDS_OWNER_PARAM;
64900
65054
  exports.Sailboat = Sailboat;
64901
65055
  exports.Save = Save;
64902
65056
  exports.Scan = Scan;
@@ -64955,7 +65109,6 @@ exports.StepInspectorPanel = StepInspectorPanel;
64955
65109
  exports.StepModelRow = StepModelRow;
64956
65110
  exports.StepTimings = StepTimings;
64957
65111
  exports.Stepper = Stepper;
64958
- exports.StorageRecordsHub = StorageRecordsHub;
64959
65112
  exports.StorageRecordsPanel = StorageRecordsPanel;
64960
65113
  exports.StreamBrokerSelector = StreamBrokerSelector;
64961
65114
  exports.StreamDebugOverlay = StreamDebugOverlay;
@@ -65057,6 +65210,7 @@ exports.badgeFor = badgeFor;
65057
65210
  exports.badgeFor$1 = badgeFor;
65058
65211
  exports.bindsToModel = bindsToModel;
65059
65212
  exports.buildColorByNodeId = buildColorByNodeId;
65213
+ exports.buildDataRecordRows = buildDataRecordRows;
65060
65214
  exports.buildPeerConnectionConfig = buildPeerConnectionConfig;
65061
65215
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
65062
65216
  exports.catalogHasDraft = catalogHasDraft;
@@ -65084,6 +65238,10 @@ exports.createTheme = require_theme_index.createTheme$1;
65084
65238
  exports.currentAddonLanguage = currentAddonLanguage;
65085
65239
  exports.cursorFractionFor = cursorFractionFor;
65086
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;
65087
65245
  exports.dataTableFacetGroups = dataTableFacetGroups;
65088
65246
  exports.dataTableFacets = dataTableFacets;
65089
65247
  exports.defaultOfField = defaultOfField;
@@ -65667,8 +65825,10 @@ exports.useDeviceStateSlice$1 = useDeviceStateSlice;
65667
65825
  exports.useDeviceStatusGetStatus = useDeviceStatusGetStatus;
65668
65826
  exports.useDeviceWebrtc = useDeviceWebrtc;
65669
65827
  exports.useDevices = useDevices;
65828
+ exports.useDishwasherGetOptions = useDishwasherGetOptions;
65670
65829
  exports.useDishwasherGetStatus = useDishwasherGetStatus;
65671
65830
  exports.useDishwasherPause = useDishwasherPause;
65831
+ exports.useDishwasherResume = useDishwasherResume;
65672
65832
  exports.useDishwasherSetChildLock = useDishwasherSetChildLock;
65673
65833
  exports.useDishwasherSetExtraDry = useDishwasherSetExtraDry;
65674
65834
  exports.useDishwasherSetHalfLoad = useDishwasherSetHalfLoad;
@@ -66363,6 +66523,7 @@ exports.useStorageMigrationCleanupStart = useStorageMigrationCleanupStart;
66363
66523
  exports.useStorageMigrationCleanupStatus = useStorageMigrationCleanupStatus;
66364
66524
  exports.useStorageMigrationDrain = useStorageMigrationDrain;
66365
66525
  exports.useStorageMigrationHistory = useStorageMigrationHistory;
66526
+ exports.useStorageMigrationMoverClearance = useStorageMigrationMoverClearance;
66366
66527
  exports.useStorageMigrationMovers = useStorageMigrationMovers;
66367
66528
  exports.useStorageMigrationPlan = useStorageMigrationPlan;
66368
66529
  exports.useStorageMigrationResidue = useStorageMigrationResidue;