@camstack/ui-library 1.2.319 → 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,
@@ -32467,9 +32736,36 @@ function ConfigSchemaFieldControl({ field, allFields, step, disabled, modelScope
32467
32736
  }
32468
32737
  //#endregion
32469
32738
  //#region src/composites/step-inspector/setting-values.ts
32739
+ /**
32740
+ * How the step inspector names a stored setting value, and what a field falls
32741
+ * back to when no layer stores it. Pure, so the provenance line and its tests
32742
+ * agree on one spelling.
32743
+ */
32470
32744
  function isValueField(field) {
32471
32745
  return field.type !== "separator" && field.type !== "info" && field.type !== "group";
32472
32746
  }
32747
+ /**
32748
+ * A field only the accelerator that runs the model may set (D927): a model id,
32749
+ * a `<key>ModelId` binding, a `minConfidence*` floor, or any field the schema
32750
+ * marks as bound to its model. A camera never renders one as an input.
32751
+ */
32752
+ function isModelScopedField(field) {
32753
+ if ("bindsToModel" in field && field.bindsToModel === true) return true;
32754
+ return "key" in field && (0, _camstack_types.isModelScopedStepSettingKey)(field.key);
32755
+ }
32756
+ /** The fields a CAMERA may edit: every model-scoped field removed, groups recursed. */
32757
+ function withoutModelScopedFields(fields) {
32758
+ return fields.flatMap((field) => {
32759
+ if (field.type === "group") {
32760
+ const inner = withoutModelScopedFields(field.fields);
32761
+ return inner.length > 0 ? [{
32762
+ ...field,
32763
+ fields: [...inner]
32764
+ }] : [];
32765
+ }
32766
+ return isModelScopedField(field) ? [] : [field];
32767
+ });
32768
+ }
32473
32769
  /** True for a D738 field whose value is stored with the model it was made for. */
32474
32770
  function bindsToModel(field) {
32475
32771
  return (field.type === "slider" || field.type === "number") && field.bindsToModel === true;
@@ -32586,7 +32882,7 @@ function SettingProvenance({ field, layers, target, labels, boundKeys, modelId,
32586
32882
  children: withDefault.join(" · ")
32587
32883
  }),
32588
32884
  pinned && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32589
- className: "rounded border border-amber-500/40 px-1.5 py-px text-amber-500",
32885
+ className: "rounded border border-warning/40 px-1.5 py-px text-warning",
32590
32886
  title: t("uiLibrary.layeredSettings.pinnedHint", "Stored by an older editor as a copy of the value below. It changes nothing today, but it stops later changes below from reaching this camera."),
32591
32887
  children: t("uiLibrary.layeredSettings.pinned", "pinned at the {{source}} value", { source: pinnedAt.toLowerCase() })
32592
32888
  }),
@@ -32603,8 +32899,9 @@ function SettingProvenance({ field, layers, target, labels, boundKeys, modelId,
32603
32899
  ]
32604
32900
  });
32605
32901
  }
32606
- function LayeredSettingsForm({ addon, fields, layers, target, labels, modelId, onChangeLayer, rootModel, pinnedKeys = [], fieldActions, headerActions, highlightKey = null, disabled = false }) {
32902
+ function LayeredSettingsForm({ addon, fields: givenFields, layers, target, labels, modelId, onChangeLayer, rootModel, pinnedKeys = [], fieldActions, headerActions, highlightKey = null, disabled = false }) {
32607
32903
  const t = useUiText();
32904
+ const fields = target === "camera" ? withoutModelScopedFields(givenFields) : givenFields;
32608
32905
  const boundKeys = (0, _camstack_types.collectModelBoundKeys)(fields);
32609
32906
  const effective = (0, _camstack_types.effectiveStepSettings)(layers);
32610
32907
  const targetLayer = layers[target];
@@ -33616,6 +33913,99 @@ function BatteryBadge({ status, variant = "compact", className }) {
33616
33913
  });
33617
33914
  }
33618
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
33619
34009
  //#region src/composites/network-link-badge.tsx
33620
34010
  /**
33621
34011
  * NetworkLinkBadge — compact link indicator for device rows and headers.
@@ -50556,6 +50946,18 @@ function dishwasherHeadline(status, t = defaultUiText) {
50556
50946
  function featureLabel(key) {
50557
50947
  return (key.split(".").pop() ?? key).replace(/([a-z0-9])([A-Z])/g, "$1 $2");
50558
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
+ }
50559
50961
  /** Whole seconds as a short duration: `1 h 24 min`, `42 min`, `30 s`. */
50560
50962
  function formatDuration(seconds, t = defaultUiText) {
50561
50963
  if (seconds < 60) return t("uiLibrary.dishwasher.durationSeconds", "{{n}} s", { n: seconds });
@@ -50712,7 +51114,7 @@ function DishwasherPanel({ trpc, deviceId, layout }) {
50712
51114
  children: [display.doorOpen === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50713
51115
  className: "inline-flex items-center gap-1 text-xs text-warning",
50714
51116
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DoorOpen, { className: "h-3.5 w-3.5" }), t("uiLibrary.dishwasher.doorOpen", "Door open")]
50715
- }), display.activeEvents.map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
51117
+ }), visibleDishwasherEvents(display.activeEvents).map((event) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50716
51118
  className: "text-xs text-warning",
50717
51119
  title: event,
50718
51120
  children: featureLabel(event)
@@ -55202,6 +55604,8 @@ function ChildRow$1({ trpc, child, onNavigate, parentName }) {
55202
55604
  const showMotionTrigger = enabled && hasMotionTriggerFeature(child);
55203
55605
  const roleDefaults = useRoleDisplayDefaults();
55204
55606
  useIconGlyphsVersion();
55607
+ const ChildIcon = deviceLeadIcon(child, roleDefaults);
55608
+ const runningSlice = useDeviceState(trpc, child.id, (s) => s.runningState);
55205
55609
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55206
55610
  onClick: (e) => {
55207
55611
  e.stopPropagation();
@@ -55209,7 +55613,10 @@ function ChildRow$1({ trpc, child, onNavigate, parentName }) {
55209
55613
  },
55210
55614
  className: "w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-surface-hover transition-colors cursor-pointer",
55211
55615
  children: [
55212
- /* @__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
+ }),
55213
55620
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
55214
55621
  className: "text-[10.5px] text-foreground flex-1 truncate",
55215
55622
  children: childListName(child, parentName)
@@ -55603,13 +56010,16 @@ var CHEVRON_SLOT_CLASS = {
55603
56010
  horizontal: "h-3.5 w-3.5",
55604
56011
  stacked: "h-4 w-4 mt-0.5"
55605
56012
  };
55606
- 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" }) {
55607
56014
  const t = useUiText();
55608
56015
  const containerChildren = useContainerChildren(device.id);
55609
56016
  const roleDefaults = useRoleDisplayDefaults();
55610
56017
  useIconGlyphsVersion();
55611
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;
55612
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");
55613
56023
  const lineClass = nameOverflow === "scroll" ? NAME_TEXT_CLASS : "truncate";
55614
56024
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55615
56025
  className: cn(stacked ? "flex items-start gap-2 w-full min-w-0" : "flex items-center gap-2 flex-1 min-w-0"),
@@ -55629,7 +56039,10 @@ function DeviceItemHeader({ device, variant, flags, parent, integrationIcon, has
55629
56039
  "data-testid": "chevron-slot",
55630
56040
  className: cn("flex-shrink-0", slotClass)
55631
56041
  }) : null,
55632
- /* @__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
+ }),
55633
56046
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55634
56047
  className: "flex-1 min-w-0",
55635
56048
  children: [
@@ -56123,6 +56536,7 @@ function DeviceItem(props) {
56123
56536
  className: "flex-shrink-0"
56124
56537
  }),
56125
56538
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceItemHeader, {
56539
+ trpc,
56126
56540
  device,
56127
56541
  variant,
56128
56542
  flags,
@@ -56199,6 +56613,7 @@ function DeviceItem(props) {
56199
56613
  className: "flex-shrink-0 mt-0.5"
56200
56614
  }),
56201
56615
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceItemHeader, {
56616
+ trpc,
56202
56617
  device,
56203
56618
  variant,
56204
56619
  flags,
@@ -56290,6 +56705,7 @@ function DeviceItemTableRow(props) {
56290
56705
  "aria-label": t("uiLibrary.deviceItem.select", "Select {{name}}", { name: device.name }),
56291
56706
  className: "flex-shrink-0"
56292
56707
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceItemHeader, {
56708
+ trpc,
56293
56709
  device,
56294
56710
  variant,
56295
56711
  flags,
@@ -61234,6 +61650,7 @@ function RecordEntrySecretField({ name, label, required, value, disabled, onPatc
61234
61650
  function SchemaField(props) {
61235
61651
  const { path, name, node, value, secretPaths, required, disabled, onPatch } = props;
61236
61652
  const label = labelFor(name, node);
61653
+ const uiText = useUiText();
61237
61654
  if (isSecretField(path, secretPaths, node)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordEntrySecretField, {
61238
61655
  name,
61239
61656
  label,
@@ -61305,7 +61722,7 @@ function SchemaField(props) {
61305
61722
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormField$1, {
61306
61723
  label,
61307
61724
  required,
61308
- description: "JSON object",
61725
+ description: uiText("uiLibrary.recordEntries.jsonObject", "JSON object"),
61309
61726
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
61310
61727
  className: cn("min-h-[8rem] w-full resize-y rounded-md border border-border bg-surface px-2.5 py-2 font-mono text-xs text-foreground", "placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-inset focus:ring-primary", "disabled:cursor-not-allowed disabled:opacity-50"),
61311
61728
  rows: 6,
@@ -62062,9 +62479,8 @@ function ResponseLog({ data, label = "Response", className = "" }) {
62062
62479
  * `clampToParent` narrows the target dropdown + access checkboxes to
62063
62480
  * what the CALLER possesses. Admins pass `null` to allow everything.
62064
62481
  *
62065
- * This is the RAW editor. The simple, two-question Access panel on
62066
- * `/system/users` composes the same wire shape through `ACCESS_ROLES`; this
62067
- * one is what an operator drops into when a grant set is not a plain role.
62482
+ * This is the RAW editor, for TOKENS. People are not edited here: a person
62483
+ * is an Admin or a Viewer (D921), set from `/system/identity?tab=users`.
62068
62484
  */
62069
62485
  /** `<select>` value → the tri-state `includeLinked` it means. */
62070
62486
  var INCLUDE_LINKED_VALUES = {
@@ -68460,6 +68876,8 @@ exports.RotateCcw = RotateCcw;
68460
68876
  exports.RotateCw = RotateCw;
68461
68877
  exports.Route = Route;
68462
68878
  exports.RunReport = RunReport;
68879
+ exports.RunningBadge = RunningBadge;
68880
+ exports.RunningStatusLine = RunningStatusLine;
68463
68881
  exports.SCENE_CAPTURE_VARIANTS = SCENE_CAPTURE_VARIANTS;
68464
68882
  exports.SCENE_CAPTURE_VARIANTS$1 = SCENE_CAPTURE_VARIANTS;
68465
68883
  exports.SECRET_MASK_PLACEHOLDER = SECRET_MASK_PLACEHOLDER;
@@ -68800,6 +69218,7 @@ exports.isFieldVisible = isFieldVisible;
68800
69218
  exports.isGroupExpanded = isGroupExpanded;
68801
69219
  exports.isHistoryWindowId = isHistoryWindowId;
68802
69220
  exports.isKeychainFieldValue = isKeychainFieldValue;
69221
+ exports.isModelScopedField = isModelScopedField;
68803
69222
  exports.isRedactedSecret = isRedactedSecret;
68804
69223
  exports.isRefusal = isRefusal;
68805
69224
  exports.isSecretField = isSecretField;
@@ -68900,6 +69319,8 @@ exports.roiTooLarge = roiTooLarge;
68900
69319
  exports.roiTooLarge$1 = roiTooLarge;
68901
69320
  exports.rootSourceState = rootSourceState;
68902
69321
  exports.routerBasename = routerBasename;
69322
+ exports.runningDurationSince = runningDurationSince;
69323
+ exports.runningStatusParts = runningStatusParts;
68903
69324
  exports.sameLocationSuggestions = sameLocationSuggestions;
68904
69325
  exports.scrubReducer = scrubReducer;
68905
69326
  exports.searchSteps = searchSteps;
@@ -69994,6 +70415,7 @@ exports.useRecordingOnboardSetSettings = useRecordingOnboardSetSettings;
69994
70415
  exports.useRecordingSignalGetStatus = useRecordingSignalGetStatus;
69995
70416
  exports.useRemoteComponent = useRemoteComponent;
69996
70417
  exports.useRoleDisplayDefaults = useRoleDisplayDefaults;
70418
+ exports.useRunningStateGetStatus = useRunningStateGetStatus;
69997
70419
  exports.useSceneMonitorCaptureReference = useSceneMonitorCaptureReference;
69998
70420
  exports.useSceneMonitorCreateScene = useSceneMonitorCreateScene;
69999
70421
  exports.useSceneMonitorDeleteReference = useSceneMonitorDeleteReference;
@@ -70264,5 +70686,6 @@ exports.withHiddenChart = withHiddenChart;
70264
70686
  exports.withPanel = withPanel;
70265
70687
  exports.withResolvedParentDetail = withResolvedParentDetail;
70266
70688
  exports.withUseRootModel = withUseRootModel;
70689
+ exports.withoutModelScopedFields = withoutModelScopedFields;
70267
70690
  exports.withoutPanel = withoutPanel;
70268
70691
  exports.writeStoredGroupBy = writeStoredGroupBy;