@camstack/ui-library 1.2.16 → 1.2.17

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
@@ -3680,6 +3680,34 @@ var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
3680
3680
  var SPLIT_PANEL_OUTER = "flex flex-col-reverse md:flex-row";
3681
3681
  /** Sidebar narrow lane in a SPLIT_PANEL_OUTER layout. */
3682
3682
  var SPLIT_PANEL_SIDE = "w-full md:w-44 lg:w-52 md:flex-shrink-0 border-b md:border-b-0 md:border-l border-border";
3683
+ /**
3684
+ * The tier boundary at which a settings label/value row stops stacking.
3685
+ *
3686
+ * Below it (L1 — compact) the row is ONE column: label on its own line, value
3687
+ * on the next at full width. Above it the row is label-left / value-right as
3688
+ * before. The two-column form needs `SETTING_ROW_LABEL`'s lane plus a readable
3689
+ * value beside it; a phone has neither, and the failure mode is not "cramped"
3690
+ * but "the value is the part that gets cut" — the one thing the operator
3691
+ * opened the page to read.
3692
+ */
3693
+ var SETTING_ROW_STACK_BREAKPOINT = "sm";
3694
+ /** Outer row: single column at L1, label/value columns from `sm` up. */
3695
+ var SETTING_ROW = "flex min-w-0 flex-col gap-0.5 py-1.5 sm:flex-row sm:items-center";
3696
+ /** Label lane. Full width (and free to wrap) at L1; a fixed lane from `sm` up. */
3697
+ var SETTING_ROW_LABEL = "min-w-0 text-[11px] leading-tight text-foreground-subtle sm:w-32 sm:shrink-0 sm:pr-2";
3698
+ /**
3699
+ * Value lane — the value text plus its trailing affordances (copy / reveal).
3700
+ * `w-full` at L1 so the value owns the whole row once the label is above it;
3701
+ * `sm:flex-1` makes it share the row again from `sm` up.
3702
+ */
3703
+ var SETTING_ROW_VALUE = "flex w-full min-w-0 items-center gap-1 sm:flex-1";
3704
+ /**
3705
+ * The value text itself. `break-all` lets an opaque token (a key, a URL) wrap
3706
+ * at L1 instead of being cut mid-word; `sm:truncate` restores the single-line
3707
+ * desktop form, where the row is wide enough for truncation to be a choice
3708
+ * rather than data loss.
3709
+ */
3710
+ var SETTING_ROW_VALUE_TEXT = "min-w-0 break-all text-xs text-foreground sm:truncate";
3683
3711
  /** Section header label (uppercase tracking-wider). */
3684
3712
  var TEXT_SECTION_LABEL = "text-[10px] sm:text-[11px] font-semibold text-foreground uppercase tracking-wider";
3685
3713
  /** Field label inside a row. */
@@ -13225,15 +13253,27 @@ var MOBILE_QUERY = "(max-width: 767px)";
13225
13253
  * so sidebar fans out at the same point grids switch to multi-column.
13226
13254
  */
13227
13255
  var MID_QUERY = "(min-width: 768px) and (max-width: 1023px)";
13256
+ /**
13257
+ * `matchMedia` is universal in browsers but absent in bare DOM environments.
13258
+ * These hooks are called from shared primitives (`DataTable` picks its narrow
13259
+ * layout with one), so throwing here takes down the whole page that merely
13260
+ * rendered a table. Where the capability is missing, report the desktop
13261
+ * layout — a widescreen rendering is wrong-looking; a crashed page is gone.
13262
+ */
13263
+ function matchQuery(query) {
13264
+ if (typeof window.matchMedia !== "function") return null;
13265
+ return window.matchMedia(query);
13266
+ }
13228
13267
  function subscribeQuery(query) {
13229
13268
  return (callback) => {
13230
- const mql = window.matchMedia(query);
13269
+ const mql = matchQuery(query);
13270
+ if (mql === null) return () => {};
13231
13271
  mql.addEventListener("change", callback);
13232
13272
  return () => mql.removeEventListener("change", callback);
13233
13273
  };
13234
13274
  }
13235
13275
  function getSnapshot(query) {
13236
- return () => window.matchMedia(query).matches;
13276
+ return () => matchQuery(query)?.matches ?? false;
13237
13277
  }
13238
13278
  function getServerSnapshot() {
13239
13279
  return false;
@@ -13651,18 +13691,75 @@ function Breadcrumb({ items, className }) {
13651
13691
  });
13652
13692
  }
13653
13693
  //#endregion
13694
+ //#region src/composites/data-table-layout.ts
13695
+ /**
13696
+ * At this many columns and above, `auto` switches a narrow viewport to cards.
13697
+ * Three columns still fit a phone at a readable size; four do not.
13698
+ */
13699
+ var CARD_MODE_MIN_COLUMNS = 4;
13700
+ function resolveTableLayout({ mode, columnCount, isNarrow }) {
13701
+ if (!isNarrow) return "table";
13702
+ if (mode === "scroll") return "table";
13703
+ if (mode === "cards") return "cards";
13704
+ if (columnCount === 0) return "table";
13705
+ return columnCount >= 4 ? "cards" : "table";
13706
+ }
13707
+ //#endregion
13708
+ //#region src/composites/setting-row.tsx
13709
+ function SettingRow({ label, children, actions, elements = "plain", className, valueClassName }) {
13710
+ const LabelTag = elements === "description" ? "dt" : "span";
13711
+ const ValueTag = elements === "description" ? "dd" : "div";
13712
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13713
+ "data-setting-row": "",
13714
+ className: cn(SETTING_ROW, className),
13715
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabelTag, {
13716
+ "data-setting-row-label": "",
13717
+ className: SETTING_ROW_LABEL,
13718
+ children: label
13719
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ValueTag, {
13720
+ "data-setting-row-value": "",
13721
+ className: SETTING_ROW_VALUE,
13722
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13723
+ "data-setting-row-value-text": "",
13724
+ className: cn(SETTING_ROW_VALUE_TEXT, valueClassName),
13725
+ children
13726
+ }), actions !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13727
+ className: "flex shrink-0 items-center gap-0.5",
13728
+ children: actions
13729
+ })]
13730
+ })]
13731
+ });
13732
+ }
13733
+ //#endregion
13654
13734
  //#region src/composites/data-table.tsx
13655
13735
  var ALIGN_CLASS = {
13656
13736
  left: "text-left",
13657
13737
  right: "text-right",
13658
13738
  center: "text-center"
13659
13739
  };
13660
- function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyMessage, className, bordered = true, rowClassName }) {
13740
+ function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyMessage, className, bordered = true, rowClassName, mobileMode = "auto" }) {
13741
+ const isNarrow = useIsMobile();
13742
+ const layout = resolveTableLayout({
13743
+ mode: mobileMode,
13744
+ columnCount: columns.length,
13745
+ isNarrow
13746
+ });
13661
13747
  if (rows.length === 0 && emptyMessage) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13662
13748
  className: `rounded-lg ${bordered ? "border border-border" : ""} bg-surface px-3 py-4 text-xs text-foreground-subtle text-center ${className ?? ""}`,
13663
13749
  children: emptyMessage
13664
13750
  });
13665
13751
  if (rows.length === 0) return null;
13752
+ if (layout === "cards") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13753
+ className: `space-y-2 ${className ?? ""}`,
13754
+ children: rows.map((row, rowIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTableCard, {
13755
+ row,
13756
+ rowIndex,
13757
+ columns,
13758
+ bordered,
13759
+ onRowClick,
13760
+ rowClassName
13761
+ }, rowKey ? rowKey(row, rowIndex) : rowIndex))
13762
+ });
13666
13763
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13667
13764
  className: `rounded-lg ${bordered ? "border border-border" : ""} bg-surface overflow-x-auto ${className ?? ""}`,
13668
13765
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
@@ -13707,6 +13804,36 @@ function DataTable({ columns, rows, rowKey, onRowClick, minWidthPx = 480, emptyM
13707
13804
  })
13708
13805
  });
13709
13806
  }
13807
+ /**
13808
+ * One row as a card. Columns that carry a plain-text `header` become labelled
13809
+ * `SettingRow`s — the label is the column title and the value gets the full
13810
+ * card width. Columns with no text header (an actions column, a
13811
+ * `headerRender`-only column) have no label to show, so they render as a
13812
+ * full-width strip at the foot of the card.
13813
+ */
13814
+ function DataTableCard({ row, rowIndex, columns, bordered, onRowClick, rowClassName }) {
13815
+ const labelled = columns.filter((col) => col.header !== void 0 && col.header !== "");
13816
+ const unlabelled = columns.filter((col) => col.header === void 0 || col.header === "");
13817
+ const interactive = onRowClick !== void 0;
13818
+ const extra = rowClassName?.(row, rowIndex) ?? "";
13819
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13820
+ "data-data-table-card": "",
13821
+ onClick: interactive ? () => onRowClick(row, rowIndex) : void 0,
13822
+ className: [
13823
+ "rounded-lg bg-surface px-3 py-1.5 divide-y divide-border-subtle",
13824
+ bordered ? "border border-border" : "",
13825
+ interactive ? "cursor-pointer hover:bg-primary/5" : "",
13826
+ extra
13827
+ ].filter(Boolean).join(" "),
13828
+ children: [labelled.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
13829
+ label: col.header,
13830
+ children: col.render(row, rowIndex)
13831
+ }, col.key)), unlabelled.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13832
+ className: "flex flex-wrap items-center justify-end gap-2 py-1.5",
13833
+ children: unlabelled.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: col.render(row, rowIndex) }, col.key))
13834
+ })]
13835
+ });
13836
+ }
13710
13837
  //#endregion
13711
13838
  //#region src/composites/slide-over-panel.tsx
13712
13839
  /**
@@ -15839,18 +15966,21 @@ function StatCard({ value, label, trend, className }) {
15839
15966
  }
15840
15967
  //#endregion
15841
15968
  //#region src/composites/key-value-list.tsx
15969
+ /**
15970
+ * A `<dl>` of label/value rows. Layout (including the L1 stacking) is owned by
15971
+ * `SettingRow`; this composite only supplies the description-list semantics.
15972
+ *
15973
+ * The rows used to be `flex items-center h-7` with a `w-1/3` term, so a label
15974
+ * could neither wrap nor stack: on a phone it took a third of the width and
15975
+ * the value took whatever was left.
15976
+ */
15842
15977
  function KeyValueList({ items, className }) {
15843
15978
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
15844
15979
  className: cn("flex flex-col", className),
15845
- children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15846
- className: "flex items-center h-7",
15847
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
15848
- className: "text-foreground-subtle text-xs w-1/3 shrink-0",
15849
- children: item.key
15850
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
15851
- className: "text-foreground text-xs",
15852
- children: item.value
15853
- })]
15980
+ children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
15981
+ elements: "description",
15982
+ label: item.key,
15983
+ children: item.value
15854
15984
  }, item.key))
15855
15985
  });
15856
15986
  }
@@ -16218,7 +16348,8 @@ function DeviceGrid({ children, minCardWidth = 220, gap = 3, className }) {
16218
16348
  * longest). So as the viewport shrinks the HIGHEST-priority-number column
16219
16349
  * drops first.
16220
16350
  *
16221
- * name (0) — never hidden; sticky left, always visible
16351
+ * name (0) — never hidden; pinned left from `md` up (see
16352
+ * `NAME_COLUMN_PIN_CLASS`), scrolls below it
16222
16353
  * previewActions (1) — never hidden; carries the live control + status dot
16223
16354
  * icon (2) — integration badge; rendered INSIDE the name cell,
16224
16355
  * not a standalone column, so it has no breakpoint
@@ -16270,7 +16401,7 @@ function columnsForContext(ctx) {
16270
16401
  }
16271
16402
  /**
16272
16403
  * Lower number = higher priority (kept longest as width shrinks). `name` = 0
16273
- * (sticky, never dropped); `previewActions` = 1 (always visible). The optional
16404
+ * (never dropped); `previewActions` = 1 (always visible). The optional
16274
16405
  * columns drop in REVERSE priority order as width shrinks — highest number
16275
16406
  * (`type`) hides first, then `features`. `icon` is a name-cell badge, not a
16276
16407
  * standalone column. See `COLUMN_BREAKPOINT_CLASS` for the derived classes.
@@ -16306,6 +16437,14 @@ var COLUMN_BREAKPOINT_CLASS = {
16306
16437
  type: "hidden lg:table-cell",
16307
16438
  manufacturer: "hidden xl:table-cell"
16308
16439
  };
16440
+ /** Pin classes for the NAME `<th>`/`<td>` — never unconditional. */
16441
+ var NAME_COLUMN_PIN_CLASS = "md:sticky md:left-0 md:z-[1]";
16442
+ /**
16443
+ * NAME column width. Narrower below the pin breakpoint so NAME + Preview fit a
16444
+ * phone viewport without a horizontal scroll at all; the roomier desktop width
16445
+ * returns with the pin.
16446
+ */
16447
+ var NAME_COLUMN_WIDTH_CLASS = "w-44 max-w-[11rem] md:w-64 md:max-w-[16rem]";
16309
16448
  //#endregion
16310
16449
  //#region src/composites/device-list/hardware.ts
16311
16450
  var MANUFACTURER_KEY = "manufacturer";
@@ -29433,7 +29572,7 @@ function DeviceItemTableRow(props) {
29433
29572
  className: cn("group cursor-pointer hover:bg-surface-hover transition-colors", !hasChildren && "border-b border-border-subtle/40", isAccessoryRow && "bg-foreground-subtle/[0.03]", rowSelected && "bg-primary/10", className),
29434
29573
  children: [
29435
29574
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
29436
- className: cn("sticky left-0 z-[1] py-1.5 align-middle pr-2 transition-colors", rowSelected ? "bg-primary/10 group-hover:bg-surface-hover" : isAccessoryRow ? "bg-surface-subtle group-hover:bg-surface-hover" : "bg-surface group-hover:bg-surface-hover", "w-64 max-w-[16rem] min-w-0 overflow-hidden", INDENT_CLASS[indentLevel]),
29575
+ className: cn(NAME_COLUMN_PIN_CLASS, "py-1.5 align-middle pr-2 transition-colors", rowSelected ? "bg-primary/10 group-hover:bg-surface-hover" : isAccessoryRow ? "bg-surface-subtle group-hover:bg-surface-hover" : "bg-surface group-hover:bg-surface-hover", NAME_COLUMN_WIDTH_CLASS, "min-w-0 overflow-hidden", INDENT_CLASS[indentLevel]),
29437
29576
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29438
29577
  className: "flex items-center gap-1.5",
29439
29578
  children: [selection && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -30436,7 +30575,7 @@ function TableLayout({ rows, accessoriesByParent, autoExpandedParents, devices,
30436
30575
  children: [
30437
30576
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
30438
30577
  "aria-sort": ariaSortFor(sort, "name"),
30439
- className: "sticky left-0 z-[1] bg-surface text-left px-2 py-2 text-[9.5px] font-medium uppercase tracking-wider text-foreground-subtle w-64 max-w-[16rem]",
30578
+ className: cn(NAME_COLUMN_PIN_CLASS, NAME_COLUMN_WIDTH_CLASS, "bg-surface text-left px-2 py-2 text-[9.5px] font-medium uppercase tracking-wider text-foreground-subtle"),
30440
30579
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SortableHeaderButton, {
30441
30580
  columnId: "name",
30442
30581
  label: "Name",
@@ -35086,15 +35225,14 @@ var MODES = [
35086
35225
  label: "Continuous"
35087
35226
  }
35088
35227
  ];
35089
- function RecordingSettings({ initial, saving, onSave, onRegenerateStrips, regeneratingStrips }) {
35228
+ function RecordingSettings({ initial, saving, onSave }) {
35090
35229
  const [tab, setTab] = (0, react$1.useState)(isAdvancedConfig(initial) ? "advanced" : "base");
35091
35230
  const [base, setBase] = (0, react$1.useState)(formStateFromConfig(initial));
35092
35231
  const [bands, setBands] = (0, react$1.useState)([...initial.bands]);
35093
35232
  const [common, setCommon] = (0, react$1.useState)({
35094
35233
  profiles: initial.profiles,
35095
35234
  segmentSeconds: initial.segmentSeconds,
35096
- retention: initial.retention,
35097
- stripsEnabled: initial.stripsEnabled
35235
+ retention: initial.retention
35098
35236
  });
35099
35237
  const ret = common.retention ?? {};
35100
35238
  const setRet = (patch) => setCommon({
@@ -35132,8 +35270,7 @@ function RecordingSettings({ initial, saving, onSave, onRegenerateStrips, regene
35132
35270
  const shared = {
35133
35271
  profiles: common.profiles ? [...common.profiles] : void 0,
35134
35272
  segmentSeconds: common.segmentSeconds,
35135
- retention: common.retention,
35136
- ...common.stripsEnabled === void 0 ? {} : { stripsEnabled: common.stripsEnabled }
35273
+ retention: common.retention
35137
35274
  };
35138
35275
  if (tab === "base") onSave({
35139
35276
  ...configFromFormState({
@@ -35285,65 +35422,37 @@ function RecordingSettings({ initial, saving, onSave, onRegenerateStrips, regene
35285
35422
  }),
35286
35423
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35287
35424
  className: "flex flex-wrap items-center gap-4 text-xs",
35288
- children: [
35289
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35290
- className: "flex items-center gap-2",
35291
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35292
- className: "text-foreground-subtle",
35293
- children: "Profiles:"
35294
- }), PROFILES.map((p) => {
35295
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35296
- type: "button",
35297
- onClick: () => toggleProfile(p),
35298
- className: `rounded px-2 py-1 ${common.profiles == null || common.profiles.includes(p) ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
35299
- children: p
35300
- }, p);
35301
- })]
35302
- }),
35303
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35304
- className: "flex items-center gap-1",
35305
- children: [
35306
- "Segment",
35307
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35308
- type: "number",
35309
- min: 1,
35310
- value: common.segmentSeconds ?? "",
35311
- placeholder: "default",
35312
- onChange: (e) => setCommon({
35313
- ...common,
35314
- segmentSeconds: numOrUndef(e.target.value)
35315
- }),
35316
- className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
35317
- }),
35318
- "s"
35319
- ]
35320
- }),
35321
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35322
- className: "flex items-center gap-1",
35323
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35324
- type: "checkbox",
35325
- checked: common.stripsEnabled === true,
35425
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35426
+ className: "flex items-center gap-2",
35427
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35428
+ className: "text-foreground-subtle",
35429
+ children: "Profiles:"
35430
+ }), PROFILES.map((p) => {
35431
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35432
+ type: "button",
35433
+ onClick: () => toggleProfile(p),
35434
+ className: `rounded px-2 py-1 ${common.profiles == null || common.profiles.includes(p) ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
35435
+ children: p
35436
+ }, p);
35437
+ })]
35438
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35439
+ className: "flex items-center gap-1",
35440
+ children: [
35441
+ "Segment",
35442
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35443
+ type: "number",
35444
+ min: 1,
35445
+ value: common.segmentSeconds ?? "",
35446
+ placeholder: "default",
35326
35447
  onChange: (e) => setCommon({
35327
35448
  ...common,
35328
- stripsEnabled: e.target.checked
35329
- })
35330
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35331
- className: "text-foreground-subtle",
35332
- children: "Scrub thumbnail strips"
35333
- })]
35334
- }),
35335
- onRegenerateStrips ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35336
- type: "button",
35337
- onClick: onRegenerateStrips,
35338
- disabled: regeneratingStrips === true,
35339
- className: "rounded-md border border-border bg-surface-hover px-2 py-1 text-xs text-foreground-subtle transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50",
35340
- children: regeneratingStrips === true ? "Regenerating…" : "Regenerate strips"
35341
- }) : null
35342
- ]
35343
- }),
35344
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35345
- className: "text-[11px] text-foreground-subtle",
35346
- children: "OPT-IN: save every keyframe of the low recording as a JPEG strip for fluid fast scrubbing. Costs disk (a derived cache, reclaimed with the footage). Regenerate clears and rebuilds the recent days from the segments already on disk."
35449
+ segmentSeconds: numOrUndef(e.target.value)
35450
+ }),
35451
+ className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
35452
+ }),
35453
+ "s"
35454
+ ]
35455
+ })]
35347
35456
  }),
35348
35457
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35349
35458
  className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
@@ -36592,14 +36701,6 @@ function RecordingPanel({ deviceId }) {
36592
36701
  const configQuery = useRecordingGetDeviceConfig({ deviceId });
36593
36702
  const setConfig = useRecordingSetDeviceConfig();
36594
36703
  const rescanStorage = useRecordingRescanStorage();
36595
- const customAction = useAddonsCustom();
36596
- const regenerateStrips = (0, react$1.useCallback)(() => {
36597
- customAction.mutate({
36598
- addonId: "recorder",
36599
- action: "regenerateStrips",
36600
- input: { deviceId }
36601
- });
36602
- }, [customAction, deviceId]);
36603
36704
  const saveConfig = (config) => {
36604
36705
  setConfig.mutate({
36605
36706
  deviceId,
@@ -36641,9 +36742,7 @@ function RecordingPanel({ deviceId }) {
36641
36742
  children: resolvedConfig ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordingSettings, {
36642
36743
  initial: resolvedConfig,
36643
36744
  saving: setConfig.isPending,
36644
- onSave: saveConfig,
36645
- onRegenerateStrips: regenerateStrips,
36646
- regeneratingStrips: customAction.isPending
36745
+ onSave: saveConfig
36647
36746
  }, JSON.stringify(resolvedConfig)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36648
36747
  className: "px-1 py-2 text-xs text-foreground-subtle",
36649
36748
  children: "Loading settings…"
@@ -38979,9 +39078,9 @@ function ObjectArrayField({ field }) {
38979
39078
  className: "rounded-md border border-border bg-surface-subtle px-3 py-2 text-xs text-foreground-subtle",
38980
39079
  children: field.emptyMessage ?? "No entries"
38981
39080
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
38982
- className: "rounded-md border border-border overflow-hidden",
39081
+ className: "rounded-md border border-border overflow-x-auto",
38983
39082
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
38984
- className: "w-full text-xs",
39083
+ className: "w-full min-w-[28rem] text-xs",
38985
39084
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", {
38986
39085
  className: "bg-surface-subtle border-b border-border",
38987
39086
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: field.columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
@@ -40208,6 +40307,31 @@ function DetectionBoxes({ detections, frameWidth, frameHeight }) {
40208
40307
  }) });
40209
40308
  }
40210
40309
  //#endregion
40310
+ //#region src/composites/reconnect-schedule.ts
40311
+ /** Exponential backoff, ±20% jitter, bounded attempts. The bound exists so a
40312
+ * permanently-dead stream stops consuming signaling; the EXHAUSTED action is
40313
+ * the caller's cue to surface a hard error (never to go silent). */
40314
+ var RECONNECT_POLICY = {
40315
+ baseDelayMs: 1500,
40316
+ maxDelayMs: 15e3,
40317
+ maxAttempts: 40
40318
+ };
40319
+ /**
40320
+ * Decide what attempt number `attempt` (0-based) should do. `random` is the
40321
+ * jitter source (unit interval), injectable for tests.
40322
+ */
40323
+ function nextReconnectAction(attempt, random = Math.random) {
40324
+ if (attempt >= RECONNECT_POLICY.maxAttempts) return {
40325
+ kind: "exhausted",
40326
+ attempts: attempt
40327
+ };
40328
+ const base = Math.min(RECONNECT_POLICY.maxDelayMs, RECONNECT_POLICY.baseDelayMs * 2 ** attempt);
40329
+ return {
40330
+ kind: "retry",
40331
+ delayMs: Math.round(base * (.8 + random() * .4))
40332
+ };
40333
+ }
40334
+ //#endregion
40211
40335
  //#region src/composites/camera-stream-player.tsx
40212
40336
  /**
40213
40337
  * Silence (or restore) a live WebRTC stream FOR REAL.
@@ -40270,11 +40394,8 @@ function computeClientHints(container) {
40270
40394
  }
40271
40395
  return hints;
40272
40396
  }
40273
- var RECONNECT_BASE_DELAY_MS = 1500;
40274
- var RECONNECT_MAX_DELAY_MS = 15e3;
40275
- var MAX_RECONNECT_ATTEMPTS = 40;
40276
40397
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
40277
- function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, onClientNetworkSample, onConnectTiming, className = "", onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel }) {
40398
+ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, onClientNetworkSample, onConnectTiming, className = "", onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, onVideoElement }) {
40278
40399
  const videoRef = (0, react$1.useRef)(null);
40279
40400
  const containerRef = (0, react$1.useRef)(null);
40280
40401
  const pcRef = (0, react$1.useRef)(null);
@@ -40290,6 +40411,14 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40290
40411
  * consumer, without re-creating the connect callback on every render. */
40291
40412
  const onControlChannelRef = (0, react$1.useRef)(onControlChannel);
40292
40413
  onControlChannelRef.current = onControlChannel;
40414
+ /** Same pattern for the video-element handle: delivered once on mount, null
40415
+ * on unmount — the ref keeps the latest consumer without re-running. */
40416
+ const onVideoElementRef = (0, react$1.useRef)(onVideoElement);
40417
+ onVideoElementRef.current = onVideoElement;
40418
+ (0, react$1.useEffect)(() => {
40419
+ onVideoElementRef.current?.(videoRef.current);
40420
+ return () => onVideoElementRef.current?.(null);
40421
+ }, []);
40293
40422
  /** The live session being polled for `pendingRenegotiation` (client-offer). */
40294
40423
  const activeSessionIdRef = (0, react$1.useRef)(null);
40295
40424
  /** Timer for the session-state (renegotiation) poll loop. */
@@ -40678,6 +40807,8 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40678
40807
  iceConnectedMs: timing.iceConnectedMs,
40679
40808
  firstTrackMs: timing.firstTrackMs
40680
40809
  });
40810
+ reportHardErrorRef.current("no decoded frame after connect");
40811
+ scheduleReconnect();
40681
40812
  }, FIRST_FRAME_TIMEOUT_MS);
40682
40813
  }
40683
40814
  }
@@ -40946,17 +41077,32 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40946
41077
  ]);
40947
41078
  const connect = useClientOffer ? connectClientOffer : useServerOffer ? connectServerOffer : connectWhep;
40948
41079
  const connectRef = (0, react$1.useRef)(() => {});
41080
+ const onReconnectAttemptRef = (0, react$1.useRef)(onReconnectAttempt);
41081
+ onReconnectAttemptRef.current = onReconnectAttempt;
41082
+ const reportHardErrorRef = (0, react$1.useRef)(() => {});
41083
+ reportHardErrorRef.current = (msg) => {
41084
+ console.warn("[WebRTC] hard error", {
41085
+ streamKey,
41086
+ msg
41087
+ });
41088
+ setErrorMessage(msg);
41089
+ onError?.(msg);
41090
+ updateState("error");
41091
+ };
40949
41092
  connectRef.current = connect;
40950
41093
  const scheduleReconnect = (0, react$1.useCallback)(() => {
40951
41094
  if (!mountedRef.current) return;
40952
- if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) return;
40953
41095
  const attempt = reconnectAttemptsRef.current;
41096
+ const action = nextReconnectAction(attempt);
41097
+ onReconnectAttemptRef.current?.(action);
41098
+ if (action.kind === "exhausted") {
41099
+ reportHardErrorRef.current(`reconnect attempts exhausted (${action.attempts})`);
41100
+ return;
41101
+ }
40954
41102
  reconnectAttemptsRef.current += 1;
40955
- const base = Math.min(RECONNECT_MAX_DELAY_MS, RECONNECT_BASE_DELAY_MS * 2 ** attempt);
40956
- const delay = Math.round(base * (.8 + Math.random() * .4));
40957
41103
  reconnectTimerRef.current = setTimeout(() => {
40958
41104
  if (mountedRef.current) connectRef.current();
40959
- }, delay);
41105
+ }, action.delayMs);
40960
41106
  }, []);
40961
41107
  (0, react$1.useEffect)(() => {
40962
41108
  mountedRef.current = true;
@@ -41337,7 +41483,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41337
41483
  children: statsText
41338
41484
  }),
41339
41485
  overlay,
41340
- !stillImg && state === "connecting" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
41486
+ showControls && !stillImg && state === "connecting" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
41341
41487
  className: "absolute inset-0 z-10 transform-gpu flex flex-col items-center justify-center bg-black/70 gap-2",
41342
41488
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
41343
41489
  className: "h-6 w-6 text-white/60 animate-spin",
@@ -41359,7 +41505,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41359
41505
  children: "Connecting…"
41360
41506
  })]
41361
41507
  }),
41362
- !stillImg && (state === "error" || state === "disconnected") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
41508
+ showControls && !stillImg && (state === "error" || state === "disconnected") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
41363
41509
  className: "absolute inset-0 z-10 transform-gpu flex flex-col items-center justify-center bg-black/70 gap-2",
41364
41510
  children: [
41365
41511
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
@@ -41384,7 +41530,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41384
41530
  })
41385
41531
  ]
41386
41532
  }),
41387
- stillImg && (state === "connecting" || state === "disconnected") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
41533
+ showControls && stillImg && (state === "connecting" || state === "disconnected") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
41388
41534
  className: "absolute top-2 left-2 z-10 transform-gpu flex h-6 w-6 items-center justify-center rounded-full bg-black/55",
41389
41535
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
41390
41536
  className: "h-3.5 w-3.5 text-white/90 animate-spin",
@@ -41403,7 +41549,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41403
41549
  })
41404
41550
  })
41405
41551
  }),
41406
- stillImg && state === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
41552
+ showControls && stillImg && state === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
41407
41553
  onClick: handleReconnect,
41408
41554
  title: errorMessage || "Reconnect",
41409
41555
  className: "absolute top-2 left-2 z-10 transform-gpu flex h-6 w-6 items-center justify-center rounded-full bg-black/55 text-white/90 hover:bg-black/75 transition-colors",
@@ -42517,7 +42663,7 @@ function StreamBrokerSelector({ deviceId, value, onChange, disabled, label, clas
42517
42663
  * needs a one-click copy affordance (export setup panels, etc.).
42518
42664
  */
42519
42665
  var COPIED_RESET_MS = 2e3;
42520
- function CopyButton({ value, label, className, disabled }) {
42666
+ function CopyButton({ value, label, srLabel, className, disabled }) {
42521
42667
  const [copied, setCopied] = (0, react$1.useState)(false);
42522
42668
  const handleCopy = (0, react$1.useCallback)(() => {
42523
42669
  if (!value) return;
@@ -42533,7 +42679,7 @@ function CopyButton({ value, label, className, disabled }) {
42533
42679
  disabled: disabled || value.length === 0,
42534
42680
  onClick: handleCopy,
42535
42681
  className: cn(className),
42536
- "aria-label": copied ? "Copied" : `Copy ${label ?? "value"}`,
42682
+ "aria-label": copied ? "Copied" : `Copy ${srLabel ?? label ?? "value"}`,
42537
42683
  children: [copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" }), label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
42538
42684
  className: "ml-1",
42539
42685
  children: copied ? "Copied" : label
@@ -42602,35 +42748,30 @@ function capitaliseLinkState(state) {
42602
42748
  /**
42603
42749
  * A single label/value row in the Setup section. `secret` rows mask the
42604
42750
  * value behind a reveal toggle; every row gets a copy button.
42751
+ *
42752
+ * Layout — including the L1 stack that keeps a long token readable on a phone
42753
+ * — is owned by the shared `<SettingRow>`; this component only decides what
42754
+ * the value and the affordances are.
42605
42755
  */
42606
42756
  function SetupFieldRow({ field }) {
42607
42757
  const [revealed, setRevealed] = (0, react$1.useState)(false);
42608
42758
  const isSecret = field.secret === true;
42609
42759
  const displayValue = isSecret && !revealed ? "•".repeat(Math.min(field.value.length, 24)) : field.value;
42610
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
42611
- className: "flex items-center gap-2 py-1.5",
42612
- children: [
42613
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
42614
- className: "text-[11px] text-foreground-subtle w-32 shrink-0",
42615
- children: field.label
42616
- }),
42617
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
42618
- className: "flex-1 min-w-0 truncate font-mono text-xs text-foreground",
42619
- children: displayValue || ""
42620
- }),
42621
- isSecret && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
42622
- size: "sm",
42623
- variant: "ghost",
42624
- type: "button",
42625
- "aria-label": revealed ? "Hide value" : "Reveal value",
42626
- onClick: () => setRevealed((v) => !v),
42627
- children: revealed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eye, { className: "h-3.5 w-3.5" })
42628
- }),
42629
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
42630
- value: field.value,
42631
- label: field.label
42632
- })
42633
- ]
42760
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
42761
+ label: field.label,
42762
+ valueClassName: "font-mono",
42763
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [isSecret && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
42764
+ size: "sm",
42765
+ variant: "ghost",
42766
+ type: "button",
42767
+ "aria-label": revealed ? "Hide value" : "Reveal value",
42768
+ onClick: () => setRevealed((v) => !v),
42769
+ children: revealed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EyeOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eye, { className: "h-3.5 w-3.5" })
42770
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
42771
+ value: field.value,
42772
+ srLabel: field.label
42773
+ })] }),
42774
+ children: displayValue || ""
42634
42775
  });
42635
42776
  }
42636
42777
  /**
@@ -46736,6 +46877,112 @@ function useDeviceDetections(trpc, deviceId) {
46736
46877
  };
46737
46878
  }
46738
46879
  //#endregion
46880
+ //#region src/hooks/turn-server-cache.ts
46881
+ /** Short enough that rotated credentials are picked up promptly. */
46882
+ var DEFAULT_FRESH_TTL_MS = 5 * 6e4;
46883
+ /** Half of the shortest provider credential lifetime in the fleet (24 h). */
46884
+ var DEFAULT_STALE_CAP_MS = 720 * 6e4;
46885
+ var DEFAULT_STORAGE_KEY = "camstack:turn-servers:v1";
46886
+ function defaultStorage() {
46887
+ const g = globalThis;
46888
+ try {
46889
+ return g.localStorage ?? null;
46890
+ } catch {
46891
+ return null;
46892
+ }
46893
+ }
46894
+ /** Type guard for a persisted entry — storage content is external data. */
46895
+ function isCacheEntry(value) {
46896
+ if (typeof value !== "object" || value === null) return false;
46897
+ const v = value;
46898
+ if (typeof v.fetchedAt !== "number" || !Array.isArray(v.servers)) return false;
46899
+ return v.servers.every((s) => {
46900
+ if (typeof s !== "object" || s === null) return false;
46901
+ const srv = s;
46902
+ if (!(typeof srv.urls === "string" || Array.isArray(srv.urls) && srv.urls.every((u) => typeof u === "string"))) return false;
46903
+ if (srv.username !== void 0 && typeof srv.username !== "string") return false;
46904
+ if (srv.credential !== void 0 && typeof srv.credential !== "string") return false;
46905
+ return true;
46906
+ });
46907
+ }
46908
+ var TurnServerCache = class {
46909
+ freshTtlMs;
46910
+ staleCapMs;
46911
+ storage;
46912
+ now;
46913
+ storageKey;
46914
+ /** Keyed on the caller's tRPC client object (stable per connected system). */
46915
+ memory = /* @__PURE__ */ new WeakMap();
46916
+ /** In-flight fetch per key — concurrent callers share one round trip. */
46917
+ inflight = /* @__PURE__ */ new WeakMap();
46918
+ constructor(options = {}) {
46919
+ this.freshTtlMs = options.freshTtlMs ?? DEFAULT_FRESH_TTL_MS;
46920
+ this.staleCapMs = options.staleCapMs ?? DEFAULT_STALE_CAP_MS;
46921
+ this.storage = "storage" in options ? options.storage ?? null : defaultStorage();
46922
+ this.now = options.now ?? (() => Date.now());
46923
+ this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
46924
+ }
46925
+ /**
46926
+ * Resolve the ICE servers for `key`, fetching via `fetch` only when no
46927
+ * fresh-enough entry exists. Never rejects: a failed fetch resolves to the
46928
+ * stale entry when one exists, else `undefined`.
46929
+ */
46930
+ async getOrFetch(key, fetch) {
46931
+ const at = this.now();
46932
+ const entry = this.memory.get(key) ?? this.readStorage();
46933
+ if (entry) {
46934
+ const age = at - entry.fetchedAt;
46935
+ if (age < this.freshTtlMs) return entry.servers;
46936
+ if (age < this.staleCapMs) {
46937
+ this.startFetch(key, fetch, entry);
46938
+ return entry.servers;
46939
+ }
46940
+ }
46941
+ return this.startFetch(key, fetch, entry);
46942
+ }
46943
+ startFetch(key, fetch, stale) {
46944
+ const pending = this.inflight.get(key);
46945
+ if (pending) return pending;
46946
+ const run = (async () => {
46947
+ try {
46948
+ const servers = await fetch();
46949
+ if (servers.length > 0) {
46950
+ const entry = {
46951
+ servers,
46952
+ fetchedAt: this.now()
46953
+ };
46954
+ this.memory.set(key, entry);
46955
+ this.writeStorage(entry);
46956
+ }
46957
+ return servers;
46958
+ } catch {
46959
+ return stale?.servers;
46960
+ }
46961
+ })().finally(() => {
46962
+ this.inflight.delete(key);
46963
+ });
46964
+ this.inflight.set(key, run);
46965
+ return run;
46966
+ }
46967
+ readStorage() {
46968
+ if (!this.storage) return void 0;
46969
+ try {
46970
+ const raw = this.storage.getItem(this.storageKey);
46971
+ if (raw === null) return void 0;
46972
+ const parsed = JSON.parse(raw);
46973
+ return isCacheEntry(parsed) ? parsed : void 0;
46974
+ } catch {
46975
+ return;
46976
+ }
46977
+ }
46978
+ writeStorage(entry) {
46979
+ if (!this.storage) return;
46980
+ try {
46981
+ this.storage.setItem(this.storageKey, JSON.stringify(entry));
46982
+ } catch {}
46983
+ }
46984
+ };
46985
+ //#endregion
46739
46986
  //#region src/hooks/use-device-webrtc.ts
46740
46987
  /**
46741
46988
  * useDeviceWebrtc — WebRTC signaling hook for device-scoped streaming.
@@ -46753,15 +47000,16 @@ function useDeviceDetections(trpc, deviceId) {
46753
47000
  * @param deviceId - numeric device ID (null = disabled)
46754
47001
  * @param pollIntervalMs - how often to refresh profile slots (default: 5000)
46755
47002
  */
46756
- /** TTL for the TURN/STUN credential cache. Short enough that rotated creds
46757
- * are picked up promptly, long enough to cover a connect + several profile
46758
- * switches without re-paying the fetch. */
46759
- var TURN_CACHE_TTL_MS = 5 * 6e4;
46760
- /** Keyed on the caller's trpc client object (stable per connected system —
46761
- * NOT on `trpc.turnProvider`, which is a proxy minted fresh on every property
46762
- * access), so two admin tabs pointed at different hubs never cross-serve
46763
- * credentials. An unstable key degrades to a cache miss, never to wrong creds. */
46764
- var turnServersCache = /* @__PURE__ */ new WeakMap();
47003
+ /** Module-level TURN/STUN credential cache. Keyed on the caller's trpc client
47004
+ * object (stable per connected system NOT on `trpc.turnProvider`, which is
47005
+ * a proxy minted fresh on every property access), so two admin tabs pointed
47006
+ * at different hubs never cross-serve credentials from the memory layer; the
47007
+ * storage layer is per-origin, and the page's origin IS the hub. Coalesces
47008
+ * concurrent fetches (the mount prefetch and the connect's own call used to
47009
+ * race into TWO round trips) and persists across page loads so a fresh embed
47010
+ * page one per camera open on native finds warm credentials instead of
47011
+ * re-paying the measured 0.2–3.6 s serialized fetch. See turn-server-cache.ts. */
47012
+ var turnServersCache = new TurnServerCache();
46765
47013
  function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
46766
47014
  const [remoteStreams, setRemoteStreams] = (0, react$1.useState)([]);
46767
47015
  (0, react$1.useEffect)(() => {
@@ -46830,25 +47078,17 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
46830
47078
  };
46831
47079
  }, [deviceId, remoteStreams]);
46832
47080
  const getIceServers = (0, react$1.useCallback)(async () => {
46833
- if (!trpc.turnProvider) return void 0;
46834
- const cached = turnServersCache.get(trpc);
46835
- if (cached && Date.now() - cached.fetchedAt < TURN_CACHE_TTL_MS) return cached.servers;
46836
- try {
46837
- const mapped = (await trpc.turnProvider.getTurnServers.query()).map((s) => {
47081
+ const provider = trpc.turnProvider;
47082
+ if (!provider) return void 0;
47083
+ return turnServersCache.getOrFetch(trpc, async () => {
47084
+ return (await provider.getTurnServers.query()).map((s) => {
46838
47085
  return {
46839
47086
  urls: typeof s.urls === "string" ? s.urls : [...s.urls],
46840
47087
  ...s.username !== void 0 ? { username: s.username } : {},
46841
47088
  ...s.credential !== void 0 ? { credential: s.credential } : {}
46842
47089
  };
46843
47090
  });
46844
- turnServersCache.set(trpc, {
46845
- servers: mapped,
46846
- fetchedAt: Date.now()
46847
- });
46848
- return mapped;
46849
- } catch {
46850
- return cached?.servers;
46851
- }
47091
+ });
46852
47092
  }, [trpc]);
46853
47093
  (0, react$1.useEffect)(() => {
46854
47094
  getIceServers();
@@ -47244,6 +47484,7 @@ exports.BrightnessPanel = BrightnessPanel;
47244
47484
  exports.Button = Button;
47245
47485
  exports.ButtonControl = ButtonControl;
47246
47486
  exports.ButtonHeroCard = ButtonHeroCard;
47487
+ exports.CARD_MODE_MIN_COLUMNS = CARD_MODE_MIN_COLUMNS;
47247
47488
  exports.CENTER = CENTER;
47248
47489
  exports.CHIP_ACTIVE = CHIP_ACTIVE;
47249
47490
  exports.CHIP_BASE = CHIP_BASE;
@@ -47395,6 +47636,7 @@ exports.PrivacyMaskSettings = PrivacyMaskSettings;
47395
47636
  exports.ProviderBadge = ProviderBadge;
47396
47637
  exports.PtzPanel = PtzPanel;
47397
47638
  exports.QrCode = QrCode;
47639
+ exports.RECONNECT_POLICY = RECONNECT_POLICY;
47398
47640
  exports.RECORDED_PLAYBACK_MODES = RECORDED_PLAYBACK_MODES;
47399
47641
  exports.RIGHT = RIGHT;
47400
47642
  exports.ROLE_DESCRIPTOR = ROLE_DESCRIPTOR;
@@ -47405,6 +47647,11 @@ exports.ResponseLog = ResponseLog;
47405
47647
  exports.SECTION_BODY = SECTION_BODY;
47406
47648
  exports.SECTION_CARD = SECTION_CARD;
47407
47649
  exports.SECTION_HEADER = SECTION_HEADER;
47650
+ exports.SETTING_ROW = SETTING_ROW;
47651
+ exports.SETTING_ROW_LABEL = SETTING_ROW_LABEL;
47652
+ exports.SETTING_ROW_STACK_BREAKPOINT = SETTING_ROW_STACK_BREAKPOINT;
47653
+ exports.SETTING_ROW_VALUE = SETTING_ROW_VALUE;
47654
+ exports.SETTING_ROW_VALUE_TEXT = SETTING_ROW_VALUE_TEXT;
47408
47655
  exports.SPLIT_PANEL_OUTER = SPLIT_PANEL_OUTER;
47409
47656
  exports.SPLIT_PANEL_SIDE = SPLIT_PANEL_SIDE;
47410
47657
  exports.STACK_GAP = STACK_GAP;
@@ -47417,6 +47664,7 @@ exports.SensorHeroCard = SensorHeroCard;
47417
47664
  exports.SensorInlineControl = SensorInlineControl;
47418
47665
  exports.SensorValueAtom = SensorValueAtom;
47419
47666
  exports.Separator = Separator;
47667
+ exports.SettingRow = SettingRow;
47420
47668
  exports.Sidebar = Sidebar;
47421
47669
  exports.SidebarItem = SidebarItem;
47422
47670
  exports.Skeleton = Skeleton;
@@ -47522,6 +47770,7 @@ exports.metadataEntries = metadataEntries;
47522
47770
  exports.metadataString = metadataString;
47523
47771
  exports.mirror = mirror;
47524
47772
  exports.mountAddonPage = mountAddonPage;
47773
+ exports.nextReconnectAction = nextReconnectAction;
47525
47774
  exports.nextSort = nextSort;
47526
47775
  exports.normalizeForSearch = normalizeForSearch;
47527
47776
  exports.overrideEntityIdFromLink = overrideEntityIdFromLink;
@@ -47535,6 +47784,7 @@ exports.resolveEventKindIcon = resolveEventKindIcon;
47535
47784
  exports.resolvePrimaryChild = resolvePrimaryChild;
47536
47785
  exports.resolveSensorDisplay = resolveSensorDisplay;
47537
47786
  exports.resolveStepDefaultModel = resolveStepDefaultModel;
47787
+ exports.resolveTableLayout = resolveTableLayout;
47538
47788
  exports.scrubReducer = scrubReducer;
47539
47789
  exports.selectedDeviceOptions = selectedDeviceOptions;
47540
47790
  exports.serializeRecordedCommand = serializeRecordedCommand;