@camstack/ui-library 1.2.343 → 1.2.345

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
@@ -5950,6 +5950,191 @@ function useDisplayIconResolver() {
5950
5950
  return (0, react$1.useMemo)(() => (name) => resolveDisplayIcon(name), [useIconGlyphsVersion()]);
5951
5951
  }
5952
5952
  //#endregion
5953
+ //#region src/hotkeys/combo.ts
5954
+ function parseCombo(combo) {
5955
+ const parts = combo.split("+");
5956
+ const key = parts[parts.length - 1] ?? "";
5957
+ const modifiers = parts.slice(0, -1);
5958
+ return {
5959
+ mod: modifiers.includes("mod"),
5960
+ shift: modifiers.includes("shift"),
5961
+ alt: modifiers.includes("alt"),
5962
+ key
5963
+ };
5964
+ }
5965
+ function detectPlatform() {
5966
+ if (typeof navigator === "undefined") return "other";
5967
+ return /mac|iphone|ipad/i.test(navigator.platform) ? "mac" : "other";
5968
+ }
5969
+ function matchesCombo(event, combo, platform) {
5970
+ const want = parseCombo(combo);
5971
+ const modDown = platform === "mac" ? event.metaKey : event.ctrlKey;
5972
+ const strayDown = platform === "mac" ? event.ctrlKey : event.metaKey;
5973
+ return event.key.toLowerCase() === want.key && modDown === want.mod && !strayDown && event.shiftKey === want.shift && event.altKey === want.alt;
5974
+ }
5975
+ //#endregion
5976
+ //#region src/hotkeys/hotkey-provider.tsx
5977
+ var PAGE_SCOPE = "page";
5978
+ /** Asked last, after the page: a global handler that yields to whatever the page registers. */
5979
+ var FALLBACK_SCOPE = "fallback";
5980
+ var RegistryContext = (0, react$1.createContext)(null);
5981
+ var ScopeContext = (0, react$1.createContext)(PAGE_SCOPE);
5982
+ function isTypingTarget(target) {
5983
+ if (!(target instanceof HTMLElement)) return false;
5984
+ if (target.isContentEditable) return true;
5985
+ return target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
5986
+ }
5987
+ /**
5988
+ * Scopes stack in mount order; the newest enabled one is asked first. A `modal`
5989
+ * scope (an open dialog) ends the walk: what sits under it is not reachable.
5990
+ * Inside a scope the newest binding wins; a handler that returns `false` hands
5991
+ * over to the next one.
5992
+ */
5993
+ function dispatch(state, event, platform) {
5994
+ if (event.isComposing || event.keyCode === 229) return;
5995
+ const typing = isTypingTarget(event.target);
5996
+ const order = [
5997
+ ...[...state.scopes].reverse().map((s) => s.id),
5998
+ PAGE_SCOPE,
5999
+ FALLBACK_SCOPE
6000
+ ];
6001
+ for (const scopeId of order) {
6002
+ const candidates = state.bindings.filter((b) => b.scope === scopeId && matchesCombo(event, b.combo, platform)).reverse();
6003
+ for (const binding of candidates) {
6004
+ if (typing && !binding.allowInInput) continue;
6005
+ if (binding.handlerRef.current(event) === false) continue;
6006
+ if (binding.preventDefault) event.preventDefault();
6007
+ return;
6008
+ }
6009
+ if (state.scopes.find((s) => s.id === scopeId)?.modal) return;
6010
+ }
6011
+ }
6012
+ function HotkeyProvider({ children, platform }) {
6013
+ const stateRef = (0, react$1.useRef)({
6014
+ bindings: [],
6015
+ scopes: []
6016
+ });
6017
+ const effectivePlatform = platform ?? detectPlatform();
6018
+ const registry = (0, react$1.useMemo)(() => ({
6019
+ addBinding: (binding) => {
6020
+ stateRef.current = {
6021
+ ...stateRef.current,
6022
+ bindings: [...stateRef.current.bindings, binding]
6023
+ };
6024
+ return () => {
6025
+ stateRef.current = {
6026
+ ...stateRef.current,
6027
+ bindings: stateRef.current.bindings.filter((b) => b.id !== binding.id)
6028
+ };
6029
+ };
6030
+ },
6031
+ addScope: (scope) => {
6032
+ stateRef.current = {
6033
+ ...stateRef.current,
6034
+ scopes: [...stateRef.current.scopes, scope]
6035
+ };
6036
+ return () => {
6037
+ stateRef.current = {
6038
+ ...stateRef.current,
6039
+ scopes: stateRef.current.scopes.filter((s) => s !== scope)
6040
+ };
6041
+ };
6042
+ }
6043
+ }), []);
6044
+ (0, react$1.useEffect)(() => {
6045
+ const onKeyDown = (event) => dispatch(stateRef.current, event, effectivePlatform);
6046
+ window.addEventListener("keydown", onKeyDown, true);
6047
+ return () => window.removeEventListener("keydown", onKeyDown, true);
6048
+ }, [effectivePlatform]);
6049
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RegistryContext.Provider, {
6050
+ value: registry,
6051
+ children
6052
+ });
6053
+ }
6054
+ var nextBindingId = 0;
6055
+ /**
6056
+ * Bind a combo for the life of the component. Without a `HotkeyProvider` above
6057
+ * it does nothing, so the browser keeps its own shortcut.
6058
+ */
6059
+ function useHotkey(combo, handler, options = {}) {
6060
+ const registry = (0, react$1.useContext)(RegistryContext);
6061
+ const inheritedScope = (0, react$1.useContext)(ScopeContext);
6062
+ const handlerRef = (0, react$1.useRef)(handler);
6063
+ handlerRef.current = handler;
6064
+ const scope = options.scope ?? inheritedScope;
6065
+ const enabled = options.enabled ?? true;
6066
+ const preventDefault = options.preventDefault ?? true;
6067
+ const allowInInput = options.allowInInput ?? parseCombo(combo).mod;
6068
+ (0, react$1.useEffect)(() => {
6069
+ if (!registry || !enabled) return void 0;
6070
+ nextBindingId += 1;
6071
+ return registry.addBinding({
6072
+ id: nextBindingId,
6073
+ combo,
6074
+ scope,
6075
+ handlerRef,
6076
+ preventDefault,
6077
+ allowInInput
6078
+ });
6079
+ }, [
6080
+ registry,
6081
+ enabled,
6082
+ combo,
6083
+ scope,
6084
+ preventDefault,
6085
+ allowInInput
6086
+ ]);
6087
+ }
6088
+ /** Bindings made below this component belong to `id`; while enabled it is the topmost scope. */
6089
+ function HotkeyScope({ id, children, enabled = true, modal = false }) {
6090
+ const registry = (0, react$1.useContext)(RegistryContext);
6091
+ (0, react$1.useEffect)(() => {
6092
+ if (!registry || !enabled) return void 0;
6093
+ return registry.addScope({
6094
+ id,
6095
+ modal
6096
+ });
6097
+ }, [
6098
+ registry,
6099
+ enabled,
6100
+ id,
6101
+ modal
6102
+ ]);
6103
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScopeContext.Provider, {
6104
+ value: id,
6105
+ children
6106
+ });
6107
+ }
6108
+ //#endregion
6109
+ //#region src/hotkeys/use-search-field-hotkey.ts
6110
+ function isReachable(el) {
6111
+ return el.isConnected && !el.disabled && el.getClientRects().length > 0;
6112
+ }
6113
+ /**
6114
+ * Register a search field: ⌘F / Ctrl+F and ⌘K / Ctrl+K focus and select it.
6115
+ * A field that is hidden or disabled declines, so the browser's find still runs.
6116
+ * The library's search inputs call this themselves.
6117
+ */
6118
+ function useSearchFieldHotkey(ref, options = {}) {
6119
+ const enabled = options.enabled ?? true;
6120
+ const scope = options.scope;
6121
+ const focusField = () => {
6122
+ const el = ref.current;
6123
+ if (!el || !isReachable(el)) return false;
6124
+ el.focus();
6125
+ el.select();
6126
+ return true;
6127
+ };
6128
+ useHotkey("mod+f", focusField, {
6129
+ enabled,
6130
+ scope
6131
+ });
6132
+ useHotkey("mod+k", focusField, {
6133
+ enabled,
6134
+ scope
6135
+ });
6136
+ }
6137
+ //#endregion
5953
6138
  //#region ../../node_modules/clsx/dist/clsx.mjs
5954
6139
  function r(e) {
5955
6140
  var t, f, n = "";
@@ -9689,6 +9874,8 @@ function windowRows(rows, offsets, total, scrollTop) {
9689
9874
  };
9690
9875
  }
9691
9876
  function IconPicker({ value, onPick, fallbackGlyph, labels, extraWords, clearGlyph, labelFor, categoryLabel = _camstack_types.iconCategoryLabel, disabled = false }) {
9877
+ const searchFieldRef = (0, react$1.useRef)(null);
9878
+ useSearchFieldHotkey(searchFieldRef);
9692
9879
  const [open, setOpen] = (0, react$1.useState)(false);
9693
9880
  const [query, setQuery] = (0, react$1.useState)("");
9694
9881
  const [scrollTop, setScrollTop] = (0, react$1.useState)(0);
@@ -9749,6 +9936,7 @@ function IconPicker({ value, onPick, fallbackGlyph, labels, extraWords, clearGly
9749
9936
  className: "w-full",
9750
9937
  children: [
9751
9938
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
9939
+ ref: searchFieldRef,
9752
9940
  type: "search",
9753
9941
  value: query,
9754
9942
  autoFocus: true,
@@ -10007,7 +10195,14 @@ var inputVariants = cva("h-7 w-full px-2.5 text-xs bg-surface border rounded-md
10007
10195
  } },
10008
10196
  defaultVariants: { state: "default" }
10009
10197
  });
10010
- var Input = (0, react$1.forwardRef)(({ className, state, leftSlot, rightSlot, ...props }, ref) => {
10198
+ var Input = (0, react$1.forwardRef)(({ className, state, leftSlot, rightSlot, searchHotkey, ...props }, ref) => {
10199
+ const innerRef = (0, react$1.useRef)(null);
10200
+ const setRef = (0, react$1.useCallback)((node) => {
10201
+ innerRef.current = node;
10202
+ if (typeof ref === "function") ref(node);
10203
+ else if (ref) ref.current = node;
10204
+ }, [ref]);
10205
+ useSearchFieldHotkey(innerRef, { enabled: searchHotkey ?? props.type === "search" });
10011
10206
  if (leftSlot || rightSlot) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10012
10207
  className: cn("relative flex items-center", className),
10013
10208
  children: [
@@ -10016,7 +10211,7 @@ var Input = (0, react$1.forwardRef)(({ className, state, leftSlot, rightSlot, ..
10016
10211
  children: leftSlot
10017
10212
  }),
10018
10213
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
10019
- ref,
10214
+ ref: setRef,
10020
10215
  className: cn(inputVariants({ state }), leftSlot && "pl-8", rightSlot && "pr-8", className),
10021
10216
  ...props
10022
10217
  }),
@@ -10027,7 +10222,7 @@ var Input = (0, react$1.forwardRef)(({ className, state, leftSlot, rightSlot, ..
10027
10222
  ]
10028
10223
  });
10029
10224
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
10030
- ref,
10225
+ ref: setRef,
10031
10226
  className: cn(inputVariants({ state }), className),
10032
10227
  ...props
10033
10228
  });
@@ -26510,6 +26705,8 @@ function TrackedToggles({ options, tracked, nameOf, onToggle }) {
26510
26705
  });
26511
26706
  }
26512
26707
  function AssociationSlotCard({ deviceId, row, onSaved, readOnly = false }) {
26708
+ const searchFieldRef = (0, react$1.useRef)(null);
26709
+ useSearchFieldHotkey(searchFieldRef);
26513
26710
  const t = useUiText();
26514
26711
  const descriptor = _camstack_types.EXTENSION_SLOTS[row.slot].descriptor;
26515
26712
  const saved = row.value.deviceIds;
@@ -26641,6 +26838,7 @@ function AssociationSlotCard({ deviceId, row, onSaved, readOnly = false }) {
26641
26838
  })]
26642
26839
  }),
26643
26840
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
26841
+ ref: searchFieldRef,
26644
26842
  type: "search",
26645
26843
  "aria-label": t("uiLibrary.associationSlotCard.searchDevices", "Search devices"),
26646
26844
  value: search,
@@ -29014,8 +29212,8 @@ var contentVariants = cva("relative fixed left-1/2 top-1/2 -translate-x-1/2 -tra
29014
29212
  * flex chain does not resolve without it on iOS Safari.
29015
29213
  */
29016
29214
  scroll: {
29017
- content: "overflow-y-auto",
29018
- body: "flex flex-col overflow-hidden"
29215
+ content: "overflow-y-auto overscroll-contain",
29216
+ body: "flex flex-col overflow-hidden overscroll-contain"
29019
29217
  },
29020
29218
  placement: {
29021
29219
  center: "",
@@ -29089,7 +29287,12 @@ var DialogContent = (0, react$1.forwardRef)(({ className, width, placement, scro
29089
29287
  "aria-label": t("uiLibrary.dialog.close", "Close"),
29090
29288
  className: "absolute right-4 top-4 z-10 shrink-0",
29091
29289
  onClick: () => setOpen(false)
29092
- }), children]
29290
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HotkeyScope, {
29291
+ id: contentId,
29292
+ modal: true,
29293
+ enabled: open,
29294
+ children
29295
+ })]
29093
29296
  });
29094
29297
  });
29095
29298
  DialogContent.displayName = "DialogContent";
@@ -29119,7 +29322,7 @@ function DialogHeader({ className, ...props }) {
29119
29322
  */
29120
29323
  function DialogBody({ className, ...props }) {
29121
29324
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
29122
- className: cn("flex-1 min-h-0 overflow-y-auto", className),
29325
+ className: cn("flex-1 min-h-0 overflow-y-auto overscroll-contain", className),
29123
29326
  ...props
29124
29327
  });
29125
29328
  }
@@ -36348,7 +36551,7 @@ function RoleNodePill({ roleLabel, roleId, nodeName, colorId, emphasized = false
36348
36551
  * ```tsx
36349
36552
  * <Breadcrumb items={[
36350
36553
  * { label: 'Integrations', onClick: () => navigate('/integrations') },
36351
- * { label: 'RTSP Cameras', onClick: () => navigate('/integrations/int_0001') },
36554
+ * { label: 'RTSP Cameras', onClick: () => navigate('/integrations/provider-rtsp?instance=int_0001') },
36352
36555
  * { label: 'Front Door' },
36353
36556
  * ]} />
36354
36557
  * ```
@@ -41956,6 +42159,8 @@ function MultiSelectDropdown({ label, options, selected, onChange }) {
41956
42159
  var CHIP_ON = "rounded-full border border-primary bg-primary/10 px-2 py-0.5 text-[10px] text-foreground";
41957
42160
  var CHIP_OFF = "rounded-full border border-border px-2 py-0.5 text-[10px] text-foreground-muted hover:bg-surface-hover";
41958
42161
  function DataTableFilterBar({ state, facets, labels, onSearch, onToggleFacet, onClear, facetLabel, groups = [], onToggleGroupFacet }) {
42162
+ const searchRef = (0, react$1.useRef)(null);
42163
+ useSearchFieldHotkey(searchRef);
41959
42164
  const active = isDataTableFilterActive(state);
41960
42165
  const showFacets = facets.length > 1;
41961
42166
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -41964,6 +42169,7 @@ function DataTableFilterBar({ state, facets, labels, onSearch, onToggleFacet, on
41964
42169
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
41965
42170
  className: "relative min-w-[12rem] flex-1",
41966
42171
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-foreground-subtle" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
42172
+ ref: searchRef,
41967
42173
  type: "search",
41968
42174
  value: state.search,
41969
42175
  onChange: (e) => onSearch(e.target.value),
@@ -44194,6 +44400,8 @@ var TEXT_DEBOUNCE_MS = 300;
44194
44400
  var TOP_LOAD_THRESHOLD_PX = 24;
44195
44401
  var BYTES_PER_MB = 1024 * 1024;
44196
44402
  function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: propsDeviceId, containerDeviceId: propsContainerDeviceId, integrationId: propsIntegrationId, requestId: propsRequestId, level: initialLevel, maxHeight = "max-h-96", showScope = false, showDeviceChip = false, showFilters = true, showSourceSelect, limit = 100, liveBuffer: externalBuffer, onClose, className }) {
44403
+ const searchFieldRef = (0, react$1.useRef)(null);
44404
+ useSearchFieldHotkey(searchFieldRef);
44197
44405
  const t = useUiText();
44198
44406
  const sourceSelectOn = showSourceSelect ?? showFilters;
44199
44407
  const [localAddonId, setLocalAddonId] = (0, react$1.useState)("");
@@ -44471,6 +44679,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
44471
44679
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
44472
44680
  className: "relative",
44473
44681
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
44682
+ ref: searchFieldRef,
44474
44683
  type: "text",
44475
44684
  value: searchText,
44476
44685
  onChange: (e) => setSearchText(e.target.value),
@@ -45122,6 +45331,8 @@ function hasContent(entry) {
45122
45331
  }
45123
45332
  var MAX_LIVE_ENTRIES = 300;
45124
45333
  function EventStream({ agentId, addonId, deviceId, defaultCategories, categories: legacyCategories, category: propsCategory, maxHeight = "max-h-96", limit = 50, showCategoryFilter = true, showFilters: _showFilters = false, liveBuffer: externalBuffer, onClose, className }) {
45334
+ const searchFieldRef = (0, react$1.useRef)(null);
45335
+ useSearchFieldHotkey(searchFieldRef);
45125
45336
  const t = useUiText();
45126
45337
  const defaultsArray = (0, react$1.useMemo)(() => defaultCategories ?? legacyCategories ?? DEFAULT_EVENT_CATEGORIES, [defaultCategories, legacyCategories]);
45127
45338
  const defaultsKey = (0, react$1.useMemo)(() => [...defaultsArray].toSorted().join(","), [defaultsArray]);
@@ -45398,6 +45609,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
45398
45609
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
45399
45610
  className: "relative",
45400
45611
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
45612
+ ref: searchFieldRef,
45401
45613
  type: "text",
45402
45614
  value: searchText,
45403
45615
  onChange: (e) => setSearchText(e.target.value),
@@ -45896,6 +46108,8 @@ function coldRuntimeStateSlice(capName) {
45896
46108
  * deeper opens on demand. */
45897
46109
  var DEFAULT_EXPAND_DEPTH = 1;
45898
46110
  function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onClose, className }) {
46111
+ const searchFieldRef = (0, react$1.useRef)(null);
46112
+ useSearchFieldHotkey(searchFieldRef);
45899
46113
  const t = useUiText();
45900
46114
  const [activeCaps, setActiveCaps] = (0, react$1.useState)((0, react$1.useMemo)(() => new Set(defaultCaps ?? []), [defaultCaps]));
45901
46115
  const [searchText, setSearchText] = (0, react$1.useState)("");
@@ -46123,6 +46337,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onCl
46123
46337
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
46124
46338
  className: "relative",
46125
46339
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
46340
+ ref: searchFieldRef,
46126
46341
  type: "text",
46127
46342
  value: searchText,
46128
46343
  onChange: (e) => setSearchText(e.target.value),
@@ -47512,6 +47727,42 @@ function resolveClimateTempStep(tempStep) {
47512
47727
  if (typeof tempStep === "number" && Number.isFinite(tempStep) && tempStep > 0) return tempStep;
47513
47728
  return 1;
47514
47729
  }
47730
+ var TARGET_FALLBACK_INT_DIGITS = 2;
47731
+ var TARGET_MAX_DECIMALS = 3;
47732
+ function decimalPlaces(n) {
47733
+ if (!Number.isFinite(n)) return 0;
47734
+ for (let d = 0; d < TARGET_MAX_DECIMALS; d += 1) if (Math.abs(n * 10 ** d - Math.round(n * 10 ** d)) < 1e-9) return d;
47735
+ return TARGET_MAX_DECIMALS;
47736
+ }
47737
+ /**
47738
+ * Character count of the longest set-temperature the device can show, derived
47739
+ * from its range and step (5-30 step 0.5 reserves "30.5" = 4). The big target
47740
+ * number reserves this width so the − / + buttons do not move when the value
47741
+ * gains decimals or loses a digit.
47742
+ */
47743
+ function climateTargetWidthChars(display) {
47744
+ const step = resolveClimateTempStep(display.tempStep);
47745
+ const bounds = [display.minTemp, display.maxTemp].filter((v) => typeof v === "number" && Number.isFinite(v));
47746
+ const intDigits = bounds.length === 0 ? TARGET_FALLBACK_INT_DIGITS : Math.max(...bounds.map((v) => String(Math.trunc(Math.abs(v))).length));
47747
+ const sign = bounds.some((v) => v < 0) ? 1 : 0;
47748
+ const decimals = Math.max(decimalPlaces(step), ...bounds.map(decimalPlaces));
47749
+ return sign + intDigits + (decimals > 0 ? 1 + decimals : 0);
47750
+ }
47751
+ /** Static class names so Tailwind generates them (no runtime-built classes). */
47752
+ var TARGET_MIN_WIDTH_CLASS = {
47753
+ 1: "min-w-[1ch]",
47754
+ 2: "min-w-[2ch]",
47755
+ 3: "min-w-[3ch]",
47756
+ 4: "min-w-[4ch]",
47757
+ 5: "min-w-[5ch]",
47758
+ 6: "min-w-[6ch]",
47759
+ 7: "min-w-[7ch]",
47760
+ 8: "min-w-[8ch]"
47761
+ };
47762
+ /** `min-w-[Nch]` + right alignment for the big target number. */
47763
+ function climateTargetWidthClass(display) {
47764
+ return `${TARGET_MIN_WIDTH_CLASS[Math.min(Math.max(climateTargetWidthChars(display), 1), 8)]} text-center`;
47765
+ }
47515
47766
  function nextClimateTarget(current, deltaSteps, display) {
47516
47767
  let next = current + deltaSteps * resolveClimateTempStep(display.tempStep);
47517
47768
  const min = display.minTemp;
@@ -53899,7 +54150,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
53899
54150
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53900
54151
  className: "flex items-baseline gap-0.5",
53901
54152
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53902
- className: "text-4xl font-bold leading-none tabular-nums",
54153
+ className: cn("text-4xl font-bold leading-none tabular-nums", climateTargetWidthClass(display)),
53903
54154
  style: { color: accent },
53904
54155
  children: display.target
53905
54156
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -53940,7 +54191,7 @@ function AcPanel({ trpc, deviceId, layout, optimistic }) {
53940
54191
  })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53941
54192
  className: "relative z-10 flex items-baseline gap-0.5",
53942
54193
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53943
- className: "text-4xl font-bold leading-none tabular-nums text-foreground",
54194
+ className: cn("text-4xl font-bold leading-none tabular-nums text-foreground", climateTargetWidthClass(display)),
53944
54195
  children: display.currentTemp ?? "—"
53945
54196
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
53946
54197
  className: "text-lg text-foreground-subtle",
@@ -54058,7 +54309,7 @@ function CompactSelect({ label, icon, value, options, disabled, onChange }) {
54058
54309
  className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
54059
54310
  children: label
54060
54311
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
54061
- className: cn("flex items-center gap-1 overflow-hidden rounded-lg border border-border/60 bg-surface-subtle px-1.5 py-1.5", disabled && "opacity-40"),
54312
+ className: cn("flex items-center gap-1 overflow-hidden rounded-lg border border-border/60 bg-surface-subtle px-1.5 py-1.5 has-[:focus-visible]:border-primary", disabled && "opacity-40"),
54062
54313
  children: [
54063
54314
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
54064
54315
  className: "flex shrink-0",
@@ -60142,6 +60393,8 @@ function GenericModeList(props) {
60142
60393
  defaultSize: fixedPageSize
60143
60394
  });
60144
60395
  const [searchInput, setSearchInput] = (0, react$1.useState)("");
60396
+ const searchRef = (0, react$1.useRef)(null);
60397
+ useSearchFieldHotkey(searchRef);
60145
60398
  const debouncedSearch = useDebouncedString(searchInput, 300);
60146
60399
  const [typeFilters, setTypeFilters] = (0, react$1.useState)(() => [...initialTypeFilters]);
60147
60400
  const [page, setPage] = (0, react$1.useState)(1);
@@ -60212,6 +60465,7 @@ function GenericModeList(props) {
60212
60465
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
60213
60466
  className: "relative",
60214
60467
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-foreground-subtle pointer-events-none" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
60468
+ ref: searchRef,
60215
60469
  type: "text",
60216
60470
  value: searchInput,
60217
60471
  onChange: (e) => setSearchInput(e.target.value),
@@ -61095,6 +61349,8 @@ function DeviceSelectorPicker({ value, onChange, includeLinked, onIncludeLinkedC
61095
61349
  const t = useUiText();
61096
61350
  const { data: rawDevices } = useDeviceManagerListAll({});
61097
61351
  const [search, setSearch] = (0, react$1.useState)("");
61352
+ const searchRef = (0, react$1.useRef)(null);
61353
+ useSearchFieldHotkey(searchRef);
61098
61354
  const devices = (0, react$1.useMemo)(() => {
61099
61355
  const all = (rawDevices ?? []).map((d) => ({
61100
61356
  id: d.id,
@@ -61225,6 +61481,7 @@ function DeviceSelectorPicker({ value, onChange, includeLinked, onIncludeLinkedC
61225
61481
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
61226
61482
  className: "relative",
61227
61483
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-foreground-subtle" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
61484
+ ref: searchRef,
61228
61485
  type: "search",
61229
61486
  value: search,
61230
61487
  onChange: (e) => setSearch(e.target.value),
@@ -62327,6 +62584,7 @@ function FilterBar({ filters, values, onChange, className }) {
62327
62584
  placeholder: searchFilter.placeholder ?? t("uiLibrary.filterBar.search", "Search..."),
62328
62585
  value: values[searchFilter.key] ?? "",
62329
62586
  onChange: (e) => handleChange(searchFilter.key, e.target.value),
62587
+ searchHotkey: true,
62330
62588
  leftSlot: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "h-3 w-3 text-foreground-subtle" }),
62331
62589
  className: "flex-1"
62332
62590
  }), hasNonSearchFilters && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -62388,6 +62646,7 @@ function FilterBar({ filters, values, onChange, className }) {
62388
62646
  placeholder: filter.placeholder ?? t("uiLibrary.filterBar.search", "Search..."),
62389
62647
  value: values[filter.key] ?? "",
62390
62648
  onChange: (e) => handleChange(filter.key, e.target.value),
62649
+ searchHotkey: true,
62391
62650
  leftSlot: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "h-3 w-3 text-foreground-subtle" }),
62392
62651
  className: "w-48"
62393
62652
  }, filter.key);
@@ -62921,7 +63180,7 @@ function mountAddonPage(PageComponent, options = {}) {
62921
63180
  console.error(`[mountAddonPage] Element #${rootId} not found`);
62922
63181
  return;
62923
63182
  }
62924
- (0, react_dom_client.createRoot)(root).render((0, react$1.createElement)(DevShell, {
63183
+ (0, react_dom_client.createRoot)(root).render((0, react$1.createElement)(HotkeyProvider, null, (0, react$1.createElement)(DevShell, {
62925
63184
  serverUrl,
62926
63185
  title,
62927
63186
  children: ({ trpc, theme }) => (0, react$1.createElement)(PageComponent, {
@@ -62931,7 +63190,7 @@ function mountAddonPage(PageComponent, options = {}) {
62931
63190
  console.log("[dev] navigate:", path);
62932
63191
  }
62933
63192
  })
62934
- }));
63193
+ })));
62935
63194
  }
62936
63195
  //#endregion
62937
63196
  //#region src/composites/node-picker.tsx
@@ -67243,6 +67502,8 @@ function zoneUnavailable(zones, t) {
67243
67502
  if (zones.state === "failed") return t("uiLibrary.deviceFilterBar.zonesUnavailable", "zones unavailable");
67244
67503
  }
67245
67504
  function DeviceFilterBar({ filters, onFiltersChange, typeOptions, zoneOptions, locationOptions, zones, grouping, searchPlaceholder, searchLabel, disabled = false }) {
67505
+ const searchFieldRef = (0, react$1.useRef)(null);
67506
+ useSearchFieldHotkey(searchFieldRef);
67246
67507
  const t = useUiText();
67247
67508
  const [text, setText] = (0, react$1.useState)(filters.query);
67248
67509
  const latest = (0, react$1.useRef)({
@@ -67272,6 +67533,7 @@ function DeviceFilterBar({ filters, onFiltersChange, typeOptions, zoneOptions, l
67272
67533
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
67273
67534
  className: INPUT_SHELL,
67274
67535
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Search, { className: "h-3.5 w-3.5 shrink-0 text-foreground-subtle" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
67536
+ ref: searchFieldRef,
67275
67537
  type: "text",
67276
67538
  value: text,
67277
67539
  placeholder: searchPlaceholder,
@@ -67397,6 +67659,64 @@ function withResolvedParentDetail(devices) {
67397
67659
  return changed ? next : devices;
67398
67660
  }
67399
67661
  //#endregion
67662
+ //#region src/composites/device-picker/device-provider-label.ts
67663
+ /**
67664
+ * Who offers a device, as the picker draws it: "{addon name} ({integration
67665
+ * name})" — the integration part only when the device comes from a named
67666
+ * integration instance — otherwise just "{addon name}".
67667
+ *
67668
+ * Derived from what the UI already reads: the device's providing `addonId` and
67669
+ * its `integrationId` (fleet listing), the addon's manifest name
67670
+ * (`addons.list`) and the instance's name (`integrations.list`). Nothing here
67671
+ * knows a kind or an addon; a row whose provider cannot be resolved shows no
67672
+ * line at all rather than a guess.
67673
+ */
67674
+ /** "Home Assistant (Casa)", or "Home Assistant" without an instance name. */
67675
+ function formatProviderLabel(addonName, integrationName) {
67676
+ const instance = integrationName?.trim() ?? "";
67677
+ return instance === "" || instance === addonName ? addonName : `${addonName} (${instance})`;
67678
+ }
67679
+ /** `deviceId → provider label`; a device whose addon is unknown is absent. */
67680
+ function providerLabelsOf(fleet, addons, integrations) {
67681
+ const addonName = /* @__PURE__ */ new Map();
67682
+ for (const a of addons) if (a.name !== void 0 && a.name !== "") addonName.set(a.id, a.name);
67683
+ const instanceName = new Map(integrations.map((i) => [i.id, i.name]));
67684
+ const out = /* @__PURE__ */ new Map();
67685
+ for (const d of fleet) {
67686
+ const addon = addonName.get(d.addonId);
67687
+ if (addon === void 0) continue;
67688
+ const instance = d.integrationId === void 0 ? null : instanceName.get(d.integrationId) ?? null;
67689
+ out.set(d.id, formatProviderLabel(addon, instance));
67690
+ }
67691
+ return out;
67692
+ }
67693
+ var NONE = /* @__PURE__ */ new Map();
67694
+ var PROVIDER_LABELS_STALE_MS = 6e4;
67695
+ /**
67696
+ * The labels for every device in the fleet; empty until the reads answer (a
67697
+ * failed read draws no provider line — it is a hint, never a gate).
67698
+ */
67699
+ function useDeviceProviderLabels(enabled) {
67700
+ const trpcClient = useSystem().trpcClient;
67701
+ const query = (0, _tanstack_react_query.useQuery)({
67702
+ queryKey: ["ui-library", "device-provider-labels"],
67703
+ enabled,
67704
+ staleTime: PROVIDER_LABELS_STALE_MS,
67705
+ queryFn: async () => {
67706
+ const [fleet, addons, integrations] = await Promise.all([
67707
+ trpcClient.deviceManager.listAll.query({ projection: "slim" }),
67708
+ trpcClient.addons.list.query(),
67709
+ trpcClient.integrations.list.query()
67710
+ ]);
67711
+ return providerLabelsOf(fleet, addons.map((a) => ({
67712
+ id: a.manifest.id,
67713
+ name: a.manifest.name
67714
+ })), integrations);
67715
+ }
67716
+ });
67717
+ return (0, react$1.useMemo)(() => query.data ?? NONE, [query.data]);
67718
+ }
67719
+ //#endregion
67400
67720
  //#region src/composites/device-picker/device-picker-grouping.ts
67401
67721
  /**
67402
67722
  * The admin side of the device picker's "Group by" (Zone | Location | None):
@@ -67761,7 +68081,7 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, s
67761
68081
  children: [
67762
68082
  listState === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesLoadingLine, { noun: plural }),
67763
68083
  listState === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DevicesFailedLine, { noun: noun === "camera" ? "devices" : plural }),
67764
- !(selection === "single" && chips.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
68084
+ !(selection === "single" && chips.length === 0 && showAll) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
67765
68085
  className: "flex flex-wrap items-center gap-1",
67766
68086
  children: [
67767
68087
  chips.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -67825,6 +68145,7 @@ function DevicePicker({ devices: devicesIn, listState, fleet, value, onChange, s
67825
68145
  ]
67826
68146
  });
67827
68147
  }
68148
+ var NO_PROVIDERS = /* @__PURE__ */ new Map();
67828
68149
  /**
67829
68150
  * The device picker's panel: mounted only while the list is open, so the zone
67830
68151
  * read below happens only then (for Zone grouping and the Zone filter).
@@ -67842,8 +68163,10 @@ function GroupedPickerPanel(props) {
67842
68163
  stored.setGroupBy(next);
67843
68164
  };
67844
68165
  const zones = usePickerSiteZones(true);
68166
+ const providers = useDeviceProviderLabels(true);
67845
68167
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PickerPanel, {
67846
68168
  ...props,
68169
+ providers,
67847
68170
  grouping: {
67848
68171
  groupBy,
67849
68172
  onGroupByChange,
@@ -67864,7 +68187,7 @@ var NO_ZONES = {
67864
68187
  zones: []
67865
68188
  };
67866
68189
  /** The searchable, grouped list — rendered one page at a time. */
67867
- function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, selection, grouping }) {
68190
+ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, disabled, locked, lockedNote, showAll, selection, grouping, providers = NO_PROVIDERS }) {
67868
68191
  const t = useUiText();
67869
68192
  const radioName = (0, react$1.useId)();
67870
68193
  const [filters, setFilters] = (0, react$1.useState)(PICKER_NO_FILTERS);
@@ -67967,7 +68290,8 @@ function PickerPanel({ devices, value, onChange, annotate, groupTitle, noun, dis
67967
68290
  locked,
67968
68291
  lockedNote,
67969
68292
  selection,
67970
- radioName
68293
+ radioName,
68294
+ providers
67971
68295
  }, row.key)), page.remaining > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
67972
68296
  type: "button",
67973
68297
  className: "mt-1 w-full rounded border border-border py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover",
@@ -67997,7 +68321,7 @@ function secondLine(device, groupBy) {
67997
68321
  ].filter((part) => part !== null);
67998
68322
  return parts.length > 0 ? parts.join(" · ") : void 0;
67999
68323
  }
68000
- function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote, selection, radioName }) {
68324
+ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTitle, groupBy, disabled, locked, lockedNote, selection, radioName, providers }) {
68001
68325
  const t = useUiText();
68002
68326
  const single = selection === "single";
68003
68327
  if (row.kind === "header") {
@@ -68049,6 +68373,8 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
68049
68373
  const isLocked = locked.has(row.device.id);
68050
68374
  const note = isLocked ? lockedNote : annotate?.(row.device.id);
68051
68375
  const second = secondLine(row.device, groupBy);
68376
+ const rowId = row.device.id;
68377
+ const provider = typeof rowId === "number" ? providers.get(rowId) : void 0;
68052
68378
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
68053
68379
  className: "flex cursor-pointer items-center gap-2.5 px-3 py-1 text-xs text-foreground hover:bg-surface-hover",
68054
68380
  "data-testid": "device-picker-row",
@@ -68072,6 +68398,11 @@ function PickerRowView({ row, value, onChange, onToggleGroup, annotate, groupTit
68072
68398
  className: "truncate text-[10px] text-foreground-subtle",
68073
68399
  children: second
68074
68400
  }),
68401
+ provider !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
68402
+ className: "truncate text-[10px] text-foreground-subtle",
68403
+ "data-testid": "device-picker-provider",
68404
+ children: provider
68405
+ }),
68075
68406
  note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
68076
68407
  className: isLocked ? "text-[10px] text-foreground-subtle" : "text-[10px] text-warning",
68077
68408
  children: note
@@ -71235,6 +71566,7 @@ exports.ExtensionsPanel = ExtensionsPanel;
71235
71566
  exports.ExternalLink = ExternalLink;
71236
71567
  exports.Eye = Eye;
71237
71568
  exports.EyeOff = EyeOff;
71569
+ exports.FALLBACK_SCOPE = FALLBACK_SCOPE;
71238
71570
  exports.FAST_ICON_GLYPHS = FAST_ICON_GLYPHS;
71239
71571
  exports.FILL = FILL;
71240
71572
  exports.FLEET_LIST_GC_TIME_MS = FLEET_LIST_GC_TIME_MS;
@@ -71286,6 +71618,8 @@ exports.History = History;
71286
71618
  exports.HistoryChart = HistoryChart;
71287
71619
  exports.HistoryWindowBar = HistoryWindowBar;
71288
71620
  exports.HlsVideo = HlsVideo;
71621
+ exports.HotkeyProvider = HotkeyProvider;
71622
+ exports.HotkeyScope = HotkeyScope;
71289
71623
  exports.House = House;
71290
71624
  exports.HoverZoomImage = HoverZoomImage;
71291
71625
  exports.HubSessionTestCard = HubSessionTestCard;
@@ -71717,6 +72051,7 @@ exports.defaultUiText = defaultUiText;
71717
72051
  exports.deriveDeviceKind = deriveDeviceKind;
71718
72052
  exports.deriveWebrtcDebugStats = deriveWebrtcDebugStats;
71719
72053
  exports.describeDeviceSelector = describeDeviceSelector;
72054
+ exports.detectPlatform = detectPlatform;
71720
72055
  exports.deviceIntegrationResolver = deviceIntegrationResolver;
71721
72056
  exports.deviceListStateOf = deviceListStateOf;
71722
72057
  exports.deviceMatchesFilter = deviceMatchesFilter;
@@ -71813,6 +72148,7 @@ exports.loadRemoteBundle = loadRemoteBundle;
71813
72148
  exports.lucideExportName = lucideExportName;
71814
72149
  exports.makeScrubBridge = makeScrubBridge;
71815
72150
  exports.mapRootDetectorPlacements = mapRootDetectorPlacements;
72151
+ exports.matchesCombo = matchesCombo;
71816
72152
  exports.mergeSecretForSubmit = mergeSecretForSubmit;
71817
72153
  exports.metadataEntries = metadataEntries;
71818
72154
  exports.metadataString = metadataString;
@@ -71844,6 +72180,7 @@ exports.pageSizeLabel = pageSizeLabel;
71844
72180
  exports.paginateRows = paginateRows;
71845
72181
  exports.parentDetailOf = parentDetailOf;
71846
72182
  exports.parseCapKey = parseCapKey;
72183
+ exports.parseCombo = parseCombo;
71847
72184
  exports.parseConsumableLevel = parseConsumableLevel;
71848
72185
  exports.parseIceCandidate = parseIceCandidate;
71849
72186
  exports.parsePickerGroupBy = parsePickerGroupBy;
@@ -72454,6 +72791,7 @@ exports.useHeatingValveGetStatus = useHeatingValveGetStatus;
72454
72791
  exports.useHistoryPanels = useHistoryPanels;
72455
72792
  exports.useHistoryReader = useHistoryReader;
72456
72793
  exports.useHistoryReads = useHistoryReads;
72794
+ exports.useHotkey = useHotkey;
72457
72795
  exports.useHumidifierGetStatus = useHumidifierGetStatus;
72458
72796
  exports.useHumidifierSetMode = useHumidifierSetMode;
72459
72797
  exports.useHumidifierSetOn = useHumidifierSetOn;
@@ -73048,6 +73386,7 @@ exports.useScriptRunnerGetStatus = useScriptRunnerGetStatus;
73048
73386
  exports.useScriptRunnerRun = useScriptRunnerRun;
73049
73387
  exports.useScriptRunnerStop = useScriptRunnerStop;
73050
73388
  exports.useScrubController = useScrubController;
73389
+ exports.useSearchFieldHotkey = useSearchFieldHotkey;
73051
73390
  exports.useServerManagementApplyServerUpdate = useServerManagementApplyServerUpdate;
73052
73391
  exports.useServerManagementCheckServerUpdate = useServerManagementCheckServerUpdate;
73053
73392
  exports.useServerManagementGetServerPackageStatus = useServerManagementGetServerPackageStatus;