@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.
- package/LICENSE +28 -0
- package/README.md +87 -3
- package/dist/App.d.ts +10 -0
- package/dist/components/BarVisualizer.d.ts +7 -0
- package/dist/components/Chooser.d.ts +8 -0
- package/dist/components/Composer.d.ts +13 -0
- package/dist/components/Launcher.d.ts +5 -0
- package/dist/components/MarkShape.d.ts +20 -0
- package/dist/components/MarkupOverlay.d.ts +17 -0
- package/dist/components/MarkupToolbar.d.ts +13 -0
- package/dist/components/RecordOverlay.d.ts +8 -0
- package/dist/components/Result.d.ts +8 -0
- package/dist/components/VoiceNote.d.ts +6 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1315 -0
- package/dist/lib/config.d.ts +14 -0
- package/dist/lib/multiband.d.ts +31 -0
- package/dist/lib/position.d.ts +3 -0
- package/dist/lib/scroll-lock.d.ts +19 -0
- package/dist/lib/shadow-css.d.ts +27 -0
- package/dist/lib/shadow-host.d.ts +12 -0
- package/dist/lib/time.d.ts +2 -0
- package/dist/lib/use-drawing.d.ts +25 -0
- package/dist/lib/use-multiband.d.ts +20 -0
- package/dist/lib/widget-store.d.ts +15 -0
- package/dist/mount.d.ts +18 -0
- package/dist/use-brainbox-widget.d.ts +6 -0
- package/dist/widget-capture.js +141 -0
- package/dist/widget.d.ts +16 -0
- package/dist/widget.js +11 -0
- package/package.json +71 -3
|
@@ -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,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,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;
|
package/dist/mount.d.ts
ADDED
|
@@ -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;
|