@ganeshdole/brainbox-widget 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,14 @@
1
+ import type { TriggerMode } from "@ganeshdole/brainbox-shared";
2
+ import type { Position } from "./position.ts";
3
+ /** What the script tag asked for, read off its `data-*` attributes. */
4
+ export interface WidgetConfig {
5
+ projectKey: string;
6
+ endpoint: string;
7
+ /** `data-mode="mount"` is the manual trigger: the host opens the widget from
8
+ * its own element (`data-mount`) or `window.Brainbox.open()`. */
9
+ trigger: TriggerMode;
10
+ mount?: string;
11
+ position: Position;
12
+ }
13
+ /** Parse the widget config off the embedding <script> tag's data-* attributes. */
14
+ export declare function readConfig(script: HTMLScriptElement | null): WidgetConfig | null;
@@ -0,0 +1,31 @@
1
+ /** Frequency-band loudness maths, adapted from ElevenLabs UI's `bar-visualizer`
2
+ * (https://ui.elevenlabs.io/docs/components/bar-visualizer). Kept pure and
3
+ * separate from the React hook so it is testable without an AudioContext. */
4
+ /** A single FFT bin's dB value as a 0..1 level. The sqrt lifts quiet speech into
5
+ * the visible range - a linear map leaves normal talking barely off the floor. */
6
+ export declare function normalizeDb(value: number): number;
7
+ /**
8
+ * Average `data` into `bands` buckets across the bin range [loPass, hiPass).
9
+ *
10
+ * Splitting the spectrum is what makes the meter read as a voice: each bar
11
+ * tracks its own slice of frequency, so they move independently instead of
12
+ * showing one overall level duplicated across the row.
13
+ */
14
+ export declare function bandLevels(data: Readonly<Float32Array>, bands: number, loPass: number, hiPass: number): number[];
15
+ /** True when any band moved enough to be worth a re-render. Called every frame,
16
+ * so this is what keeps the meter off the render path while the room is quiet. */
17
+ export declare function bandsChanged(next: readonly number[], prev: readonly number[], threshold?: number): boolean;
18
+ /** A 0..1 level as a bar height percentage. */
19
+ export declare function barHeight(level: number): number;
20
+ /**
21
+ * One overall 0..1 level from a band set - recorded per tick to redraw the clip
22
+ * as a static envelope during playback.
23
+ *
24
+ * Peak, not mean: averaging across bands cancels out syllables (loud vowels sit
25
+ * in a couple of bands while the rest stay quiet), which flattens the saved
26
+ * envelope into a near-constant block. The peak keeps that punch.
27
+ */
28
+ export declare function peakLevel(bands: readonly number[]): number;
29
+ /** Average a whole clip's recorded levels into `count` buckets - the envelope
30
+ * shown as a fixed number of bars once recording stops. */
31
+ export declare function downsample(levels: readonly number[], count: number): number[];
@@ -0,0 +1,3 @@
1
+ export type Position = "bottom-right" | "bottom-left" | "top-right" | "top-left";
2
+ /** Tailwind anchor classes for a fixed element pinned to a viewport corner. */
3
+ export declare function posClass(p: Position): string;
@@ -0,0 +1,19 @@
1
+ /** Whether `key` pressed on `target` would scroll the page. In a text field the
2
+ * same keys move the caret, and Space on a button presses it (the toolbar's,
3
+ * for keyboard users), so those are left alone. */
4
+ export declare function isScrollKey(key: string, target: EventTarget | null): boolean;
5
+ /**
6
+ * Hold the host page still while the frozen shot is up.
7
+ *
8
+ * A `position: fixed` overlay covers the page but doesn't stop the wheel, a
9
+ * touch drag or the keyboard from scrolling it underneath. That isn't just
10
+ * cosmetic: marks are drawn in viewport coordinates over the shot, and the
11
+ * element under them (the region's selector) is read from the live page - a
12
+ * page that has scrolled since would name the wrong one.
13
+ *
14
+ * Scrolling is blocked at the input rather than with `overflow: hidden` on the
15
+ * root, which would drop the scrollbar and widen the viewport out from under
16
+ * the shot. A snap-back on `scroll` covers what can't be cancelled, like
17
+ * dragging a classic scrollbar. Returns the unlock.
18
+ */
19
+ export declare function lockScroll(win?: Window): () => void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Prepare the widget's stylesheet for injection into the shadow root.
3
+ *
4
+ * Two things the browser will not do for us inside a shadow tree:
5
+ *
6
+ * 1. `:root` never matches, so the design-token layer has to be retargeted at
7
+ * `:host`.
8
+ *
9
+ * 2. `@property` registration is **document-scoped**. Rules inside a shadow
10
+ * root's `<style>` are parsed and then ignored, so every custom property
11
+ * Tailwind registers there is left with no initial value.
12
+ *
13
+ * That is not cosmetic. Tailwind v4 compiles transforms to
14
+ * `translate: var(--tw-translate-x) var(--tw-translate-y)`; with
15
+ * `--tw-translate-y` unregistered it resolves to the guaranteed-invalid
16
+ * value, which makes the whole declaration invalid at computed-value time
17
+ * and drops it. The result is `translate: none` - every `-translate-x-1/2`
18
+ * in the widget silently did nothing, so anything centred that way sat half
19
+ * its own width to the right.
20
+ *
21
+ * Tailwind emits exactly the right fallback already, but behind an
22
+ * `@supports` that only matches browsers *lacking* `@property`. Chrome has
23
+ * it, so the guard is false - and Chrome still needs the fallback here.
24
+ * Re-declaring each registered property's initial value on a universal
25
+ * selector restores it.
26
+ */
27
+ export declare function shadowCss(css: string): string;
@@ -0,0 +1,12 @@
1
+ /** The widget's foothold in the host page: one element for `<body>` with a
2
+ * shadow root, and inside it the container the React tree renders into. */
3
+ export interface ShadowHost {
4
+ /** The element for the host document. Hidden while screenshotting. */
5
+ element: HTMLElement;
6
+ /** Where to render. Carries the `.dark` class the stylesheet keys off. */
7
+ container: HTMLElement;
8
+ }
9
+ /** Build the shadow host with the widget stylesheet injected (ADR 0001: host
10
+ * CSS cannot reach in, ours cannot leak out). Detached: the caller appends
11
+ * `element` to `<body>` when it is ready to show it. */
12
+ export declare function createShadowHost(css: string): ShadowHost;
@@ -0,0 +1,2 @@
1
+ /** Seconds as `m:ss` - shared by the record overlay and the voice note meter. */
2
+ export declare function fmtDuration(total: number): string;
@@ -0,0 +1,25 @@
1
+ import { type Mark } from "@ganeshdole/brainbox-core";
2
+ /** Tools that are drawn by dragging. `select` and `text` are stateful in ways
3
+ * that belong to the surface using them, so they stay out of here. */
4
+ export type DragTool = "box" | "arrow" | "pen";
5
+ export declare function isDragTool(t: string): t is DragTool;
6
+ export declare const nextMarkId: () => string;
7
+ /**
8
+ * Drag-to-draw, shared by the two surfaces that need it: the markup step and
9
+ * the annotations drawn during a recording. Keeping it in one place is what
10
+ * stops the two from drifting into subtly different feels.
11
+ *
12
+ * The stroke in progress is mutated on a ref and flushed to state once per
13
+ * animation frame. A fast drag fires far more pointer events than the display
14
+ * refreshes, and re-rendering per sample is exactly what makes a drawing
15
+ * surface feel like it's lagging behind the cursor.
16
+ */
17
+ export declare function useDrawing({ color, onCommit, }: {
18
+ color: string;
19
+ onCommit: (m: Mark) => void;
20
+ }): {
21
+ draft: Mark | null;
22
+ begin: (tool: DragTool, e: React.PointerEvent) => void;
23
+ extend: (e: React.PointerEvent) => void;
24
+ finish: () => void;
25
+ };
@@ -0,0 +1,20 @@
1
+ /** Live mic analysis, adapted from ElevenLabs UI's `bar-visualizer`
2
+ * (https://ui.elevenlabs.io/docs/components/bar-visualizer). Changes: the
3
+ * agent-state sequencer and demo mode are dropped (we only ever show a live
4
+ * mic) and the maths moved to multiband.ts so it can be tested. */
5
+ export interface MultibandOptions {
6
+ bands?: number;
7
+ /** FFT bin slice spread across the bars. */
8
+ loPass?: number;
9
+ hiPass?: number;
10
+ /** Sampling throttle - rAF fires far more often than the eye needs. */
11
+ updateInterval?: number;
12
+ fftSize?: number;
13
+ }
14
+ /**
15
+ * Per-band loudness of a live mic stream, 0..1 per band.
16
+ *
17
+ * The meter is decoration: if the browser gives us no AudioContext the hook
18
+ * stays flat and the recording carries on regardless.
19
+ */
20
+ export declare function useMultibandVolume(stream: MediaStream | null, options?: MultibandOptions): number[];
@@ -0,0 +1,15 @@
1
+ import type { Brainbox } from "@ganeshdole/brainbox-core";
2
+ /** Whether the widget UI is open, shared between `<BrainboxWidget />` and
3
+ * whoever wants to open it: a host button, `window.Brainbox.open()`. Core
4
+ * has no idea of "open"; that is a UI matter, so it lives here. */
5
+ export interface WidgetStore {
6
+ isOpen(): boolean;
7
+ subscribe(listener: () => void): () => void;
8
+ open(): void;
9
+ close(): void;
10
+ }
11
+ export declare function createWidgetStore(): WidgetStore;
12
+ /** The store for an instance, created on first use. */
13
+ export declare function widgetStore(brainbox: Brainbox): WidgetStore;
14
+ /** For a tree with no instance yet: never open, and nothing to do. */
15
+ export declare const NO_WIDGET: WidgetStore;
@@ -0,0 +1,18 @@
1
+ import { type BrainboxOptions, type Identity } from "@ganeshdole/brainbox-core";
2
+ import type { TriggerMode } from "@ganeshdole/brainbox-shared";
3
+ import type { Position } from "./lib/position.ts";
4
+ export interface MountOptions extends BrainboxOptions {
5
+ trigger?: TriggerMode;
6
+ position?: Position;
7
+ }
8
+ /** The mounted widget, for a host with no React tree of its own. */
9
+ export interface WidgetHandle {
10
+ open(): void;
11
+ close(): void;
12
+ identify(identity: Identity): void;
13
+ /** Unmount the UI, remove it from the page and destroy the instance. */
14
+ destroy(): void;
15
+ }
16
+ /** Put the widget on the page. This is what the script-tag bundle calls; a
17
+ * React host should render `<BrainboxWidget />` instead. */
18
+ export declare function mount({ trigger, position, ...options }: MountOptions): WidgetHandle;
@@ -0,0 +1,6 @@
1
+ /** Open or close the widget from anywhere under the same provider. */
2
+ export declare function useBrainboxWidget(): {
3
+ isOpen: boolean;
4
+ open(): void;
5
+ close(): void;
6
+ };