@camstack/ui-library 1.2.320 → 1.2.321

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
@@ -12229,6 +12229,8 @@ var useRecordingOnboardSetSettings = trpc.recordingOnboard.setSettings.useMutati
12229
12229
  var useRecordingOnboardGetStatus = trpc.recordingOnboard.getStatus.useQuery;
12230
12230
  /** Generated alias around `trpc.recordingSignal.getStatus.useQuery`. */
12231
12231
  var useRecordingSignalGetStatus = trpc.recordingSignal.getStatus.useQuery;
12232
+ /** Generated alias around `trpc.runningState.getStatus.useQuery`. */
12233
+ var useRunningStateGetStatus = trpc.runningState.getStatus.useQuery;
12232
12234
  /** Generated alias around `trpc.sceneMonitor.listScenes.useQuery`. */
12233
12235
  var useSceneMonitorListScenes = trpc.sceneMonitor.listScenes.useQuery;
12234
12236
  /** Generated alias around `trpc.sceneMonitor.listScenesBatch.useQuery`. */
@@ -13026,7 +13028,7 @@ function selectedDeviceOptions(options, selected) {
13026
13028
  * select. Both are inline in ui-library (like `node-select`) so any host that
13027
13029
  * renders the shared form field gets them without extra registration.
13028
13030
  */
13029
- var INPUT_CLASS$1 = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
13031
+ var INPUT_CLASS$2 = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
13030
13032
  var INPUT_SHELL$1 = "flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30";
13031
13033
  var LABEL_CLASS$3 = "block text-[11px] font-medium text-foreground mb-1 break-words";
13032
13034
  var DESC_CLASS$2 = "text-[10px] text-foreground-subtle mt-0.5 break-words";
@@ -13156,7 +13158,7 @@ function DeviceSelectField({ field, value, onChange, disabled, translationFn })
13156
13158
  required: field.required,
13157
13159
  translationFn,
13158
13160
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
13159
- className: INPUT_CLASS$1,
13161
+ className: INPUT_CLASS$2,
13160
13162
  value: current,
13161
13163
  disabled: disabled || field.disabled,
13162
13164
  onChange: (e) => onChange(e.target.value),
@@ -14382,6 +14384,282 @@ function useCustomFieldRenderer(type) {
14382
14384
  return (0, react$1.useContext)(CustomFieldRenderersContext)[type] ?? null;
14383
14385
  }
14384
14386
  //#endregion
14387
+ //#region src/primitives/button.tsx
14388
+ var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
14389
+ variants: {
14390
+ variant: {
14391
+ primary: "bg-primary text-primary-foreground hover:bg-primary/90",
14392
+ secondary: "bg-surface text-foreground hover:bg-surface-hover",
14393
+ ghost: "hover:bg-surface-hover text-foreground-muted",
14394
+ danger: "bg-danger text-white hover:bg-danger/90",
14395
+ outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
14396
+ },
14397
+ size: {
14398
+ sm: "h-7 px-2.5 text-xs gap-1.5",
14399
+ md: "h-8 px-3 text-sm gap-2",
14400
+ lg: "h-9 px-4 text-sm gap-2"
14401
+ }
14402
+ },
14403
+ defaultVariants: {
14404
+ variant: "primary",
14405
+ size: "sm"
14406
+ }
14407
+ });
14408
+ var Button = (0, react$1.forwardRef)(({ className, variant, size, ...props }, ref) => {
14409
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14410
+ ref,
14411
+ className: cn(buttonVariants({
14412
+ variant,
14413
+ size
14414
+ }), className),
14415
+ ...props
14416
+ });
14417
+ });
14418
+ Button.displayName = "Button";
14419
+ //#endregion
14420
+ //#region src/composites/account-sign-in-field.tsx
14421
+ var INPUT_CLASS$1 = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
14422
+ var BARE_CODE = /^[A-Za-z0-9._~-]{8,}$/;
14423
+ function isRecord$3(value) {
14424
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14425
+ }
14426
+ function isPresent(value) {
14427
+ if (typeof value === "string") return value.trim().length > 0;
14428
+ if ((0, _camstack_types.isSecretFieldView)(value) || (0, _camstack_types.isSecretMarker)(value)) return true;
14429
+ return false;
14430
+ }
14431
+ function accountText(value) {
14432
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : "";
14433
+ }
14434
+ function countFromValue(value) {
14435
+ if (typeof value === "number" && Number.isFinite(value)) return value;
14436
+ if (Array.isArray(value)) return value.length;
14437
+ if (typeof value === "string" && value.trim().startsWith("[")) try {
14438
+ const parsed = JSON.parse(value);
14439
+ if (Array.isArray(parsed)) return parsed.length;
14440
+ } catch {
14441
+ return null;
14442
+ }
14443
+ return null;
14444
+ }
14445
+ function nounFor(count, noun) {
14446
+ return count === 1 ? noun : `${noun}s`;
14447
+ }
14448
+ function choicesOf(value) {
14449
+ if (!Array.isArray(value)) return [];
14450
+ const out = [];
14451
+ for (const row of value) {
14452
+ if (!isRecord$3(row) || typeof row["value"] !== "string" || typeof row["label"] !== "string") continue;
14453
+ out.push({
14454
+ value: row["value"],
14455
+ label: row["label"],
14456
+ ...typeof row["brand"] === "string" ? { brand: row["brand"] } : {},
14457
+ ...typeof row["region"] === "string" ? { region: row["region"] } : {}
14458
+ });
14459
+ }
14460
+ return out;
14461
+ }
14462
+ function selectedChoice(value) {
14463
+ return typeof value === "string" ? value : "";
14464
+ }
14465
+ function shouldComplete(text) {
14466
+ const trimmed = text.trim();
14467
+ if (trimmed.length === 0) return false;
14468
+ if (trimmed.includes("code=") || trimmed.includes("://") || trimmed.includes("error=")) return true;
14469
+ return BARE_CODE.test(trimmed);
14470
+ }
14471
+ function AccountSignInField({ field, values, disabled, onAction, onChangeMany }) {
14472
+ const t = useUiText();
14473
+ const [phase, setPhase] = (0, react$1.useState)("idle");
14474
+ const [attemptId, setAttemptId] = (0, react$1.useState)(null);
14475
+ const [authorizeUrl, setAuthorizeUrl] = (0, react$1.useState)(null);
14476
+ const [paste, setPaste] = (0, react$1.useState)("");
14477
+ const [error, setError] = (0, react$1.useState)(null);
14478
+ const [working, setWorking] = (0, react$1.useState)(false);
14479
+ const [choices, setChoices] = (0, react$1.useState)([]);
14480
+ const inFlight = (0, react$1.useRef)(false);
14481
+ const account = accountText(values[field.accountKey]);
14482
+ const tokenPresent = field.secretKeys.some((key) => isPresent(values[key]));
14483
+ const signedIn = account.length > 0 || tokenPresent;
14484
+ const count = (field.countKey !== void 0 ? countFromValue(values[field.countKey]) : null) ?? (field.countFromKey !== void 0 ? countFromValue(values[field.countFromKey]) : null);
14485
+ const showPaste = phase === "pasting" || !signedIn && attemptId !== null;
14486
+ const applyResult = (result) => {
14487
+ const patch = {};
14488
+ for (const [formKey, resultKey] of Object.entries(field.assignResult ?? {})) {
14489
+ const assigned = result[resultKey];
14490
+ if (typeof assigned === "string" || typeof assigned === "number") patch[formKey] = assigned;
14491
+ }
14492
+ if (Object.keys(patch).length > 0) onChangeMany?.(patch);
14493
+ const nextChoices = choicesOf(result["choices"]);
14494
+ setChoices(nextChoices.length > 1 ? nextChoices : []);
14495
+ setPhase("idle");
14496
+ setPaste("");
14497
+ setAttemptId(null);
14498
+ setAuthorizeUrl(null);
14499
+ };
14500
+ const begin = async () => {
14501
+ if (!onAction || working) return;
14502
+ setWorking(true);
14503
+ setError(null);
14504
+ try {
14505
+ const result = await onAction(field.beginAction, field.key, {});
14506
+ if (!isRecord$3(result) || typeof result["authorizeUrl"] !== "string" || typeof result["attemptId"] !== "string") {
14507
+ setError(t("uiLibrary.accountSignIn.noLink", "The sign-in page could not be opened. Try again."));
14508
+ return;
14509
+ }
14510
+ if ("verifier" in result || "state" in result) {
14511
+ setError(t("uiLibrary.accountSignIn.noLink", "The sign-in page could not be opened. Try again."));
14512
+ return;
14513
+ }
14514
+ setAttemptId(result["attemptId"]);
14515
+ setAuthorizeUrl(result["authorizeUrl"]);
14516
+ setPhase("pasting");
14517
+ window.open(result["authorizeUrl"], "_blank", "noopener,noreferrer");
14518
+ } catch (err) {
14519
+ setError(err instanceof Error ? err.message : String(err));
14520
+ } finally {
14521
+ setWorking(false);
14522
+ }
14523
+ };
14524
+ const complete = async (callback) => {
14525
+ if (!onAction || attemptId === null || inFlight.current) return;
14526
+ inFlight.current = true;
14527
+ setWorking(true);
14528
+ setError(null);
14529
+ try {
14530
+ const result = await onAction(field.completeAction, field.key, {
14531
+ attemptId,
14532
+ callback
14533
+ });
14534
+ if (!isRecord$3(result)) {
14535
+ setError(t("uiLibrary.accountSignIn.rejected", "The sign-in was not accepted. Start again."));
14536
+ return;
14537
+ }
14538
+ applyResult(result);
14539
+ } catch (err) {
14540
+ setError(err instanceof Error ? err.message : String(err));
14541
+ } finally {
14542
+ inFlight.current = false;
14543
+ setWorking(false);
14544
+ }
14545
+ };
14546
+ const signOut = async () => {
14547
+ if (!onAction || working) return;
14548
+ setWorking(true);
14549
+ setError(null);
14550
+ try {
14551
+ await onAction(field.signOutAction, field.key, {});
14552
+ const patch = { [field.accountKey]: "" };
14553
+ if (field.countKey !== void 0) patch[field.countKey] = 0;
14554
+ if (field.choiceKey !== void 0) patch[field.choiceKey] = "";
14555
+ if (field.countFromKey !== void 0) patch[field.countFromKey] = [];
14556
+ for (const key of field.secretKeys) patch[key] = _camstack_types.SECRET_CLEAR;
14557
+ onChangeMany?.(patch);
14558
+ setChoices([]);
14559
+ setPhase("idle");
14560
+ setAttemptId(null);
14561
+ setPaste("");
14562
+ } catch (err) {
14563
+ setError(err instanceof Error ? err.message : String(err));
14564
+ } finally {
14565
+ setWorking(false);
14566
+ }
14567
+ };
14568
+ const pick = (value) => {
14569
+ const chosen = choices.find((row) => row.value === value);
14570
+ if (chosen === void 0 || field.choiceKey === void 0) return;
14571
+ const patch = { [field.choiceKey]: chosen.value };
14572
+ if (chosen.brand !== void 0) patch["brand"] = chosen.brand;
14573
+ if (chosen.region !== void 0) patch["region"] = chosen.region;
14574
+ onChangeMany?.(patch);
14575
+ };
14576
+ const statusAccount = account.length > 0 ? account : t("uiLibrary.accountSignIn.yourAccount", "your account");
14577
+ const status = count === null ? t("uiLibrary.accountSignIn.signedIn", "Signed in as {{account}}", { account: statusAccount }) : t("uiLibrary.accountSignIn.signedInCount", "Signed in as {{account}} · {{detail}}", {
14578
+ account: statusAccount,
14579
+ detail: field.countNoun !== void 0 ? `${count} ${nounFor(count, field.countNoun)}` : String(count)
14580
+ });
14581
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14582
+ className: "col-span-1 flex min-w-0 flex-col gap-2",
14583
+ children: [
14584
+ signedIn && phase === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14585
+ className: "flex flex-col gap-2",
14586
+ children: [
14587
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14588
+ className: "text-xs text-foreground",
14589
+ children: status
14590
+ }),
14591
+ choices.length > 1 && field.choiceKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
14592
+ "aria-label": field.choiceLabel ?? t("uiLibrary.accountSignIn.choose", "Choose"),
14593
+ options: choices.map((row) => ({
14594
+ value: row.value,
14595
+ label: row.label
14596
+ })),
14597
+ value: selectedChoice(values[field.choiceKey]),
14598
+ onChange: (event) => pick(event.target.value),
14599
+ disabled: disabled || working
14600
+ }),
14601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14602
+ className: "flex flex-wrap gap-2",
14603
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
14604
+ type: "button",
14605
+ variant: "outline",
14606
+ size: "sm",
14607
+ disabled: disabled || working,
14608
+ onClick: () => void begin(),
14609
+ children: t("uiLibrary.accountSignIn.again", "Sign in again")
14610
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
14611
+ type: "button",
14612
+ variant: "ghost",
14613
+ size: "sm",
14614
+ disabled: disabled || working,
14615
+ onClick: () => void signOut(),
14616
+ children: t("uiLibrary.accountSignIn.signOut", "Sign out")
14617
+ })]
14618
+ })
14619
+ ]
14620
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
14621
+ type: "button",
14622
+ variant: "primary",
14623
+ size: "sm",
14624
+ disabled: disabled || working,
14625
+ onClick: () => void begin(),
14626
+ children: working && phase !== "pasting" ? t("uiLibrary.accountSignIn.working", "Working…") : t("uiLibrary.accountSignIn.signInWith", "Sign in with {{provider}}", { provider: field.providerLabel })
14627
+ }),
14628
+ showPaste && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14629
+ className: "flex flex-col gap-1.5",
14630
+ children: [authorizeUrl !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
14631
+ className: "text-[11px] text-primary break-all",
14632
+ href: authorizeUrl,
14633
+ target: "_blank",
14634
+ rel: "noopener noreferrer",
14635
+ children: t("uiLibrary.accountSignIn.open", "Open the sign-in page")
14636
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
14637
+ className: `${INPUT_CLASS$1} min-h-[60px] resize-y`,
14638
+ "aria-label": t("uiLibrary.accountSignIn.paste", "Redirect address or code"),
14639
+ placeholder: field.pastePlaceholder,
14640
+ value: paste,
14641
+ disabled: disabled || working,
14642
+ onChange: (event) => {
14643
+ const next = event.target.value;
14644
+ setPaste(next);
14645
+ if (shouldComplete(next)) complete(next);
14646
+ }
14647
+ })]
14648
+ }),
14649
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14650
+ className: "text-[11px] text-danger",
14651
+ children: error
14652
+ })
14653
+ ]
14654
+ });
14655
+ }
14656
+ function SecretPresenceField({ field, values }) {
14657
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14658
+ className: "col-span-1 text-xs text-foreground",
14659
+ children: isPresent(values[field.sourceKey]) ? field.yesLabel : field.noLabel
14660
+ });
14661
+ }
14662
+ //#endregion
14385
14663
  //#region src/composites/confirm-dialog.tsx
14386
14664
  /**
14387
14665
  * The ONE modal dialog of the product — confirm, alert and prompt.
@@ -20685,39 +20963,6 @@ var Skeleton = (0, react$1.forwardRef)(({ className, ...props }, ref) => {
20685
20963
  });
20686
20964
  Skeleton.displayName = "Skeleton";
20687
20965
  //#endregion
20688
- //#region src/primitives/button.tsx
20689
- var buttonVariants = cva("inline-flex items-center justify-center rounded-md font-medium transition-colors cursor-pointer disabled:opacity-50 disabled:pointer-events-none", {
20690
- variants: {
20691
- variant: {
20692
- primary: "bg-primary text-primary-foreground hover:bg-primary/90",
20693
- secondary: "bg-surface text-foreground hover:bg-surface-hover",
20694
- ghost: "hover:bg-surface-hover text-foreground-muted",
20695
- danger: "bg-danger text-white hover:bg-danger/90",
20696
- outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
20697
- },
20698
- size: {
20699
- sm: "h-7 px-2.5 text-xs gap-1.5",
20700
- md: "h-8 px-3 text-sm gap-2",
20701
- lg: "h-9 px-4 text-sm gap-2"
20702
- }
20703
- },
20704
- defaultVariants: {
20705
- variant: "primary",
20706
- size: "sm"
20707
- }
20708
- });
20709
- var Button = (0, react$1.forwardRef)(({ className, variant, size, ...props }, ref) => {
20710
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
20711
- ref,
20712
- className: cn(buttonVariants({
20713
- variant,
20714
- size
20715
- }), className),
20716
- ...props
20717
- });
20718
- });
20719
- Button.displayName = "Button";
20720
- //#endregion
20721
20966
  //#region src/composites/cap-settings/consumable-rows.tsx
20722
20967
  /** `value` null = unknown (D393) — renders an em-dash, never a fake `0`. */
20723
20968
  function formatRemaining(t, r) {
@@ -29962,7 +30207,7 @@ function ProbeField({ field, value, onChange, disabled, translationFn, onTestFie
29962
30207
  ]
29963
30208
  });
29964
30209
  }
29965
- function GroupField({ field, values, onChange, disabled, translationFn, onTestField }) {
30210
+ function GroupField({ field, values, onChange, onChangeMany, disabled, translationFn, onTestField, onAction }) {
29966
30211
  const [collapsed, setCollapsed] = (0, react$1.useState)(field.defaultCollapsed ?? false);
29967
30212
  const isAccordion = field.style === "accordion";
29968
30213
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -29986,9 +30231,11 @@ function GroupField({ field, values, onChange, disabled, translationFn, onTestFi
29986
30231
  field: subField,
29987
30232
  values,
29988
30233
  onChange,
30234
+ onChangeMany,
29989
30235
  disabled,
29990
30236
  translationFn,
29991
- onTestField
30237
+ onTestField,
30238
+ onAction
29992
30239
  }, subField.key))
29993
30240
  })]
29994
30241
  })
@@ -30404,6 +30651,10 @@ function AddonActionSelectField({ field, value, values, onChange, disabled, onAc
30404
30651
  }
30405
30652
  }
30406
30653
  setOptions(list);
30654
+ if (field.hideWhenFewerThan !== void 0 && list.length === 1 && (typeof value !== "string" || value.length === 0)) {
30655
+ const only = list[0]?.[valueKey];
30656
+ if (typeof only === "string" && only.length > 0) onChange(only);
30657
+ }
30407
30658
  }).catch((err) => {
30408
30659
  if (cancelled) return;
30409
30660
  setError(err instanceof Error ? err.message : String(err));
@@ -30423,6 +30674,8 @@ function AddonActionSelectField({ field, value, values, onChange, disabled, onAc
30423
30674
  const valueKey = field.mapOption.value;
30424
30675
  const labelKey = field.mapOption.label;
30425
30676
  const descKey = field.mapOption.description;
30677
+ if (!onAction && field.hideWhenFewerThan !== void 0) return null;
30678
+ if (field.hideWhenFewerThan !== void 0 && options !== null && options.length < field.hideWhenFewerThan) return null;
30426
30679
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
30427
30680
  label: field.label,
30428
30681
  description: field.description,
@@ -31055,9 +31308,11 @@ function FormField({ field, values, onChange, onChangeMany, disabled, translatio
31055
31308
  field,
31056
31309
  values,
31057
31310
  onChange,
31311
+ onChangeMany,
31058
31312
  disabled,
31059
31313
  translationFn,
31060
- onTestField
31314
+ onTestField,
31315
+ onAction
31061
31316
  });
31062
31317
  case "sub-tabs": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubTabsField, {
31063
31318
  field,
@@ -31086,22 +31341,36 @@ function FormField({ field, values, onChange, onChangeMany, disabled, translatio
31086
31341
  disabled,
31087
31342
  onAction
31088
31343
  });
31089
- case "addon-action-select": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddonActionSelectField, {
31344
+ case "addon-action-select": {
31345
+ const storedKey = field.writesKey ?? field.key;
31346
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddonActionSelectField, {
31347
+ field,
31348
+ value: values[storedKey],
31349
+ values,
31350
+ onChange: (next) => onChange(storedKey, next),
31351
+ disabled,
31352
+ onAction
31353
+ });
31354
+ }
31355
+ case "addon-action-button": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddonActionButtonField, {
31090
31356
  field,
31091
- value,
31092
31357
  values,
31093
- onChange: handleChange,
31094
31358
  disabled,
31095
- onAction
31359
+ onAction,
31360
+ onChange,
31361
+ onChangeMany
31096
31362
  });
31097
- case "addon-action-button": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddonActionButtonField, {
31363
+ case "account-sign-in": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountSignInField, {
31098
31364
  field,
31099
31365
  values,
31100
31366
  disabled,
31101
31367
  onAction,
31102
- onChange,
31103
31368
  onChangeMany
31104
31369
  });
31370
+ case "secret-presence": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretPresenceField, {
31371
+ field,
31372
+ values
31373
+ });
31105
31374
  case "device-action-button": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceActionButtonField, {
31106
31375
  field,
31107
31376
  values,
@@ -33644,6 +33913,99 @@ function BatteryBadge({ status, variant = "compact", className }) {
33644
33913
  });
33645
33914
  }
33646
33915
  //#endregion
33916
+ //#region src/composites/running-status.ts
33917
+ function trimmedPhase(phase) {
33918
+ if (phase === null) return void 0;
33919
+ const trimmed = phase.trim();
33920
+ return trimmed === "" ? void 0 : trimmed;
33921
+ }
33922
+ /** Minutes elapsed since `since`, at least 1; null when `since` is not usable. */
33923
+ function runningDurationSince(since, now) {
33924
+ if (since === null || !Number.isFinite(since) || since <= 0) return null;
33925
+ const ms = now - since;
33926
+ if (ms <= 0) return null;
33927
+ const totalMinutes = Math.floor(ms / 6e4);
33928
+ if (totalMinutes < 60) return { minutes: Math.max(1, totalMinutes) };
33929
+ return { hours: Math.floor(totalMinutes / 60) };
33930
+ }
33931
+ function runningStatusParts(running, since, phase, now) {
33932
+ const phaseLabel = trimmedPhase(phase);
33933
+ if (running === null) return { key: "unknown" };
33934
+ if (running === false) return phaseLabel === void 0 ? { key: "idle" } : {
33935
+ key: "idle",
33936
+ phase: phaseLabel
33937
+ };
33938
+ const duration = runningDurationSince(since, now);
33939
+ if (duration === null) return phaseLabel === void 0 ? { key: "running" } : {
33940
+ key: "running",
33941
+ phase: phaseLabel
33942
+ };
33943
+ return {
33944
+ key: "running",
33945
+ duration,
33946
+ ...phaseLabel === void 0 ? {} : { phase: phaseLabel }
33947
+ };
33948
+ }
33949
+ //#endregion
33950
+ //#region src/composites/running-badge.tsx
33951
+ function RunningBadge({ running, children, className }) {
33952
+ const t = useUiText();
33953
+ if (running === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
33954
+ "data-running": "unknown",
33955
+ className: cn("inline-flex shrink-0", className),
33956
+ children
33957
+ });
33958
+ if (running === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
33959
+ "data-running": "running",
33960
+ className: cn("relative inline-flex shrink-0 rounded-full p-0.5", className),
33961
+ "aria-label": t("uiLibrary.runningBadge.running", "Running"),
33962
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
33963
+ className: "pointer-events-none absolute inset-0 rounded-full border border-primary animate-pulse",
33964
+ "aria-hidden": true
33965
+ }), children]
33966
+ });
33967
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
33968
+ "data-running": "idle",
33969
+ className: cn("relative inline-flex shrink-0", className),
33970
+ "aria-label": t("uiLibrary.runningBadge.idle", "Idle"),
33971
+ children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
33972
+ className: "pointer-events-none absolute -bottom-px -right-px h-1.5 w-1.5 rounded-full bg-foreground-subtle",
33973
+ "aria-hidden": true
33974
+ })]
33975
+ });
33976
+ }
33977
+ function RunningStatusLine({ running, since, phase, className, now = Date.now() }) {
33978
+ const t = useUiText();
33979
+ const parts = runningStatusParts(running, since, phase, now);
33980
+ if (parts.key === "unknown") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
33981
+ className: cn("text-[11px] text-foreground-subtle", className),
33982
+ children: t("uiLibrary.runningStatus.unknown", "Unknown")
33983
+ });
33984
+ if (parts.key === "idle") {
33985
+ const base = t("uiLibrary.runningStatus.idle", "Idle");
33986
+ const text = parts.phase === void 0 ? base : t("uiLibrary.runningStatus.withPhase", "{{state}} · {{phase}}", {
33987
+ state: base,
33988
+ phase: parts.phase
33989
+ });
33990
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
33991
+ className: cn("text-[11px] text-foreground-muted", className),
33992
+ children: text
33993
+ });
33994
+ }
33995
+ let base;
33996
+ if (parts.duration === void 0) base = t("uiLibrary.runningStatus.running", "Running");
33997
+ else if ("minutes" in parts.duration) base = t("uiLibrary.runningStatus.runningForMinutes", "Running for {{count}} min", { count: parts.duration.minutes });
33998
+ else base = t("uiLibrary.runningStatus.runningForHours", "Running for {{count}} h", { count: parts.duration.hours });
33999
+ const text = parts.phase === void 0 ? base : t("uiLibrary.runningStatus.withPhase", "{{state}} · {{phase}}", {
34000
+ state: base,
34001
+ phase: parts.phase
34002
+ });
34003
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34004
+ className: cn("text-[11px] text-foreground-muted", className),
34005
+ children: text
34006
+ });
34007
+ }
34008
+ //#endregion
33647
34009
  //#region src/composites/network-link-badge.tsx
33648
34010
  /**
33649
34011
  * NetworkLinkBadge — compact link indicator for device rows and headers.
@@ -50584,6 +50946,18 @@ function dishwasherHeadline(status, t = defaultUiText) {
50584
50946
  function featureLabel(key) {
50585
50947
  return (key.split(".").pop() ?? key).replace(/([a-z0-9])([A-Z])/g, "$1 $2");
50586
50948
  }
50949
+ /**
50950
+ * Salt and rinse aid are consumable items (D915). Their events must not
50951
+ * reappear on the hero — the Consumables section is the only surface.
50952
+ */
50953
+ function isConsumableDishwasherEvent(event) {
50954
+ const short = (event.split(".").pop() ?? event).toLowerCase();
50955
+ return short.includes("salt") || short.includes("rinse");
50956
+ }
50957
+ /** Raised events the hero may name. Consumable events are dropped. */
50958
+ function visibleDishwasherEvents(events) {
50959
+ return events.filter((event) => !isConsumableDishwasherEvent(event));
50960
+ }
50587
50961
  /** Whole seconds as a short duration: `1 h 24 min`, `42 min`, `30 s`. */
50588
50962
  function formatDuration(seconds, t = defaultUiText) {
50589
50963
  if (seconds < 60) return t("uiLibrary.dishwasher.durationSeconds", "{{n}} s", { n: seconds });
@@ -50740,7 +51114,7 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
50740
51114
  children: [display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50741
51115
  className: "inline-flex items-center gap-1 text-xs text-warning",
50742
51116
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
50743
- }), display.activeEvents.map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
51117
+ }), visibleDishwasherEvents(display.activeEvents).map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50744
51118
  className: "text-xs text-warning",
50745
51119
  title: event,
50746
51120
  children: featureLabel(event)
@@ -55230,6 +55604,8 @@ function ChildRow$1({ trpc, child, onNavigate, parentName }) {
55230
55604
  const showMotionTrigger = enabled && hasMotionTriggerFeature(child);
55231
55605
  const roleDefaults = useRoleDisplayDefaults();
55232
55606
  useIconGlyphsVersion();
55607
+ const ChildIcon = deviceLeadIcon(child, roleDefaults);
55608
+ const runningSlice = useDeviceState(trpc, child.id, (s) => s.runningState);
55233
55609
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55234
55610
  onClick: (e) => {
55235
55611
  e.stopPropagation();
@@ -55237,7 +55613,10 @@ function ChildRow$1({ trpc, child, onNavigate, parentName }) {
55237
55613
  },
55238
55614
  className: "w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-surface-hover transition-colors cursor-pointer",
55239
55615
  children: [
55240
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(deviceLeadIcon(child, roleDefaults), { className: "h-3 w-3 text-foreground-subtle flex-shrink-0" }),
55616
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
55617
+ running: runningSlice === void 0 || runningSlice.running === null ? null : runningSlice.running,
55618
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChildIcon, { className: "h-3 w-3 text-foreground-subtle flex-shrink-0" })
55619
+ }),
55241
55620
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55242
55621
  className: "text-[10.5px] text-foreground flex-1 truncate",
55243
55622
  children: childListName(child, parentName)
@@ -55631,13 +56010,16 @@ var CHEVRON_SLOT_CLASS = {
55631
56010
  horizontal: "h-3.5 w-3.5",
55632
56011
  stacked: "h-4 w-4 mt-0.5"
55633
56012
  };
55634
- function DeviceItemHeader({ device, variant, flags, parent, integrationIcon, hasChildren, expanded, onToggleExpand, onNavigateToParent, stacked = false, nameOverflow = "truncate" }) {
56013
+ function DeviceItemHeader({ trpc, device, variant, flags, parent, integrationIcon, hasChildren, expanded, onToggleExpand, onNavigateToParent, stacked = false, nameOverflow = "truncate" }) {
55635
56014
  const t = useUiText();
55636
56015
  const containerChildren = useContainerChildren(device.id);
55637
56016
  const roleDefaults = useRoleDisplayDefaults();
55638
56017
  useIconGlyphsVersion();
55639
56018
  const Icon = deviceLeadIcon(effectiveIconDevice(device, containerChildren), roleDefaults);
56019
+ const runningSlice = useDeviceState(trpc, device.id, (s) => s.runningState);
56020
+ const runningBadge = runningSlice === void 0 || runningSlice.running === null ? null : runningSlice.running;
55640
56021
  const slotClass = stacked ? CHEVRON_SLOT_CLASS.stacked : CHEVRON_SLOT_CLASS.horizontal;
56022
+ const iconClass = cn("text-foreground-subtle flex-shrink-0", stacked ? "h-4 w-4 mt-0.5" : variant === "minimal" ? "h-3 w-3" : "h-3.5 w-3.5");
55641
56023
  const lineClass = nameOverflow === "scroll" ? NAME_TEXT_CLASS : "truncate";
55642
56024
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55643
56025
  className: cn(stacked ? "flex items-start gap-2 w-full min-w-0" : "flex items-center gap-2 flex-1 min-w-0"),
@@ -55657,7 +56039,10 @@ function DeviceItemHeader({ device, variant, flags, parent, integrationIcon, has
55657
56039
  "data-testid": "chevron-slot",
55658
56040
  className: cn("flex-shrink-0", slotClass)
55659
56041
  }) : null,
55660
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: cn("text-foreground-subtle flex-shrink-0", stacked ? "h-4 w-4 mt-0.5" : variant === "minimal" ? "h-3 w-3" : "h-3.5 w-3.5") }),
56042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RunningBadge, {
56043
+ running: runningBadge,
56044
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: iconClass })
56045
+ }),
55661
56046
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55662
56047
  className: "flex-1 min-w-0",
55663
56048
  children: [
@@ -56151,6 +56536,7 @@ function DeviceItem(props) {
56151
56536
  className: "flex-shrink-0"
56152
56537
  }),
56153
56538
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceItemHeader, {
56539
+ trpc,
56154
56540
  device,
56155
56541
  variant,
56156
56542
  flags,
@@ -56227,6 +56613,7 @@ function DeviceItem(props) {
56227
56613
  className: "flex-shrink-0 mt-0.5"
56228
56614
  }),
56229
56615
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceItemHeader, {
56616
+ trpc,
56230
56617
  device,
56231
56618
  variant,
56232
56619
  flags,
@@ -56318,6 +56705,7 @@ function DeviceItemTableRow(props) {
56318
56705
  "aria-label": t("uiLibrary.deviceItem.select", "Select {{name}}", { name: device.name }),
56319
56706
  className: "flex-shrink-0"
56320
56707
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceItemHeader, {
56708
+ trpc,
56321
56709
  device,
56322
56710
  variant,
56323
56711
  flags,
@@ -68488,6 +68876,8 @@ exports.RotateCcw = RotateCcw;
68488
68876
  exports.RotateCw = RotateCw;
68489
68877
  exports.Route = Route;
68490
68878
  exports.RunReport = RunReport;
68879
+ exports.RunningBadge = RunningBadge;
68880
+ exports.RunningStatusLine = RunningStatusLine;
68491
68881
  exports.SCENE_CAPTURE_VARIANTS = SCENE_CAPTURE_VARIANTS;
68492
68882
  exports.SCENE_CAPTURE_VARIANTS$1 = SCENE_CAPTURE_VARIANTS;
68493
68883
  exports.SECRET_MASK_PLACEHOLDER = SECRET_MASK_PLACEHOLDER;
@@ -68929,6 +69319,8 @@ exports.roiTooLarge = roiTooLarge;
68929
69319
  exports.roiTooLarge$1 = roiTooLarge;
68930
69320
  exports.rootSourceState = rootSourceState;
68931
69321
  exports.routerBasename = routerBasename;
69322
+ exports.runningDurationSince = runningDurationSince;
69323
+ exports.runningStatusParts = runningStatusParts;
68932
69324
  exports.sameLocationSuggestions = sameLocationSuggestions;
68933
69325
  exports.scrubReducer = scrubReducer;
68934
69326
  exports.searchSteps = searchSteps;
@@ -70023,6 +70415,7 @@ exports.useRecordingOnboardSetSettings = useRecordingOnboardSetSettings;
70023
70415
  exports.useRecordingSignalGetStatus = useRecordingSignalGetStatus;
70024
70416
  exports.useRemoteComponent = useRemoteComponent;
70025
70417
  exports.useRoleDisplayDefaults = useRoleDisplayDefaults;
70418
+ exports.useRunningStateGetStatus = useRunningStateGetStatus;
70026
70419
  exports.useSceneMonitorCaptureReference = useSceneMonitorCaptureReference;
70027
70420
  exports.useSceneMonitorCreateScene = useSceneMonitorCreateScene;
70028
70421
  exports.useSceneMonitorDeleteReference = useSceneMonitorDeleteReference;