@camstack/ui-library 1.2.347 → 1.2.348

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
@@ -12528,6 +12528,16 @@ var useRecordingOnboardSetSettings = trpc.recordingOnboard.setSettings.useMutati
12528
12528
  var useRecordingOnboardGetStatus = trpc.recordingOnboard.getStatus.useQuery;
12529
12529
  /** Generated alias around `trpc.recordingSignal.getStatus.useQuery`. */
12530
12530
  var useRecordingSignalGetStatus = trpc.recordingSignal.getStatus.useQuery;
12531
+ /** Generated alias around `trpc.remote.getConfig.useQuery`. */
12532
+ var useRemoteGetConfig = trpc.remote.getConfig.useQuery;
12533
+ /** Generated alias around `trpc.remote.setConfig.useMutation`. */
12534
+ var useRemoteSetConfig = trpc.remote.setConfig.useMutation;
12535
+ /** Generated alias around `trpc.remote.listCommands.useQuery`. */
12536
+ var useRemoteListCommands = trpc.remote.listCommands.useQuery;
12537
+ /** Generated alias around `trpc.remote.sendCommand.useMutation`. */
12538
+ var useRemoteSendCommand = trpc.remote.sendCommand.useMutation;
12539
+ /** Generated alias around `trpc.remote.sendMediaCommand.useMutation`. */
12540
+ var useRemoteSendMediaCommand = trpc.remote.sendMediaCommand.useMutation;
12531
12541
  /** Generated alias around `trpc.runningState.getStatus.useQuery`. */
12532
12542
  var useRunningStateGetStatus = trpc.runningState.getStatus.useQuery;
12533
12543
  /** Generated alias around `trpc.sceneMonitor.listScenes.useQuery`. */
@@ -12814,6 +12824,16 @@ var useSystemSetLoggingSettings = trpc.system.setLoggingSettings.useMutation;
12814
12824
  var useTamperGetStatus = trpc.tamper.getStatus.useQuery;
12815
12825
  /** Generated alias around `trpc.temperatureSensor.getStatus.useQuery`. */
12816
12826
  var useTemperatureSensorGetStatus = trpc.temperatureSensor.getStatus.useQuery;
12827
+ /** Generated alias around `trpc.template.list.useQuery`. */
12828
+ var useTemplateList = trpc.template.list.useQuery;
12829
+ /** Generated alias around `trpc.template.get.useQuery`. */
12830
+ var useTemplateGet = trpc.template.get.useQuery;
12831
+ /** Generated alias around `trpc.template.create.useMutation`. */
12832
+ var useTemplateCreate = trpc.template.create.useMutation;
12833
+ /** Generated alias around `trpc.template.update.useMutation`. */
12834
+ var useTemplateUpdate = trpc.template.update.useMutation;
12835
+ /** Generated alias around `trpc.template.delete.useMutation`. */
12836
+ var useTemplateDelete = trpc.template.delete.useMutation;
12817
12837
  /** Generated alias around `trpc.terminalSession.listProfiles.useQuery`. */
12818
12838
  var useTerminalSessionListProfiles = trpc.terminalSession.listProfiles.useQuery;
12819
12839
  /** Generated alias around `trpc.terminalSession.listInstances.useQuery`. */
@@ -14732,7 +14752,7 @@ Button.displayName = "Button";
14732
14752
  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";
14733
14753
  var CLAIM_POLL_MS = 2e3;
14734
14754
  var BARE_CODE = /^[A-Za-z0-9._~-]{8,}$/;
14735
- function isRecord$4(value) {
14755
+ function isRecord$5(value) {
14736
14756
  return typeof value === "object" && value !== null && !Array.isArray(value);
14737
14757
  }
14738
14758
  function isPresent(value) {
@@ -14761,7 +14781,7 @@ function choicesOf(value) {
14761
14781
  if (!Array.isArray(value)) return [];
14762
14782
  const out = [];
14763
14783
  for (const row of value) {
14764
- if (!isRecord$4(row) || typeof row["value"] !== "string" || typeof row["label"] !== "string") continue;
14784
+ if (!isRecord$5(row) || typeof row["value"] !== "string" || typeof row["label"] !== "string") continue;
14765
14785
  out.push({
14766
14786
  value: row["value"],
14767
14787
  label: row["label"],
@@ -14823,7 +14843,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
14823
14843
  const timer = setInterval(() => {
14824
14844
  if (inFlight.current) return;
14825
14845
  onAction(claimAction, field.key, { attemptId }).then((result) => {
14826
- if (stopped || !isRecord$4(result) || result["pending"] === true) return;
14846
+ if (stopped || !isRecord$5(result) || result["pending"] === true) return;
14827
14847
  stopped = true;
14828
14848
  applyResult(result);
14829
14849
  }).catch((err) => {
@@ -14849,7 +14869,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
14849
14869
  setError(null);
14850
14870
  try {
14851
14871
  const result = await onAction(field.beginAction, field.key, {});
14852
- if (!isRecord$4(result) || typeof result["authorizeUrl"] !== "string" || typeof result["attemptId"] !== "string") {
14872
+ if (!isRecord$5(result) || typeof result["authorizeUrl"] !== "string" || typeof result["attemptId"] !== "string") {
14853
14873
  setError(t("uiLibrary.accountSignIn.noLink", "The sign-in page could not be opened. Try again."));
14854
14874
  return;
14855
14875
  }
@@ -14877,7 +14897,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
14877
14897
  attemptId,
14878
14898
  callback
14879
14899
  });
14880
- if (!isRecord$4(result)) {
14900
+ if (!isRecord$5(result)) {
14881
14901
  setError(t("uiLibrary.accountSignIn.rejected", "The sign-in was not accepted. Start again."));
14882
14902
  return;
14883
14903
  }
@@ -20045,7 +20065,12 @@ var DEVICE_NAME_FORMS = [
20045
20065
  "nested",
20046
20066
  "with-location"
20047
20067
  ];
20048
- var PARENT_SEPARATORS = [" — ", " "];
20068
+ var PARENT_SEPARATORS = [
20069
+ " — ",
20070
+ " - ",
20071
+ ": ",
20072
+ " "
20073
+ ];
20049
20074
  /**
20050
20075
  * Readings that are never what a container is. Matches the comment on
20051
20076
  * `CONTAINER_CHILD_ROLE_PRIORITY`: battery, temperature, humidity, illuminance,
@@ -20069,9 +20094,10 @@ function sameDeviceName(left, right) {
20069
20094
  return foldDeviceName(left) === foldDeviceName(right);
20070
20095
  }
20071
20096
  /**
20072
- * Drop a parent prefix. `" — "` before a space, case-insensitive, so a provider
20073
- * that capitalised the parent differently still strips, and an em-dash child
20074
- * does not keep a dangling dash.
20097
+ * Drop a parent prefix. The separators `" — "`, `" - "` and `": "` are tried
20098
+ * before a plain space, case-insensitive, so a provider that capitalised the
20099
+ * parent differently still strips, and a dash or colon child does not keep a
20100
+ * dangling mark.
20075
20101
  */
20076
20102
  function stripDeviceParentPrefix(name, parentName) {
20077
20103
  if (parentName.length === 0) return name;
@@ -21396,11 +21422,11 @@ function useDeviceSnapshotImage(deviceId, options = {}) {
21396
21422
  //#region src/composites/cap-settings/camera-grid-layout.ts
21397
21423
  /** Smallest rectangle the editor will produce — ffmpeg refuses a zero-area crop. */
21398
21424
  var MIN_EXTENT = .02;
21399
- function isRecord$3(value) {
21425
+ function isRecord$4(value) {
21400
21426
  return value !== null && typeof value === "object" && !Array.isArray(value);
21401
21427
  }
21402
21428
  function readRect(value) {
21403
- if (!isRecord$3(value)) return null;
21429
+ if (!isRecord$4(value)) return null;
21404
21430
  const { x, y, width, height } = value;
21405
21431
  if (typeof x !== "number" || typeof y !== "number" || typeof width !== "number" || typeof height !== "number") return null;
21406
21432
  if (x < 0 || y < 0 || width <= 0 || height <= 0) return null;
@@ -21413,7 +21439,7 @@ function readRect(value) {
21413
21439
  };
21414
21440
  }
21415
21441
  function readCell(value) {
21416
- if (!isRecord$3(value)) return null;
21442
+ if (!isRecord$4(value)) return null;
21417
21443
  const deviceId = value["deviceId"];
21418
21444
  if (typeof deviceId !== "number" || !Number.isInteger(deviceId)) return null;
21419
21445
  const source = readRect(value["source"]);
@@ -21426,7 +21452,7 @@ function readCell(value) {
21426
21452
  };
21427
21453
  }
21428
21454
  function readProfileOffer(value) {
21429
- if (!isRecord$3(value)) return null;
21455
+ if (!isRecord$4(value)) return null;
21430
21456
  const profile = value["profile"];
21431
21457
  const offered = value["offered"];
21432
21458
  const missing = value["missingSources"];
@@ -21455,7 +21481,7 @@ function readProfileOffer(value) {
21455
21481
  */
21456
21482
  function parseGridView(raw) {
21457
21483
  if (raw === null) return null;
21458
- if (!isRecord$3(raw)) return "unreadable";
21484
+ if (!isRecord$4(raw)) return "unreadable";
21459
21485
  const { instanceId, deviceId, name, fps, cells, profiles } = raw;
21460
21486
  if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
21461
21487
  const parsed = [];
@@ -48319,6 +48345,76 @@ function formatNumeric(value, precision) {
48319
48345
  return Number.isInteger(value) ? String(value) : value.toFixed(1);
48320
48346
  }
48321
48347
  //#endregion
48348
+ //#region src/lib/unit-preferences.ts
48349
+ /**
48350
+ * The signed-in user's unit preferences (account preference, namespace
48351
+ * `admin.units`). Stored numbers stay in each quantity's base unit (D944); a
48352
+ * preference only picks the label and the conversion at display time.
48353
+ *
48354
+ * Two independent choices: the temperature scale, and metric or imperial for
48355
+ * every other quantity. The default is metric and °C, so a user who never
48356
+ * opens the page sees what they always saw.
48357
+ */
48358
+ var UNIT_PREFERENCES_NAMESPACE = "admin.units";
48359
+ var TEMPERATURE_SCALES = ["celsius", "fahrenheit"];
48360
+ var DEFAULT_UNIT_PREFERENCES = {
48361
+ system: _camstack_types.DEFAULT_DISPLAY_PREFERENCE,
48362
+ temperature: "celsius"
48363
+ };
48364
+ function isSystem(value) {
48365
+ return _camstack_types.DISPLAY_PREFERENCES.some((candidate) => candidate === value);
48366
+ }
48367
+ function isScale(value) {
48368
+ return TEMPERATURE_SCALES.some((candidate) => candidate === value);
48369
+ }
48370
+ function isRecord$3(value) {
48371
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48372
+ }
48373
+ /** A missing row or field is the default; a field of the wrong shape is unreadable. */
48374
+ function readUnitPreferences(raw) {
48375
+ if (raw === void 0 || raw === null) return {
48376
+ value: DEFAULT_UNIT_PREFERENCES,
48377
+ parsed: true
48378
+ };
48379
+ if (!isRecord$3(raw)) return {
48380
+ value: DEFAULT_UNIT_PREFERENCES,
48381
+ parsed: false
48382
+ };
48383
+ const system = raw["system"];
48384
+ const temperature = raw["temperature"];
48385
+ const systemOk = system === void 0 || isSystem(system);
48386
+ const scaleOk = temperature === void 0 || isScale(temperature);
48387
+ return {
48388
+ value: {
48389
+ system: isSystem(system) ? system : DEFAULT_UNIT_PREFERENCES.system,
48390
+ temperature: isScale(temperature) ? temperature : DEFAULT_UNIT_PREFERENCES.temperature
48391
+ },
48392
+ parsed: systemOk && scaleOk
48393
+ };
48394
+ }
48395
+ /** The display preference to hand `formatReading` for one quantity. */
48396
+ function preferenceForQuantity(preferences, quantity) {
48397
+ if (quantity === "temperature") return preferences.temperature === "fahrenheit" ? "imperial" : "metric";
48398
+ return preferences.system;
48399
+ }
48400
+ //#endregion
48401
+ //#region src/contexts/unit-preferences.tsx
48402
+ /**
48403
+ * The signed-in user's unit preferences, published by context so every
48404
+ * reading renders in the same units without a query of its own. Without a
48405
+ * provider (a spec, a standalone addon page) the default applies: metric, °C.
48406
+ */
48407
+ var UnitPreferencesContext = (0, react$1.createContext)(DEFAULT_UNIT_PREFERENCES);
48408
+ function UnitPreferencesProvider({ value, children }) {
48409
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnitPreferencesContext.Provider, {
48410
+ value,
48411
+ children
48412
+ });
48413
+ }
48414
+ function useUnitPreferences() {
48415
+ return (0, react$1.useContext)(UnitPreferencesContext);
48416
+ }
48417
+ //#endregion
48322
48418
  //#region src/lib/resolve-sensor-display.ts
48323
48419
  function canonUnit(raw) {
48324
48420
  if (raw === void 0) return void 0;
@@ -48337,7 +48433,7 @@ function resolveSensorDisplay(input) {
48337
48433
  const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
48338
48434
  const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.declarationPrecision;
48339
48435
  const quantity = concreteQuantity(input);
48340
- const preference = input.preference ?? _camstack_types.DEFAULT_DISPLAY_PREFERENCE;
48436
+ const preference = input.preference ?? (input.unitPreferences === void 0 ? _camstack_types.DEFAULT_DISPLAY_PREFERENCE : preferenceForQuantity(input.unitPreferences, quantity));
48341
48437
  const base = quantity === void 0 ? void 0 : (0, _camstack_types.baseUnitOf)(quantity);
48342
48438
  const preferred = quantity === void 0 ? void 0 : (0, _camstack_types.displayUnitFor)(quantity, preference);
48343
48439
  const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
@@ -48552,13 +48648,15 @@ function TextControl({ value, busy, onCommit }) {
48552
48648
  }
48553
48649
  function ControlWidget({ slice, busy, setValueFn, layout, quantity, display, dateTimeShape }) {
48554
48650
  const t = useUiText();
48651
+ const unitPreferences = useUnitPreferences();
48555
48652
  switch (slice.kind) {
48556
48653
  case "numeric": {
48557
48654
  const numericValue = typeof slice.value === "number" ? slice.value : Number(slice.value);
48558
48655
  const resolved = quantity === void 0 ? void 0 : resolveSensorDisplay({
48559
48656
  quantity,
48560
48657
  display,
48561
- capName: "control"
48658
+ capName: "control",
48659
+ unitPreferences
48562
48660
  });
48563
48661
  const toShown = resolved?.toDisplayValue ?? ((raw) => raw);
48564
48662
  const toBase = resolved?.fromDisplayValue ?? ((shown) => shown);
@@ -49012,6 +49110,7 @@ function renderPresenceLabel$1(state, t) {
49012
49110
  }
49013
49111
  function SensorValueAtom({ trpc, device }) {
49014
49112
  const t = useUiText();
49113
+ const unitPreferences = useUnitPreferences();
49015
49114
  const roleDescriptor = device.role != null ? ROLE_DESCRIPTOR[device.role] : void 0;
49016
49115
  const isBattery = isBatterySensorRole(device.role);
49017
49116
  const { data: roleDefaultsData } = useDeviceManagerGetRoleDisplayDefaults({}, { staleTime: 6e4 });
@@ -49040,7 +49139,8 @@ function SensorValueAtom({ trpc, device }) {
49040
49139
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
49041
49140
  roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : void 0,
49042
49141
  quantity: descriptor.quantity ?? "device",
49043
- deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
49142
+ deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity,
49143
+ unitPreferences
49044
49144
  }).format(rawValue);
49045
49145
  const unit = rendered.unit;
49046
49146
  const formatted = rendered.text;
@@ -49764,6 +49864,7 @@ function BatteryBar({ percent }) {
49764
49864
  }
49765
49865
  function SensorHeroInner({ trpc, device }) {
49766
49866
  const t = useUiText();
49867
+ const unitPreferences = useUnitPreferences();
49767
49868
  const roleDescriptor = device.role != null ? ROLE_DESCRIPTOR[device.role] : ROLE_DESCRIPTOR["temperature-sensor"];
49768
49869
  const isBattery = isBatterySensorRole(device.role);
49769
49870
  const { data: roleDefaultsData } = useDeviceManagerGetRoleDisplayDefaults({}, { staleTime: 6e4 });
@@ -49817,7 +49918,8 @@ function SensorHeroInner({ trpc, device }) {
49817
49918
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
49818
49919
  roleDefault: device.role != null ? roleDefaultsData?.defaults?.[device.role] : roleDefaultsData?.defaults?.["temperature-sensor"],
49819
49920
  quantity: descriptor.quantity ?? "device",
49820
- deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
49921
+ deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity,
49922
+ unitPreferences
49821
49923
  }).format(rawValue);
49822
49924
  const unit = rendered.unit;
49823
49925
  const formatted = rendered.text;
@@ -52338,13 +52440,14 @@ function useWeather(trpc, deviceId) {
52338
52440
  slice: (d) => d.state.weather
52339
52441
  });
52340
52442
  }
52341
- /** Format a base-unit weather reading (metric). */
52342
- function formatWeather(value, quantity) {
52443
+ /** Format a base-unit weather reading in the user's units. */
52444
+ function formatWeather(value, quantity, unitPreferences) {
52343
52445
  if (value === null) return "—";
52344
52446
  const rendered = (0, _camstack_types.formatReading)({
52345
52447
  value,
52346
52448
  quantity,
52347
- precision: 0
52449
+ precision: 0,
52450
+ preference: preferenceForQuantity(unitPreferences, quantity)
52348
52451
  });
52349
52452
  return rendered.unit.length === 0 ? rendered.text : `${rendered.text}${rendered.unit}`;
52350
52453
  }
@@ -52355,6 +52458,7 @@ function formatWeather(value, quantity) {
52355
52458
  function WeatherInlineControl({ trpc, deviceId }) {
52356
52459
  const t = useUiText();
52357
52460
  const { slice } = useWeather(trpc, deviceId);
52461
+ const unitPreferences = useUnitPreferences();
52358
52462
  if (slice === void 0) return null;
52359
52463
  const meta = weatherConditionMeta(slice.condition, t);
52360
52464
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -52366,7 +52470,7 @@ function WeatherInlineControl({ trpc, deviceId }) {
52366
52470
  children: inlineGlyph(meta.glyph)
52367
52471
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
52368
52472
  className: "tabular-nums",
52369
- children: formatWeather(slice.temperature, "temperature")
52473
+ children: formatWeather(slice.temperature, "temperature", unitPreferences)
52370
52474
  })]
52371
52475
  });
52372
52476
  }
@@ -52673,13 +52777,15 @@ function Readout$1({ icon, label, value }) {
52673
52777
  function WeatherHeroCard({ trpc, deviceId }) {
52674
52778
  const t = useUiText();
52675
52779
  const { slice } = useWeather(trpc, deviceId);
52780
+ const unitPreferences = useUnitPreferences();
52676
52781
  if (slice === void 0) return null;
52677
52782
  const meta = weatherConditionMeta(slice.condition, t);
52678
52783
  const tint = weatherTint(slice.condition);
52679
52784
  const temperature = slice.temperature === null ? void 0 : (0, _camstack_types.formatReading)({
52680
52785
  value: slice.temperature,
52681
52786
  quantity: "temperature",
52682
- precision: 0
52787
+ precision: 0,
52788
+ preference: preferenceForQuantity(unitPreferences, "temperature")
52683
52789
  });
52684
52790
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
52685
52791
  className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-4",
@@ -52701,7 +52807,8 @@ function WeatherHeroCard({ trpc, deviceId }) {
52701
52807
  className: "text-base text-foreground-subtle",
52702
52808
  children: temperature !== void 0 ? temperature.unit : (0, _camstack_types.formatReading)({
52703
52809
  value: 0,
52704
- quantity: "temperature"
52810
+ quantity: "temperature",
52811
+ preference: preferenceForQuantity(unitPreferences, "temperature")
52705
52812
  }).unit
52706
52813
  }),
52707
52814
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -52723,12 +52830,12 @@ function WeatherHeroCard({ trpc, deviceId }) {
52723
52830
  style: { transform: `rotate(${slice.windBearing}deg)` }
52724
52831
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
52725
52832
  label: t("uiLibrary.weatherControl.wind", "Wind"),
52726
- value: formatWeather(slice.windSpeed, "speed")
52833
+ value: formatWeather(slice.windSpeed, "speed", unitPreferences)
52727
52834
  }),
52728
52835
  slice.pressure !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout$1, {
52729
52836
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5" }),
52730
52837
  label: t("uiLibrary.weatherControl.pressure", "Pressure"),
52731
- value: formatWeather(slice.pressure, "pressure")
52838
+ value: formatWeather(slice.pressure, "pressure", unitPreferences)
52732
52839
  })
52733
52840
  ]
52734
52841
  })]
@@ -52968,6 +53075,12 @@ var VOLUME_MAX = 9;
52968
53075
  /** Amber kibble accent for the bowl fill; destructive red when low. */
52969
53076
  var KIBBLE = "#f59e0b";
52970
53077
  var KIBBLE_LOW = "#ef4444";
53078
+ /** Hold-to-repeat timing: first repeat after HOLD_DELAY_MS, then every
53079
+ * HOLD_INTERVAL_MS, and after HOLD_ACCEL_AFTER repeats the step grows to 5 g. */
53080
+ var HOLD_DELAY_MS = 350;
53081
+ var HOLD_INTERVAL_MS = 70;
53082
+ var HOLD_ACCEL_AFTER = 8;
53083
+ var HOLD_FAST_STEP = 5;
52971
53084
  function clampPortion(grams) {
52972
53085
  return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
52973
53086
  }
@@ -52987,9 +53100,9 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
52987
53100
  slice: (d) => d.state.petFeeder
52988
53101
  });
52989
53102
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
52990
- const [portion, setPortion] = (0, react$1.useState)(DEFAULT_PORTION);
52991
- const [hopper1, setHopper1] = (0, react$1.useState)(DEFAULT_PORTION);
52992
- const [hopper2, setHopper2] = (0, react$1.useState)(DEFAULT_PORTION);
53103
+ const [portion, setPortion] = usePortion(display?.lastFeedGrams ?? null);
53104
+ const [hopper1, setHopper1] = usePortion(display?.lastFeedHopper1 ?? null);
53105
+ const [hopper2, setHopper2] = usePortion(display?.lastFeedHopper2 ?? null);
52993
53106
  if (cap === void 0) return null;
52994
53107
  if (display === void 0) return null;
52995
53108
  const busy = !available;
@@ -53246,7 +53359,8 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53246
53359
  label: t("uiLibrary.petFeeder.decreaseVolume", "Decrease volume"),
53247
53360
  symbol: "−",
53248
53361
  disabled: busy || display.volume <= 0,
53249
- onClick: () => {
53362
+ repeat: false,
53363
+ onStep: () => {
53250
53364
  const next = Math.max(0, display.volume - 1);
53251
53365
  applyOptimistic({ volume: next });
53252
53366
  cap.setVolume({ level: next });
@@ -53260,7 +53374,8 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53260
53374
  label: t("uiLibrary.petFeeder.increaseVolume", "Increase volume"),
53261
53375
  symbol: "+",
53262
53376
  disabled: busy || display.volume >= VOLUME_MAX,
53263
- onClick: () => {
53377
+ repeat: false,
53378
+ onStep: () => {
53264
53379
  const next = Math.min(VOLUME_MAX, display.volume + 1);
53265
53380
  applyOptimistic({ volume: next });
53266
53381
  cap.setVolume({ level: next });
@@ -53272,6 +53387,22 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
53272
53387
  ]
53273
53388
  });
53274
53389
  }
53390
+ /**
53391
+ * Local manual-feed portion. `null` = untouched: the value follows the device's
53392
+ * remembered last feed (or the default) and picks it up when the slice arrives;
53393
+ * once the operator steps it, the local value wins and stays put.
53394
+ */
53395
+ function usePortion(remembered) {
53396
+ const [local, setLocal] = (0, react$1.useState)(null);
53397
+ const value = local ?? remembered ?? DEFAULT_PORTION;
53398
+ const valueRef = (0, react$1.useRef)(value);
53399
+ valueRef.current = value;
53400
+ return [value, (0, react$1.useCallback)((update) => {
53401
+ const next = clampPortion(update(valueRef.current));
53402
+ valueRef.current = next;
53403
+ setLocal(next);
53404
+ }, [])];
53405
+ }
53275
53406
  /** Labelled control group — a small uppercase header above its control. */
53276
53407
  function Field$1({ label, children }) {
53277
53408
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -53367,8 +53498,9 @@ function HopperFeed({ label, grams, busy, onGrams, onFeed }) {
53367
53498
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep$1, {
53368
53499
  label: t("uiLibrary.petFeeder.decreasePortion", "Decrease {{label}} portion", { label }),
53369
53500
  symbol: "−",
53370
- disabled: busy || grams <= _camstack_types.PET_FEEDER_MANUAL_FEED_MIN,
53371
- onClick: () => onGrams(clampPortion(grams - 1))
53501
+ large: true,
53502
+ disabled: grams <= _camstack_types.PET_FEEDER_MANUAL_FEED_MIN,
53503
+ onStep: (step) => onGrams((current) => current - step)
53372
53504
  }),
53373
53505
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
53374
53506
  className: "w-12 text-center text-sm font-semibold tabular-nums text-foreground",
@@ -53380,8 +53512,9 @@ function HopperFeed({ label, grams, busy, onGrams, onFeed }) {
53380
53512
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep$1, {
53381
53513
  label: t("uiLibrary.petFeeder.increasePortion", "Increase {{label}} portion", { label }),
53382
53514
  symbol: "+",
53383
- disabled: busy || grams >= _camstack_types.PET_FEEDER_MANUAL_FEED_MAX,
53384
- onClick: () => onGrams(clampPortion(grams + 1))
53515
+ large: true,
53516
+ disabled: grams >= _camstack_types.PET_FEEDER_MANUAL_FEED_MAX,
53517
+ onStep: (step) => onGrams((current) => current + step)
53385
53518
  })
53386
53519
  ]
53387
53520
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -53397,14 +53530,55 @@ function HopperFeed({ label, grams, busy, onGrams, onFeed }) {
53397
53530
  })]
53398
53531
  });
53399
53532
  }
53400
- /** Circular ± button. */
53401
- function RoundStep$1({ label, symbol, disabled, onClick }) {
53533
+ /**
53534
+ * Circular ± button. Pressing steps once at once; holding repeats (and speeds
53535
+ * up) so a large change is one gesture, not dozens of taps. `repeat={false}`
53536
+ * makes it a plain one-shot button (the volume steppers). A keyboard activation
53537
+ * (click without a pointer press, `detail === 0`) steps once.
53538
+ */
53539
+ function RoundStep$1({ label, symbol, disabled, onStep, repeat = true, large = false }) {
53540
+ const stepRef = (0, react$1.useRef)(onStep);
53541
+ stepRef.current = onStep;
53542
+ const delayRef = (0, react$1.useRef)(null);
53543
+ const loopRef = (0, react$1.useRef)(null);
53544
+ const stop = (0, react$1.useCallback)(() => {
53545
+ if (delayRef.current !== null) clearTimeout(delayRef.current);
53546
+ if (loopRef.current !== null) clearInterval(loopRef.current);
53547
+ delayRef.current = null;
53548
+ loopRef.current = null;
53549
+ }, []);
53550
+ (0, react$1.useEffect)(() => stop, [stop]);
53551
+ (0, react$1.useEffect)(() => {
53552
+ if (disabled) stop();
53553
+ }, [disabled, stop]);
53554
+ const start = () => {
53555
+ if (disabled) return;
53556
+ stepRef.current(1);
53557
+ if (!repeat) return;
53558
+ let repeats = 0;
53559
+ delayRef.current = setTimeout(() => {
53560
+ loopRef.current = setInterval(() => {
53561
+ repeats += 1;
53562
+ stepRef.current(repeats > HOLD_ACCEL_AFTER ? HOLD_FAST_STEP : 1);
53563
+ }, HOLD_INTERVAL_MS);
53564
+ }, HOLD_DELAY_MS);
53565
+ };
53402
53566
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
53403
53567
  type: "button",
53404
53568
  "aria-label": label,
53405
53569
  disabled,
53406
- onClick,
53407
- className: cn("flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-border bg-surface text-base leading-none text-foreground-subtle transition-colors hover:bg-surface-hover hover:text-foreground active:scale-95", disabled && "opacity-40 cursor-not-allowed"),
53570
+ onPointerDown: (e) => {
53571
+ if (e.button !== 0) return;
53572
+ start();
53573
+ },
53574
+ onPointerUp: stop,
53575
+ onPointerLeave: stop,
53576
+ onPointerCancel: stop,
53577
+ onContextMenu: (e) => e.preventDefault(),
53578
+ onClick: (e) => {
53579
+ if (e.detail === 0 && !disabled) stepRef.current(1);
53580
+ },
53581
+ className: cn("flex shrink-0 touch-manipulation select-none items-center justify-center rounded-full border border-border bg-surface leading-none text-foreground-subtle transition-colors hover:bg-surface-hover hover:text-foreground active:bg-surface-hover", large ? "h-9 w-9 text-xl" : "h-7 w-7 text-base", disabled && "opacity-40 cursor-not-allowed"),
53408
53582
  children: symbol
53409
53583
  });
53410
53584
  }
@@ -57216,17 +57390,13 @@ function deviceLeadIcon(device, roleDefaults) {
57216
57390
  *
57217
57391
  * The `" — "` separator is tried BEFORE the plain space so an Ecowitt child
57218
57392
  * (`"Ecowitt Gateway — gateway"`) yields `"gateway"` rather than `"— gateway"`.
57219
- * A missing parent name, or a name that doesn't carry the prefix, is returned
57220
- * verbatim.
57393
+ * Matching is case-insensitive and also accepts `" - "` and `": "`. A missing
57394
+ * parent name, a name that doesn't carry the prefix, a name equal to the
57395
+ * parent's, or one with nothing after the prefix is returned verbatim.
57221
57396
  */
57222
- var CHILD_NAME_SEPARATORS = [" — ", " "];
57223
57397
  function stripParentNamePrefix(name, parentName) {
57224
- if (parentName.length === 0) return name;
57225
- for (const sep of CHILD_NAME_SEPARATORS) {
57226
- const prefix = `${parentName}${sep}`;
57227
- if (name.startsWith(prefix)) return name.slice(prefix.length);
57228
- }
57229
- return name;
57398
+ const rest = stripDeviceParentPrefix(name, parentName.trim());
57399
+ return rest.length > 0 ? rest : name;
57230
57400
  }
57231
57401
  /**
57232
57402
  * Display name for a child device rendered UNDER its container/parent row.
@@ -59047,7 +59217,7 @@ function DeviceItem(props) {
59047
59217
  const flags = resolveFlags(props);
59048
59218
  const variant = props.variant ?? "compact";
59049
59219
  const view = props.view ?? "card";
59050
- const nameForm = props.isAccessoryRow ? "nested" : "compact";
59220
+ const nameForm = props.nameForm ?? (props.isAccessoryRow ? "nested" : "compact");
59051
59221
  const nameParent = nameForm === "nested" && parent ? {
59052
59222
  id: parent.id,
59053
59223
  name: parent.name,
@@ -70368,12 +70538,12 @@ function useHistoryReads(items, window, compare, enabled = true) {
70368
70538
  * re-applies the same change to the row as it now stands, once. A row this
70369
70539
  * client cannot read is never overwritten.
70370
70540
  */
70371
- var QUERY_KEY = ["preferences", HISTORY_PANELS_NAMESPACE];
70541
+ var QUERY_KEY$1 = ["preferences", HISTORY_PANELS_NAMESPACE];
70372
70542
  function useHistoryPanels() {
70373
70543
  const client = useSystem().trpcClient;
70374
70544
  const qc = (0, _tanstack_react_query.useQueryClient)();
70375
70545
  const q = (0, _tanstack_react_query.useQuery)({
70376
- queryKey: QUERY_KEY,
70546
+ queryKey: QUERY_KEY$1,
70377
70547
  queryFn: async () => {
70378
70548
  const row = (await client.preferences.get.query({ namespaces: [HISTORY_PANELS_NAMESPACE] })).rows.find((x) => x.namespace === HISTORY_PANELS_NAMESPACE);
70379
70549
  return row === void 0 ? null : {
@@ -70396,7 +70566,7 @@ function useHistoryPanels() {
70396
70566
  baseRevision: base?.revision ?? 0
70397
70567
  });
70398
70568
  if (res.status === "saved") {
70399
- qc.setQueryData(QUERY_KEY, {
70569
+ qc.setQueryData(QUERY_KEY$1, {
70400
70570
  value: res.row.value,
70401
70571
  revision: res.row.revision
70402
70572
  });
@@ -70406,7 +70576,7 @@ function useHistoryPanels() {
70406
70576
  value: res.current.value,
70407
70577
  revision: res.current.revision
70408
70578
  };
70409
- qc.setQueryData(QUERY_KEY, fresh);
70579
+ qc.setQueryData(QUERY_KEY$1, fresh);
70410
70580
  if (!retry) throw new Error("history-panels: the row changed while saving");
70411
70581
  await write(fresh, false);
70412
70582
  };
@@ -70620,6 +70790,83 @@ function HistorySkeleton() {
70620
70790
  });
70621
70791
  }
70622
70792
  //#endregion
70793
+ //#region src/hooks/use-account-unit-preferences.ts
70794
+ /**
70795
+ * Unit preferences in the signed-in user's ACCOUNT preferences (D894): one
70796
+ * row, namespace `admin.units`, synced to every device signed in as that user.
70797
+ *
70798
+ * A write is a compare-and-set on the revision it was based on; a conflict
70799
+ * re-applies the same change to the row as it now stands, once. A row this
70800
+ * client cannot read is never overwritten.
70801
+ */
70802
+ var QUERY_KEY = ["preferences", UNIT_PREFERENCES_NAMESPACE];
70803
+ function useAccountUnitPreferences() {
70804
+ const client = useSystem().trpcClient;
70805
+ const qc = (0, _tanstack_react_query.useQueryClient)();
70806
+ const q = (0, _tanstack_react_query.useQuery)({
70807
+ queryKey: QUERY_KEY,
70808
+ queryFn: async () => {
70809
+ const row = (await client.preferences.get.query({ namespaces: [UNIT_PREFERENCES_NAMESPACE] })).rows.find((x) => x.namespace === UNIT_PREFERENCES_NAMESPACE);
70810
+ return row === void 0 ? null : {
70811
+ value: row.value,
70812
+ revision: row.revision
70813
+ };
70814
+ },
70815
+ staleTime: 3e4
70816
+ });
70817
+ const row = q.data ?? null;
70818
+ const parsed = readUnitPreferences(row?.value);
70819
+ const update = (0, react$1.useCallback)(async (change) => {
70820
+ const write = async (base, retry) => {
70821
+ const current = readUnitPreferences(base?.value);
70822
+ if (!current.parsed) throw new Error("unit-preferences: stored row is not readable");
70823
+ const next = {
70824
+ ...current.value,
70825
+ ...change
70826
+ };
70827
+ const res = await client.preferences.put.mutate({
70828
+ namespace: UNIT_PREFERENCES_NAMESPACE,
70829
+ value: { ...next },
70830
+ baseRevision: base?.revision ?? 0
70831
+ });
70832
+ if (res.status === "saved") {
70833
+ qc.setQueryData(QUERY_KEY, {
70834
+ value: res.row.value,
70835
+ revision: res.row.revision
70836
+ });
70837
+ return;
70838
+ }
70839
+ const fresh = res.current === null ? null : {
70840
+ value: res.current.value,
70841
+ revision: res.current.revision
70842
+ };
70843
+ qc.setQueryData(QUERY_KEY, fresh);
70844
+ if (!retry) throw new Error("unit-preferences: the row changed while saving");
70845
+ await write(fresh, false);
70846
+ };
70847
+ await write(row, true);
70848
+ }, [
70849
+ client,
70850
+ qc,
70851
+ row
70852
+ ]);
70853
+ return {
70854
+ status: q.status === "success" ? "ok" : q.status === "error" ? "error" : "pending",
70855
+ value: q.status === "success" ? parsed.value : DEFAULT_UNIT_PREFERENCES,
70856
+ writable: q.status === "success" && parsed.parsed,
70857
+ update
70858
+ };
70859
+ }
70860
+ //#endregion
70861
+ //#region src/contexts/unit-preferences-from-server.tsx
70862
+ function UnitPreferencesFromServer({ children }) {
70863
+ const { value } = useAccountUnitPreferences();
70864
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnitPreferencesProvider, {
70865
+ value,
70866
+ children
70867
+ });
70868
+ }
70869
+ //#endregion
70623
70870
  //#region src/contexts/zone-editing.tsx
70624
70871
  /**
70625
70872
  * Per-device zone-editing state shared between the player overlay
@@ -71503,6 +71750,7 @@ exports.DATA_RECORDS_OWNER_PARAM = DATA_RECORDS_OWNER_PARAM;
71503
71750
  exports.DATA_RECORD_FACET_GROUPS = DATA_RECORD_FACET_GROUPS;
71504
71751
  exports.DEFAULT_COLOR = DEFAULT_COLOR;
71505
71752
  exports.DEFAULT_PAGINATION_LABELS = DEFAULT_PAGINATION_LABELS;
71753
+ exports.DEFAULT_UNIT_PREFERENCES = DEFAULT_UNIT_PREFERENCES;
71506
71754
  exports.DEVICE_COLUMNS = DEVICE_COLUMNS;
71507
71755
  exports.DEVICE_EXPORT_PAGE_SIZE_KEY = DEVICE_EXPORT_PAGE_SIZE_KEY;
71508
71756
  exports.DEVICE_LIST_PAGE_SIZE_KEY = DEVICE_LIST_PAGE_SIZE_KEY;
@@ -71914,6 +72162,7 @@ exports.SwitchInlineControl = SwitchInlineControl;
71914
72162
  exports.SwitchPanel = SwitchPanel;
71915
72163
  exports.SystemProvider = SystemProvider;
71916
72164
  exports.TABLE_SCROLL_MAX_H = TABLE_SCROLL_MAX_H;
72165
+ exports.TEMPERATURE_SCALES = TEMPERATURE_SCALES;
71917
72166
  exports.TEXT_FIELD_LABEL = TEXT_FIELD_LABEL;
71918
72167
  exports.TEXT_HINT = TEXT_HINT;
71919
72168
  exports.TEXT_METRIC = TEXT_METRIC;
@@ -71949,10 +72198,13 @@ exports.TrendingUp = TrendingUp;
71949
72198
  exports.TriangleAlert = TriangleAlert;
71950
72199
  exports.Truck = Truck;
71951
72200
  exports.Type = Type;
72201
+ exports.UNIT_PREFERENCES_NAMESPACE = UNIT_PREFERENCES_NAMESPACE;
71952
72202
  exports.UiTextProvider = UiTextProvider;
71953
72203
  exports.UiTrans = UiTrans;
71954
72204
  exports.Undo2 = Undo2;
71955
72205
  exports.UnitPicker = UnitPicker;
72206
+ exports.UnitPreferencesFromServer = UnitPreferencesFromServer;
72207
+ exports.UnitPreferencesProvider = UnitPreferencesProvider;
71956
72208
  exports.Unlink2 = Unlink2;
71957
72209
  exports.Upload = Upload;
71958
72210
  exports.User = User;
@@ -72220,6 +72472,7 @@ exports.pickerSearchText = pickerSearchText;
72220
72472
  exports.pickerZoneFilterSuspended = pickerZoneFilterSuspended;
72221
72473
  exports.pinnedStreamChoices = pinnedStreamChoices;
72222
72474
  exports.placePopover = placePopover;
72475
+ exports.preferenceForQuantity = preferenceForQuantity;
72223
72476
  exports.previousWindow = previousWindow;
72224
72477
  Object.defineProperty(exports, "projectSubjectPath", {
72225
72478
  enumerable: true,
@@ -72232,6 +72485,7 @@ exports.readHistory = readHistory;
72232
72485
  exports.readHistoryPanels = readHistoryPanels;
72233
72486
  exports.readItemId = readItemId;
72234
72487
  exports.readStoredGroupBy = readStoredGroupBy;
72488
+ exports.readUnitPreferences = readUnitPreferences;
72235
72489
  exports.reduceSessionRun = reduceSessionRun;
72236
72490
  exports.registerAddonBundle = registerAddonBundle;
72237
72491
  exports.renderMarkup = renderMarkup;
@@ -72328,6 +72582,7 @@ exports.typeLabel = typeLabel;
72328
72582
  exports.unitAxes = unitAxes;
72329
72583
  exports.useAccessoriesGetStatus = useAccessoriesGetStatus;
72330
72584
  exports.useAccessoriesSetChildHidden = useAccessoriesSetChildHidden;
72585
+ exports.useAccountUnitPreferences = useAccountUnitPreferences;
72331
72586
  exports.useActionSourceListActions = useActionSourceListActions;
72332
72587
  exports.useActionSourceRunAction = useActionSourceRunAction;
72333
72588
  exports.useActionSourceStopAction = useActionSourceStopAction;
@@ -73391,6 +73646,11 @@ exports.useRecordingOnboardGetStatus = useRecordingOnboardGetStatus;
73391
73646
  exports.useRecordingOnboardSetSettings = useRecordingOnboardSetSettings;
73392
73647
  exports.useRecordingSignalGetStatus = useRecordingSignalGetStatus;
73393
73648
  exports.useRemoteComponent = useRemoteComponent;
73649
+ exports.useRemoteGetConfig = useRemoteGetConfig;
73650
+ exports.useRemoteListCommands = useRemoteListCommands;
73651
+ exports.useRemoteSendCommand = useRemoteSendCommand;
73652
+ exports.useRemoteSendMediaCommand = useRemoteSendMediaCommand;
73653
+ exports.useRemoteSetConfig = useRemoteSetConfig;
73394
73654
  exports.useRoleDisplayDefaults = useRoleDisplayDefaults;
73395
73655
  exports.useRunningStateGetStatus = useRunningStateGetStatus;
73396
73656
  exports.useSceneMonitorCaptureReference = useSceneMonitorCaptureReference;
@@ -73543,6 +73803,11 @@ exports.useSystemSetRetentionConfig = useSystemSetRetentionConfig;
73543
73803
  exports.useSystemSetSiteLocation = useSystemSetSiteLocation;
73544
73804
  exports.useTamperGetStatus = useTamperGetStatus;
73545
73805
  exports.useTemperatureSensorGetStatus = useTemperatureSensorGetStatus;
73806
+ exports.useTemplateCreate = useTemplateCreate;
73807
+ exports.useTemplateDelete = useTemplateDelete;
73808
+ exports.useTemplateGet = useTemplateGet;
73809
+ exports.useTemplateList = useTemplateList;
73810
+ exports.useTemplateUpdate = useTemplateUpdate;
73546
73811
  exports.useTerminalSessionAdoptLegacyMonitor = useTerminalSessionAdoptLegacyMonitor;
73547
73812
  exports.useTerminalSessionClose = useTerminalSessionClose;
73548
73813
  exports.useTerminalSessionCreateInstance = useTerminalSessionCreateInstance;
@@ -73574,6 +73839,7 @@ exports.useTrainingStartJob = useTrainingStartJob;
73574
73839
  exports.useTrainingStartJobBatch = useTrainingStartJobBatch;
73575
73840
  exports.useTurnProviderGetTurnServers = useTurnProviderGetTurnServers;
73576
73841
  exports.useUiText = useUiText;
73842
+ exports.useUnitPreferences = useUnitPreferences;
73577
73843
  exports.useUpdateGetStatus = useUpdateGetStatus;
73578
73844
  exports.useUpdateInstallUpdate = useUpdateInstallUpdate;
73579
73845
  exports.useUrlQueryParam = useUrlQueryParam;