@camstack/ui-library 1.2.16 → 1.2.18

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.
@@ -1,5 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { CamProfile } from '@camstack/types';
3
+ import { ReconnectAction } from './reconnect-schedule';
3
4
  type WebkitPresentationMode = 'inline' | 'picture-in-picture' | 'fullscreen';
4
5
  declare global {
5
6
  interface HTMLVideoElement {
@@ -145,6 +146,13 @@ export interface CameraStreamPlayerProps {
145
146
  onStateChange?: (state: PlayerConnectionState) => void;
146
147
  /** Called on error */
147
148
  onError?: (error: string) => void;
149
+ /**
150
+ * Observer of the auto-retry loop: called with every scheduled attempt and
151
+ * with the exhaustion edge. Hosts wire it to their log pipeline (tagged with
152
+ * the camera) — a retry loop nobody can see is how a camera stays dark
153
+ * while looking busy.
154
+ */
155
+ onReconnectAttempt?: (action: ReconnectAction) => void;
148
156
  /** Custom overlay rendered on top of the video */
149
157
  overlay?: ReactNode;
150
158
  /**
@@ -242,6 +250,13 @@ export interface CameraStreamPlayerProps {
242
250
  * live player — no data channel is created and behaviour is unchanged.
243
251
  */
244
252
  onControlChannel?: (channel: RTCDataChannel) => void;
253
+ /**
254
+ * Optional handle to the player's `<video>` element, delivered once on mount
255
+ * (and `null` on unmount). Consumers that pace server-pushed frames by what
256
+ * the viewer actually RENDERED (`requestVideoFrameCallback`) need the
257
+ * element itself — the recorded-scrub receiver-credit acks are the use case.
258
+ */
259
+ onVideoElement?: (el: HTMLVideoElement | null) => void;
245
260
  }
246
- export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onPlaybackStats, onClientNetworkSample, onConnectTiming, className, onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, }: CameraStreamPlayerProps): import("react").JSX.Element;
261
+ export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onPlaybackStats, onClientNetworkSample, onConnectTiming, className, onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, onVideoElement, }: CameraStreamPlayerProps): import("react").JSX.Element;
247
262
  export {};
@@ -3,10 +3,6 @@ interface RecordingSettingsProps {
3
3
  readonly initial: RecordingConfig;
4
4
  readonly saving: boolean;
5
5
  readonly onSave: (config: RecordingConfig) => void;
6
- /** Regenerate the scrub strips for this camera (clear + rebuild from the
7
- * recorded low segments). Absent → the button is hidden. */
8
- readonly onRegenerateStrips?: () => void;
9
- readonly regeneratingStrips?: boolean;
10
6
  }
11
- export declare function RecordingSettings({ initial, saving, onSave, onRegenerateStrips, regeneratingStrips, }: RecordingSettingsProps): import("react").JSX.Element;
7
+ export declare function RecordingSettings({ initial, saving, onSave }: RecordingSettingsProps): import("react").JSX.Element;
12
8
  export {};
@@ -2,11 +2,20 @@ import { ReactElement } from 'react';
2
2
  export interface CopyButtonProps {
3
3
  /** String copied to the clipboard when the button is pressed. */
4
4
  readonly value: string;
5
- /** Optional visible label rendered next to the icon. Icon-only when omitted. */
5
+ /** Optional visible label rendered next to the icon. Icon-only when omitted.
6
+ *
7
+ * Do NOT pass the name of the field the button sits next to: the row
8
+ * already shows it, and the repeat is what overlapped the value on a phone
9
+ * (a token row rendered its own name on top of the masked secret). Use
10
+ * {@link CopyButtonProps.srLabel} for that — it names the target for screen
11
+ * readers without spending horizontal space. */
6
12
  readonly label?: string;
13
+ /** Names what is copied, for assistive tech only — never rendered. Falls back
14
+ * to `label`, then to a generic word. */
15
+ readonly srLabel?: string;
7
16
  /** Extra classes merged onto the button. */
8
17
  readonly className?: string;
9
18
  /** Disable the button (e.g. nothing to copy). */
10
19
  readonly disabled?: boolean;
11
20
  }
12
- export declare function CopyButton({ value, label, className, disabled }: CopyButtonProps): ReactElement;
21
+ export declare function CopyButton({ value, label, srLabel, className, disabled, }: CopyButtonProps): ReactElement;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The one decision a shared table has to make on a phone.
3
+ *
4
+ * A wide table has no good phone rendering. There are exactly two honest
5
+ * answers — become a list of cards, or scroll horizontally INSIDE its own
6
+ * container while the page body never moves sideways — and shrinking the font
7
+ * is not one of them. `DataTable` supports both; this module decides which, so
8
+ * the choice is a pure, testable rule instead of per-page CSS.
9
+ *
10
+ * The default rule is column count. Below the card breakpoint a phone fits
11
+ * roughly three readable columns; past that, a sideways scroll means the
12
+ * operator reads a wide table through a slit and loses the row they started
13
+ * on. Cards trade the column alignment (which nobody can use at that width
14
+ * anyway) for every field of one row being readable at once.
15
+ *
16
+ * Pass an explicit `mobileMode` to override per table — a wide table whose
17
+ * value IS the side-by-side comparison (a schedule, a matrix) is better
18
+ * scrolled than carded, and says so at its call site.
19
+ */
20
+ /** Per-table override. `auto` applies the column-count rule below. */
21
+ export type TableMobileMode = 'auto' | 'scroll' | 'cards';
22
+ /** How the table renders once the decision is made. */
23
+ export type TableLayoutKind = 'table' | 'cards';
24
+ /**
25
+ * At this many columns and above, `auto` switches a narrow viewport to cards.
26
+ * Three columns still fit a phone at a readable size; four do not.
27
+ */
28
+ export declare const CARD_MODE_MIN_COLUMNS = 4;
29
+ export interface ResolveTableLayoutInput {
30
+ readonly mode: TableMobileMode;
31
+ readonly columnCount: number;
32
+ /** Viewport below the card breakpoint (`useIsMobile`). */
33
+ readonly isNarrow: boolean;
34
+ }
35
+ export declare function resolveTableLayout({ mode, columnCount, isNarrow, }: ResolveTableLayoutInput): TableLayoutKind;
@@ -1,4 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
+ import { TableMobileMode } from './data-table-layout';
2
3
  export interface DataColumn<T> {
3
4
  /** Stable key for React reconciliation + tests. */
4
5
  readonly key: string;
@@ -33,5 +34,12 @@ export interface DataTableProps<T> {
33
34
  readonly bordered?: boolean;
34
35
  /** Per-row className resolver (e.g. status-coloured backgrounds). */
35
36
  readonly rowClassName?: (row: T, rowIndex: number) => string | undefined;
37
+ /**
38
+ * Narrow-viewport rendering. Default `auto` — cards from
39
+ * `CARD_MODE_MIN_COLUMNS` columns up, rows below it. Pass `scroll` when the
40
+ * point of the table is the side-by-side comparison, `cards` to force the
41
+ * card list at any column count.
42
+ */
43
+ readonly mobileMode?: TableMobileMode;
36
44
  }
37
- export declare function DataTable<T>({ columns, rows, rowKey, onRowClick, minWidthPx, emptyMessage, className, bordered, rowClassName, }: DataTableProps<T>): import("react").JSX.Element | null;
45
+ export declare function DataTable<T>({ columns, rows, rowKey, onRowClick, minWidthPx, emptyMessage, className, bordered, rowClassName, mobileMode, }: DataTableProps<T>): import("react").JSX.Element | null;
@@ -13,7 +13,8 @@
13
13
  * longest). So as the viewport shrinks the HIGHEST-priority-number column
14
14
  * drops first.
15
15
  *
16
- * name (0) — never hidden; sticky left, always visible
16
+ * name (0) — never hidden; pinned left from `md` up (see
17
+ * `NAME_COLUMN_PIN_CLASS`), scrolls below it
17
18
  * previewActions (1) — never hidden; carries the live control + status dot
18
19
  * icon (2) — integration badge; rendered INSIDE the name cell,
19
20
  * not a standalone column, so it has no breakpoint
@@ -44,7 +45,7 @@ export type TableContext = 'devices' | 'integration-detail';
44
45
  export declare function columnsForContext(ctx: TableContext): readonly DeviceColumnId[];
45
46
  /**
46
47
  * Lower number = higher priority (kept longest as width shrinks). `name` = 0
47
- * (sticky, never dropped); `previewActions` = 1 (always visible). The optional
48
+ * (never dropped); `previewActions` = 1 (always visible). The optional
48
49
  * columns drop in REVERSE priority order as width shrinks — highest number
49
50
  * (`type`) hides first, then `features`. `icon` is a name-cell badge, not a
50
51
  * standalone column. See `COLUMN_BREAKPOINT_CLASS` for the derived classes.
@@ -66,3 +67,27 @@ export declare const COLUMN_PRIORITY: Record<DeviceColumnId, number>;
66
67
  * `features` (3) below `md`.
67
68
  */
68
69
  export declare const COLUMN_BREAKPOINT_CLASS: Record<DeviceColumnId, string>;
70
+ /**
71
+ * ── The NAME column pin ───────────────────────────────────────────────────
72
+ *
73
+ * A sticky first column earns its keep only where the remaining columns still
74
+ * fit BESIDE it. Read the drop order above: below `md` every optional column
75
+ * is already folded away, so the table is NAME + Preview and there is nothing
76
+ * left to scroll under the pin — the pinned column simply holds its width
77
+ * permanently and pushes Preview off the right edge. On a phone the viewport
78
+ * is narrower than the pinned column alone, which is the reported defect:
79
+ * "il nome alla sinistra è fixed, dovrebbe scorrere naturalmente."
80
+ *
81
+ * So the pin starts at `md`, the same width at which `features` returns and a
82
+ * column exists to scroll under it. Below `md` the name column scrolls with
83
+ * everything else.
84
+ */
85
+ export declare const NAME_COLUMN_PIN_BREAKPOINT: "md";
86
+ /** Pin classes for the NAME `<th>`/`<td>` — never unconditional. */
87
+ export declare const NAME_COLUMN_PIN_CLASS: "md:sticky md:left-0 md:z-[1]";
88
+ /**
89
+ * NAME column width. Narrower below the pin breakpoint so NAME + Preview fit a
90
+ * phone viewport without a horizontal scroll at all; the roomier desktop width
91
+ * returns with the pin.
92
+ */
93
+ export declare const NAME_COLUMN_WIDTH_CLASS: "w-44 max-w-[11rem] md:w-64 md:max-w-[16rem]";
@@ -27,6 +27,10 @@ export * from './error-box';
27
27
  export * from './confirm-dialog';
28
28
  export * from './stat-card';
29
29
  export * from './key-value-list';
30
+ export { SettingRow } from './setting-row';
31
+ export type { SettingRowElements, SettingRowProps } from './setting-row';
32
+ export { CARD_MODE_MIN_COLUMNS, resolveTableLayout } from './data-table-layout';
33
+ export type { TableLayoutKind, TableMobileMode } from './data-table-layout';
30
34
  export * from './code-block';
31
35
  export * from './filter-bar';
32
36
  export * from './app-shell';
@@ -74,6 +78,8 @@ export type { ProbeState } from './config-form-field';
74
78
  export { DetectionOverlay } from './detection-overlay';
75
79
  export type { DetectionOverlayProps } from './detection-overlay';
76
80
  export { CameraStreamPlayer } from './camera-stream-player';
81
+ export { nextReconnectAction, RECONNECT_POLICY } from './reconnect-schedule';
82
+ export type { ReconnectAction, ReconnectPolicy } from './reconnect-schedule';
77
83
  export type { CameraStreamPlayerProps, PlayerConnectionState, PlayerWebrtcTarget, SignalingResult, ClientOfferResult, ClientStreamHints, SessionSignalingState, } from './camera-stream-player';
78
84
  export { StreamPanel } from './stream-panel';
79
85
  export type { StreamPanelProps, StreamChoice, StreamStats } from './stream-panel';
@@ -6,4 +6,12 @@ export interface KeyValueListProps {
6
6
  }[];
7
7
  className?: string;
8
8
  }
9
+ /**
10
+ * A `<dl>` of label/value rows. Layout (including the L1 stacking) is owned by
11
+ * `SettingRow`; this composite only supplies the description-list semantics.
12
+ *
13
+ * The rows used to be `flex items-center h-7` with a `w-1/3` term, so a label
14
+ * could neither wrap nor stack: on a phone it took a third of the width and
15
+ * the value took whatever was left.
16
+ */
9
17
  export declare function KeyValueList({ items, className }: KeyValueListProps): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Reconnect schedule — the stream player's auto-retry policy as a pure
3
+ * decision function.
4
+ *
5
+ * Why it exists as a module: the inline version had two silent behaviours the
6
+ * operator ran into — exhaustion returned without a trace (the retry loop
7
+ * just stopped, so a camera stayed dark while looking busy), and no attempt
8
+ * was observable anywhere. Attempt in, action out: the component executes the
9
+ * action (timer + callbacks) and can log/report every step; the backoff shape
10
+ * and the exhaustion edge are pinned by tests.
11
+ */
12
+ export interface ReconnectPolicy {
13
+ readonly baseDelayMs: number;
14
+ readonly maxDelayMs: number;
15
+ readonly maxAttempts: number;
16
+ }
17
+ /** Exponential backoff, ±20% jitter, bounded attempts. The bound exists so a
18
+ * permanently-dead stream stops consuming signaling; the EXHAUSTED action is
19
+ * the caller's cue to surface a hard error (never to go silent). */
20
+ export declare const RECONNECT_POLICY: ReconnectPolicy;
21
+ export type ReconnectAction = {
22
+ readonly kind: 'retry';
23
+ readonly delayMs: number;
24
+ } | {
25
+ readonly kind: 'exhausted';
26
+ readonly attempts: number;
27
+ };
28
+ /**
29
+ * Decide what attempt number `attempt` (0-based) should do. `random` is the
30
+ * jitter source (unit interval), injectable for tests.
31
+ */
32
+ export declare function nextReconnectAction(attempt: number, random?: () => number): ReconnectAction;
@@ -0,0 +1,24 @@
1
+ import { ReactNode } from 'react';
2
+ /**
3
+ * Which elements the row emits.
4
+ *
5
+ * - `plain` (default) — `div` / `span`; valid anywhere.
6
+ * - `description` — `dt` / `dd` inside a wrapper `div`, so the row is a
7
+ * valid child of a `<dl>` (`KeyValueList`).
8
+ */
9
+ export type SettingRowElements = 'plain' | 'description';
10
+ export interface SettingRowProps {
11
+ /** Left-hand label. Free to wrap — it is never given a fixed width at L1. */
12
+ readonly label: ReactNode;
13
+ /** The value. Wraps at L1, truncates from the stack breakpoint up. */
14
+ readonly children: ReactNode;
15
+ /** Trailing affordances (copy / reveal), kept beside the value. */
16
+ readonly actions?: ReactNode;
17
+ /** Element flavour — see `SettingRowElements`. Default `plain`. */
18
+ readonly elements?: SettingRowElements;
19
+ /** Extra classes on the outer row. */
20
+ readonly className?: string;
21
+ /** Extra classes on the value text (e.g. `font-mono`). */
22
+ readonly valueClassName?: string;
23
+ }
24
+ export declare function SettingRow({ label, children, actions, elements, className, valueClassName, }: SettingRowProps): ReactNode;
@@ -1399,6 +1399,10 @@ export declare const useSettingsStoreInsert: typeof trpc.settingsStore.insert.us
1399
1399
  export declare const useSettingsStoreUpdate: typeof trpc.settingsStore.update.useMutation;
1400
1400
  /** Generated alias around `trpc.settingsStore.delete.useMutation`. */
1401
1401
  export declare const useSettingsStoreDelete: typeof trpc.settingsStore.delete.useMutation;
1402
+ /** Generated alias around `trpc.settingsStore.deleteWhere.useMutation`. */
1403
+ export declare const useSettingsStoreDeleteWhere: typeof trpc.settingsStore.deleteWhere.useMutation;
1404
+ /** Generated alias around `trpc.settingsStore.updateWhere.useMutation`. */
1405
+ export declare const useSettingsStoreUpdateWhere: typeof trpc.settingsStore.updateWhere.useMutation;
1402
1406
  /** Generated alias around `trpc.settingsStore.count.useQuery`. */
1403
1407
  export declare const useSettingsStoreCount: typeof trpc.settingsStore.count.useQuery;
1404
1408
  /** Generated alias around `trpc.settingsStore.histogram.useQuery`. */
@@ -0,0 +1,77 @@
1
+ /**
2
+ * TurnServerCache — shared STUN/TURN credential cache for the WebRTC connect
3
+ * path.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * The ICE-server fetch is serialized BEFORE `createOffer` on every connect
8
+ * (`camera-stream-player.tsx` → `getIceServers()`), so its latency lands 1:1
9
+ * in the time-to-first-frame. Production `embed:webrtc-timing` lines measured
10
+ * it at 0.2–3.6 s on a cold page. Two verified defects drove that:
11
+ *
12
+ * 1. **No in-flight coalescing.** The mount-time prefetch and the connect's
13
+ * own call raced: whichever started second issued a SECOND full
14
+ * `getTurnServers` round trip instead of awaiting the first.
15
+ * 2. **Per-page cache.** The cache was a `WeakMap` keyed on the page's tRPC
16
+ * client. The native app loads a fresh embed page per camera open, so the
17
+ * common remote path started cold every single time.
18
+ *
19
+ * ## Design
20
+ *
21
+ * Three layers, checked in order:
22
+ * - **memory** (`WeakMap` keyed on the caller's tRPC client — unchanged
23
+ * semantics: two admin tabs pointed at different hubs never cross-serve),
24
+ * - **in-flight** (concurrent callers share one promise),
25
+ * - **storage** (`localStorage` when available) so a fresh page finds warm
26
+ * credentials. Storage is per-origin, and the page's origin IS the hub, so
27
+ * cross-hub leakage is structurally impossible.
28
+ *
29
+ * Freshness: entries younger than `freshTtlMs` are served as-is. Entries older
30
+ * than that but younger than `staleCapMs` are served IMMEDIATELY while a
31
+ * background refresh runs (credentials from the hub's providers are valid for
32
+ * hours — e.g. 24 h for the managed-relay provider — so a bounded stale window
33
+ * is safe; an actually-expired relay credential degrades to the public-STUN
34
+ * fallback exactly like an absent one, and the refresh heals the next
35
+ * connect). Beyond `staleCapMs` the fetch is awaited.
36
+ *
37
+ * Failure: a failed fetch serves the stale entry when one exists, else
38
+ * `undefined` (caller falls back to public STUN). Failures are never cached.
39
+ */
40
+ /** Narrow storage contract — satisfied by `localStorage`, injectable in tests. */
41
+ export interface TurnCacheStorage {
42
+ getItem(key: string): string | null;
43
+ setItem(key: string, value: string): void;
44
+ }
45
+ export interface TurnServerCacheOptions {
46
+ /** Entries younger than this are served without any fetch. */
47
+ readonly freshTtlMs?: number;
48
+ /** Entries older than `freshTtlMs` but younger than this are served
49
+ * immediately while a background refresh runs. */
50
+ readonly staleCapMs?: number;
51
+ /** Storage layer. `null` disables persistence (memory-only). When omitted,
52
+ * `globalThis.localStorage` is used if present. */
53
+ readonly storage?: TurnCacheStorage | null;
54
+ readonly now?: () => number;
55
+ readonly storageKey?: string;
56
+ }
57
+ export declare class TurnServerCache {
58
+ private readonly freshTtlMs;
59
+ private readonly staleCapMs;
60
+ private readonly storage;
61
+ private readonly now;
62
+ private readonly storageKey;
63
+ /** Keyed on the caller's tRPC client object (stable per connected system). */
64
+ private readonly memory;
65
+ /** In-flight fetch per key — concurrent callers share one round trip. */
66
+ private readonly inflight;
67
+ constructor(options?: TurnServerCacheOptions);
68
+ /**
69
+ * Resolve the ICE servers for `key`, fetching via `fetch` only when no
70
+ * fresh-enough entry exists. Never rejects: a failed fetch resolves to the
71
+ * stale entry when one exists, else `undefined`.
72
+ */
73
+ getOrFetch(key: object, fetch: () => Promise<readonly RTCIceServer[]>): Promise<readonly RTCIceServer[] | undefined>;
74
+ private startFetch;
75
+ private readStorage;
76
+ private writeStorage;
77
+ }