@medalsocial/meda 0.1.1 → 0.3.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.
Files changed (73) hide show
  1. package/README.md +7 -2
  2. package/dist/chat/index.d.ts +11 -0
  3. package/dist/chat/index.js +5 -0
  4. package/dist/chat/latency-badge.d.ts +7 -0
  5. package/dist/chat/latency-badge.js +19 -0
  6. package/dist/chat/latency-breakdown.d.ts +8 -0
  7. package/dist/chat/latency-breakdown.js +9 -0
  8. package/dist/chat/public.d.ts +1 -0
  9. package/dist/chat/public.js +1 -0
  10. package/dist/chat/tool-call-block.d.ts +6 -0
  11. package/dist/chat/tool-call-block.js +19 -0
  12. package/dist/chat/transcript-stream.d.ts +10 -0
  13. package/dist/chat/transcript-stream.js +40 -0
  14. package/dist/chat/turn-card.d.ts +10 -0
  15. package/dist/chat/turn-card.js +23 -0
  16. package/dist/chat/types.d.ts +36 -0
  17. package/dist/chat/types.js +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +3 -0
  20. package/dist/lib/format-time.d.ts +9 -0
  21. package/dist/lib/format-time.js +34 -0
  22. package/dist/panel/index.d.ts +6 -0
  23. package/dist/panel/index.js +3 -0
  24. package/dist/panel/inspector-field.d.ts +9 -0
  25. package/dist/panel/inspector-field.js +4 -0
  26. package/dist/panel/inspector-json.d.ts +7 -0
  27. package/dist/panel/inspector-json.js +73 -0
  28. package/dist/panel/inspector.d.ts +7 -0
  29. package/dist/panel/inspector.js +12 -0
  30. package/dist/panel/public.d.ts +1 -0
  31. package/dist/panel/public.js +1 -0
  32. package/dist/panel/types.d.ts +6 -0
  33. package/dist/panel/types.js +1 -0
  34. package/dist/styles.css +22 -0
  35. package/dist/timeline/date-switcher.d.ts +11 -0
  36. package/dist/timeline/date-switcher.js +62 -0
  37. package/dist/timeline/event-card.d.ts +9 -0
  38. package/dist/timeline/event-card.js +26 -0
  39. package/dist/timeline/index.d.ts +13 -0
  40. package/dist/timeline/index.js +6 -0
  41. package/dist/timeline/live-indicator.d.ts +7 -0
  42. package/dist/timeline/live-indicator.js +5 -0
  43. package/dist/timeline/public.d.ts +1 -0
  44. package/dist/timeline/public.js +1 -0
  45. package/dist/timeline/scrub-bar.d.ts +14 -0
  46. package/dist/timeline/scrub-bar.js +44 -0
  47. package/dist/timeline/timeline-rail.d.ts +19 -0
  48. package/dist/timeline/timeline-rail.js +64 -0
  49. package/dist/timeline/timeline-tape.d.ts +19 -0
  50. package/dist/timeline/timeline-tape.js +66 -0
  51. package/dist/timeline/types.d.ts +40 -0
  52. package/dist/timeline/types.js +1 -0
  53. package/dist/voice/index.d.ts +9 -0
  54. package/dist/voice/index.js +4 -0
  55. package/dist/voice/public.d.ts +1 -0
  56. package/dist/voice/public.js +1 -0
  57. package/dist/voice/types.d.ts +8 -0
  58. package/dist/voice/types.js +1 -0
  59. package/dist/voice/use-mic-capture.d.ts +14 -0
  60. package/dist/voice/use-mic-capture.js +185 -0
  61. package/dist/voice/voice-level.d.ts +8 -0
  62. package/dist/voice/voice-level.js +29 -0
  63. package/dist/voice/voice-orb-scene.d.ts +12 -0
  64. package/dist/voice/voice-orb-scene.js +121 -0
  65. package/dist/voice/voice-orb-shader.d.ts +2 -0
  66. package/dist/voice/voice-orb-shader.js +255 -0
  67. package/dist/voice/voice-orb-volume.d.ts +12 -0
  68. package/dist/voice/voice-orb-volume.js +21 -0
  69. package/dist/voice/voice-orb.d.ts +45 -0
  70. package/dist/voice/voice-orb.js +123 -0
  71. package/dist/voice/voice-status-pill.d.ts +7 -0
  72. package/dist/voice/voice-status-pill.js +26 -0
  73. package/package.json +36 -3
package/README.md CHANGED
@@ -56,12 +56,17 @@ See the [demo app](./demo) for a live playground.
56
56
 
57
57
  ## Alternative: shadcn registry
58
58
 
59
- Prefer to copy source into your project instead of installing? The shadcn-compatible registry lives at [`./registry`](./registry). Serve it statically and run:
59
+ Prefer to copy source into your project instead of installing? The shadcn-compatible registry is served alongside the demo playground on Cloudflare Workers.
60
60
 
61
61
  ```bash
62
- npx shadcn add <registry-url>/r/meda-shell.json
62
+ # Once DNS for meda.medalsocial.com is live:
63
+ npx shadcn add https://meda.medalsocial.com/r/meda-shell.json
64
+ npx shadcn add https://meda.medalsocial.com/r/meda-shell-state.json
65
+ npx shadcn add https://meda.medalsocial.com/r/meda-workbench-layout.json
63
66
  ```
64
67
 
68
+ The registry index is at `https://meda.medalsocial.com/registry.json`. Source JSON files live under [`./registry`](./registry) in this repo and are deployed as static assets via Cloudflare Workers — see `wrangler.toml` and `.github/workflows/deploy-worker.yml`.
69
+
65
70
  ## Development
66
71
 
67
72
  ```bash
@@ -0,0 +1,11 @@
1
+ export type { LatencyBadgeProps, LatencyKind } from './latency-badge.js';
2
+ export { LatencyBadge } from './latency-badge.js';
3
+ export type { LatencyBreakdownProps } from './latency-breakdown.js';
4
+ export { LatencyBreakdown } from './latency-breakdown.js';
5
+ export type { ToolCallBlockProps } from './tool-call-block.js';
6
+ export { ToolCallBlock } from './tool-call-block.js';
7
+ export type { TranscriptStreamProps } from './transcript-stream.js';
8
+ export { TranscriptStream } from './transcript-stream.js';
9
+ export type { TurnCardProps } from './turn-card.js';
10
+ export { TurnCard } from './turn-card.js';
11
+ export type { Speaker, ToolCall, Turn, TurnLatency } from './types.js';
@@ -0,0 +1,5 @@
1
+ export { LatencyBadge } from './latency-badge.js';
2
+ export { LatencyBreakdown } from './latency-breakdown.js';
3
+ export { ToolCallBlock } from './tool-call-block.js';
4
+ export { TranscriptStream } from './transcript-stream.js';
5
+ export { TurnCard } from './turn-card.js';
@@ -0,0 +1,7 @@
1
+ export type LatencyKind = 'stt' | 'claude' | 'tts';
2
+ export interface LatencyBadgeProps {
3
+ kind: LatencyKind;
4
+ ms: number;
5
+ className?: string;
6
+ }
7
+ export declare function LatencyBadge({ kind, ms, className }: LatencyBadgeProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const KIND = {
3
+ stt: { label: 'STT', tone: 'text-sky-400' },
4
+ claude: { label: 'Claude', tone: 'text-primary' },
5
+ tts: { label: 'TTS', tone: 'text-emerald-500' },
6
+ };
7
+ function fmt(ms) {
8
+ if (ms >= 1000)
9
+ return `${(ms / 1000).toFixed(1)}s`;
10
+ return `${Math.round(ms)}ms`;
11
+ }
12
+ export function LatencyBadge({ kind, ms, className }) {
13
+ const meta = KIND[kind];
14
+ return (_jsxs("span", { "data-kind": kind, className: [
15
+ 'inline-flex items-center gap-1.5 text-[11px] tabular-nums',
16
+ meta.tone,
17
+ className ?? '',
18
+ ].join(' '), children: [_jsx("span", { "aria-hidden": "true", className: "size-1 rounded-full bg-current" }), meta.label, " ", fmt(ms)] }));
19
+ }
@@ -0,0 +1,8 @@
1
+ export interface LatencyBreakdownProps {
2
+ sttMs: number;
3
+ claudeMs: number;
4
+ ttsMs: number;
5
+ showLegend?: boolean;
6
+ className?: string;
7
+ }
8
+ export declare function LatencyBreakdown({ sttMs, claudeMs, ttsMs, showLegend, className, }: LatencyBreakdownProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { LatencyBadge } from './latency-badge.js';
3
+ export function LatencyBreakdown({ sttMs, claudeMs, ttsMs, showLegend = true, className, }) {
4
+ const total = Math.max(1, sttMs + claudeMs + ttsMs);
5
+ const sttPct = (sttMs / total) * 100;
6
+ const claudePct = (claudeMs / total) * 100;
7
+ const ttsPct = (ttsMs / total) * 100;
8
+ return (_jsxs("div", { className: className, children: [_jsxs("div", { className: "flex h-1.5 gap-0.5 overflow-hidden rounded-sm bg-muted", children: [_jsx("span", { "data-segment": "stt", className: "block bg-sky-400", style: { width: `${sttPct}%` } }), _jsx("span", { "data-segment": "claude", className: "block bg-primary", style: { width: `${claudePct}%` } }), _jsx("span", { "data-segment": "tts", className: "block bg-emerald-500", style: { width: `${ttsPct}%` } })] }), showLegend && (_jsxs("div", { className: "mt-1.5 flex flex-wrap gap-3 text-[11px] tabular-nums text-muted-foreground", children: [_jsx(LatencyBadge, { kind: "stt", ms: sttMs }), _jsx(LatencyBadge, { kind: "claude", ms: claudeMs }), _jsx(LatencyBadge, { kind: "tts", ms: ttsMs })] }))] }));
9
+ }
@@ -0,0 +1 @@
1
+ export type { Speaker, ToolCall, Turn, TurnLatency } from './types.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { ToolCall } from './types.js';
2
+ export interface ToolCallBlockProps {
3
+ call: ToolCall;
4
+ className?: string;
5
+ }
6
+ export declare function ToolCallBlock({ call, className }: ToolCallBlockProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Zap } from 'lucide-react';
3
+ function safeStringify(v) {
4
+ // Tolerate values JSON.stringify can't handle (BigInt, circular refs, etc.)
5
+ // so a malformed tool arg never crashes the transcript view.
6
+ try {
7
+ const out = JSON.stringify(v);
8
+ return out === undefined ? String(v) : out;
9
+ }
10
+ catch {
11
+ return String(v);
12
+ }
13
+ }
14
+ export function ToolCallBlock({ call, className }) {
15
+ return (_jsxs("div", { className: [
16
+ 'mt-3 rounded-md border border-border border-l-2 border-l-amber-500 bg-card p-3 font-mono text-xs',
17
+ className ?? '',
18
+ ].join(' '), children: [_jsxs("div", { className: "mb-1.5 flex items-center gap-2 text-[11px]", children: [_jsx(Zap, { className: "size-3 text-amber-500" }), _jsx("strong", { className: "font-semibold text-foreground", children: call.name }), call.latencyMs != null && (_jsxs("span", { className: "ml-auto tabular-nums text-muted-foreground", children: [call.latencyMs, "ms"] }))] }), _jsxs("div", { className: "text-muted-foreground", children: [Object.entries(call.args && typeof call.args === 'object' ? call.args : {}).map(([k, v], i) => (_jsxs("span", { children: [i > 0 && ', ', _jsx("span", { className: "text-primary", children: k }), ": ", '', _jsx("span", { className: "text-emerald-500", children: safeStringify(v) })] }, k))), call.resultSummary && (_jsxs("span", { className: "text-muted-foreground", children: [" \u2192 ", call.resultSummary] }))] })] }));
19
+ }
@@ -0,0 +1,10 @@
1
+ import type { Turn } from './types.js';
2
+ export interface TranscriptStreamProps {
3
+ turns: Turn[];
4
+ /** When true (default), auto-scroll to bottom on new turn IF user is already near bottom. */
5
+ autoScroll?: boolean;
6
+ tz?: string;
7
+ onTurnPlay?: (turnId: string) => void;
8
+ className?: string;
9
+ }
10
+ export declare function TranscriptStream({ turns, autoScroll, tz, onTurnPlay, className, }: TranscriptStreamProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { TurnCard } from './turn-card.js';
4
+ const SCROLL_THRESHOLD = 120;
5
+ export function TranscriptStream({ turns, autoScroll = true, tz, onTurnPlay, className, }) {
6
+ const ref = useRef(null);
7
+ // Snapshot of "was the user near the bottom?" sampled BEFORE the new turn
8
+ // renders, so that a tall appended turn doesn't push the bottom off-screen
9
+ // and falsely look like the user scrolled away.
10
+ const wasAtBottomRef = useRef(true);
11
+ useEffect(() => {
12
+ const el = ref.current;
13
+ if (!el)
14
+ return;
15
+ const onScroll = () => {
16
+ const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
17
+ wasAtBottomRef.current = distanceFromBottom < SCROLL_THRESHOLD;
18
+ };
19
+ el.addEventListener('scroll', onScroll, { passive: true });
20
+ return () => el.removeEventListener('scroll', onScroll);
21
+ }, []);
22
+ // Auto-follow last turn when the user was near the bottom before the new turn appended.
23
+ // biome-ignore lint/correctness/useExhaustiveDependencies: turns changes trigger scroll check
24
+ useEffect(() => {
25
+ const el = ref.current;
26
+ if (!el || !autoScroll)
27
+ return;
28
+ if (wasAtBottomRef.current) {
29
+ el.scrollTop = el.scrollHeight;
30
+ }
31
+ }, [turns, autoScroll]);
32
+ if (turns.length === 0) {
33
+ return (_jsx("div", { ref: ref, className: [
34
+ 'flex flex-1 items-center justify-center text-sm text-muted-foreground',
35
+ className ?? '',
36
+ ].join(' '), children: "No turns yet." }));
37
+ }
38
+ const ref0 = turns[0]?.startedAt ?? 0;
39
+ return (_jsx("div", { ref: ref, className: ['flex-1 overflow-y-auto px-7 pb-6 pt-2', className ?? ''].join(' '), children: turns.map((t) => (_jsx(TurnCard, { turn: t, startedAtRef: ref0, tz: tz, onPlay: onTurnPlay }, t.id))) }));
40
+ }
@@ -0,0 +1,10 @@
1
+ import type { Turn } from './types.js';
2
+ export interface TurnCardProps {
3
+ turn: Turn;
4
+ /** Reference instant for the relative offset column (typically the conversation start). */
5
+ startedAtRef: number;
6
+ tz?: string;
7
+ onPlay?: (turnId: string) => void;
8
+ className?: string;
9
+ }
10
+ export declare function TurnCard({ turn, startedAtRef, tz, onPlay, className }: TurnCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Play } from 'lucide-react';
3
+ import { formatClock, formatRelativeOffset } from '../lib/format-time.js';
4
+ import { LatencyBadge } from './latency-badge.js';
5
+ import { ToolCallBlock } from './tool-call-block.js';
6
+ const SPEAKER_LABEL = {
7
+ user: 'You',
8
+ assistant: 'Assistant',
9
+ system: 'System',
10
+ };
11
+ export function TurnCard({ turn, startedAtRef, tz, onPlay, className }) {
12
+ const label = turn.speakerLabel ?? SPEAKER_LABEL[turn.speaker];
13
+ const offset = turn.startedAt - startedAtRef;
14
+ const speakerColor = turn.speaker === 'assistant'
15
+ ? 'text-primary'
16
+ : turn.speaker === 'system'
17
+ ? 'text-amber-500'
18
+ : 'text-foreground';
19
+ return (_jsxs("div", { className: [
20
+ 'grid grid-cols-[64px_1fr] gap-5 border-b border-border py-4 last:border-b-0',
21
+ className ?? '',
22
+ ].join(' '), children: [_jsxs("div", { className: "pt-1 text-[11px] tabular-nums text-muted-foreground", children: [_jsx("strong", { className: "mb-0.5 block text-[12px] font-medium text-muted-foreground", children: formatClock(new Date(turn.startedAt), { tz }) }), formatRelativeOffset(offset)] }), _jsxs("div", { children: [_jsxs("div", { className: "mb-1.5 flex items-center gap-2", children: [_jsx("span", { className: ['text-[11px] font-semibold uppercase tracking-wider', speakerColor].join(' '), children: label }), turn.modelLabel && (_jsx("span", { className: "rounded bg-muted px-1.5 py-px text-[10px] font-medium text-muted-foreground", children: turn.modelLabel })), turn.spokenSeconds != null && turn.speaker === 'user' && (_jsxs("span", { className: "rounded bg-muted px-1.5 py-px text-[10px] font-medium text-muted-foreground", children: ["spoken \u00B7 ", turn.spokenSeconds.toFixed(1), "s"] }))] }), _jsxs("p", { className: "text-[15px] leading-relaxed text-foreground", "data-streaming": String(turn.streaming ?? false), children: [turn.text, turn.streaming && (_jsx("span", { "aria-hidden": "true", className: "ml-0.5 animate-pulse text-primary", children: "\u258D" }))] }), turn.toolCalls?.map((c) => (_jsx(ToolCallBlock, { call: c }, c.id))), (turn.audioUrl || turn.latency) && (_jsxs("div", { className: "mt-2.5 flex flex-wrap items-center gap-4 text-[11px] text-muted-foreground", children: [turn.audioUrl && (_jsx("button", { type: "button", "aria-label": "Play this turn", onClick: () => onPlay?.(turn.id), className: "inline-flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground hover:text-foreground", children: _jsx(Play, { className: "size-3" }) })), turn.latency?.sttMs != null && _jsx(LatencyBadge, { kind: "stt", ms: turn.latency.sttMs }), turn.latency?.claudeMs != null && (_jsx(LatencyBadge, { kind: "claude", ms: turn.latency.claudeMs })), turn.latency?.ttsMs != null && _jsx(LatencyBadge, { kind: "tts", ms: turn.latency.ttsMs })] }))] })] }));
23
+ }
@@ -0,0 +1,36 @@
1
+ export type Speaker = 'user' | 'assistant' | 'system';
2
+ /** Per-stage latency breakdown for one turn. */
3
+ export interface TurnLatency {
4
+ sttMs?: number;
5
+ claudeMs?: number;
6
+ ttsMs?: number;
7
+ }
8
+ /** A single AI tool / function-call invocation. */
9
+ export interface ToolCall {
10
+ id: string;
11
+ name: string;
12
+ args: Record<string, unknown>;
13
+ /** Short human-readable summary, e.g. "3 events". */
14
+ resultSummary?: string;
15
+ latencyMs?: number;
16
+ }
17
+ /** One conversational exchange. */
18
+ export interface Turn {
19
+ id: string;
20
+ speaker: Speaker;
21
+ /** Display label, e.g. "Morpheus" or "Operator (Ali)". Optional override of speaker. */
22
+ speakerLabel?: string;
23
+ /** Optional model identifier, e.g. "claude-opus-4-7". */
24
+ modelLabel?: string;
25
+ /** URL or blob: URL for per-turn replay. */
26
+ audioUrl?: string;
27
+ /** Spoken duration if known. */
28
+ spokenSeconds?: number;
29
+ text: string;
30
+ /** Unix ms. */
31
+ startedAt: number;
32
+ /** True if the assistant is still streaming this turn. */
33
+ streaming?: boolean;
34
+ latency?: TurnLatency;
35
+ toolCalls?: ToolCall[];
36
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1 +1,4 @@
1
+ export * from './chat/public.js';
2
+ export * from './panel/public.js';
1
3
  export * from './shell/public.js';
4
+ export * from './timeline/public.js';
package/dist/index.js CHANGED
@@ -1 +1,4 @@
1
+ export * from './chat/public.js';
2
+ export * from './panel/public.js';
1
3
  export * from './shell/public.js';
4
+ export * from './timeline/public.js';
@@ -0,0 +1,9 @@
1
+ export interface FormatClockOptions {
2
+ /** IANA timezone, e.g. "Europe/Stockholm". Defaults to local tz. */
3
+ tz?: string;
4
+ /** Include seconds (default true). */
5
+ withSeconds?: boolean;
6
+ }
7
+ export declare function formatClock(date: Date, opts?: FormatClockOptions): string;
8
+ export declare function formatDuration(ms: number): string;
9
+ export declare function formatRelativeOffset(ms: number): string;
@@ -0,0 +1,34 @@
1
+ export function formatClock(date, opts = {}) {
2
+ const { tz, withSeconds = true } = opts;
3
+ const fmt = new Intl.DateTimeFormat('en-GB', {
4
+ hour: '2-digit',
5
+ minute: '2-digit',
6
+ second: withSeconds ? '2-digit' : undefined,
7
+ hour12: false,
8
+ timeZone: tz,
9
+ });
10
+ return fmt.format(date);
11
+ }
12
+ export function formatDuration(ms) {
13
+ if (ms < 0)
14
+ ms = 0;
15
+ const totalSec = Math.floor(ms / 1000);
16
+ const h = Math.floor(totalSec / 3600);
17
+ const m = Math.floor((totalSec % 3600) / 60);
18
+ const s = totalSec % 60;
19
+ if (h > 0)
20
+ return `${h}h ${m}m`;
21
+ if (m > 0)
22
+ return `${m}m ${s}s`;
23
+ return `${s}s`;
24
+ }
25
+ export function formatRelativeOffset(ms) {
26
+ const sign = ms < 0 ? '-' : '+';
27
+ const abs = Math.abs(ms);
28
+ const totalSec = Math.floor(abs / 1000);
29
+ const m = Math.floor(totalSec / 60);
30
+ const s = totalSec % 60;
31
+ if (m === 0)
32
+ return `${sign}${s}s`;
33
+ return `${sign}${m}m ${s}s`;
34
+ }
@@ -0,0 +1,6 @@
1
+ export type { InspectorProps } from './inspector.js';
2
+ export { Inspector } from './inspector.js';
3
+ export type { InspectorFieldProps } from './inspector-field.js';
4
+ export { InspectorField } from './inspector-field.js';
5
+ export type { InspectorJSONProps } from './inspector-json.js';
6
+ export { InspectorJSON } from './inspector-json.js';
@@ -0,0 +1,3 @@
1
+ export { Inspector } from './inspector.js';
2
+ export { InspectorField } from './inspector-field.js';
3
+ export { InspectorJSON } from './inspector-json.js';
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface InspectorFieldProps {
3
+ label: string;
4
+ value: ReactNode;
5
+ /** Optional smaller hint after the value, e.g. provider info. */
6
+ hint?: ReactNode;
7
+ className?: string;
8
+ }
9
+ export declare function InspectorField({ label, value, hint, className }: InspectorFieldProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function InspectorField({ label, value, hint, className }) {
3
+ return (_jsxs("div", { className: ['border-b border-border py-3 last:border-b-0', className ?? ''].join(' '), children: [_jsx("div", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground", children: label }), _jsxs("div", { className: "text-[13px] tabular-nums text-foreground", children: [value, hint != null && _jsxs("span", { className: "text-[12px] text-muted-foreground", children: [" ", hint] })] })] }));
4
+ }
@@ -0,0 +1,7 @@
1
+ export interface InspectorJSONProps {
2
+ data: unknown;
3
+ /** Indent size in spaces. */
4
+ indent?: number;
5
+ className?: string;
6
+ }
7
+ export declare function InspectorJSON({ data, indent, className }: InspectorJSONProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ function tokenize(value, level, key, indent, seen) {
3
+ const out = [];
4
+ const pad = ' '.repeat(level * indent);
5
+ if (key !== null) {
6
+ out.push({ kind: 'punct', text: pad });
7
+ out.push({ kind: 'key', text: `"${key}"` });
8
+ out.push({ kind: 'punct', text: ': ' });
9
+ }
10
+ else {
11
+ out.push({ kind: 'punct', text: pad });
12
+ }
13
+ if (value === null) {
14
+ out.push({ kind: 'literal', text: 'null' });
15
+ }
16
+ else if (typeof value === 'string') {
17
+ out.push({ kind: 'string', text: JSON.stringify(value) });
18
+ }
19
+ else if (typeof value === 'number') {
20
+ out.push({ kind: 'number', text: String(value) });
21
+ }
22
+ else if (typeof value === 'boolean') {
23
+ out.push({ kind: 'literal', text: String(value) });
24
+ }
25
+ else if (Array.isArray(value)) {
26
+ if (seen.has(value)) {
27
+ out.push({ kind: 'literal', text: '"[Circular]"' });
28
+ }
29
+ else {
30
+ seen.add(value);
31
+ out.push({ kind: 'punct', text: '[\n' });
32
+ value.forEach((v, i) => {
33
+ out.push(...tokenize(v, level + 1, null, indent, seen));
34
+ out.push({ kind: 'punct', text: i < value.length - 1 ? ',\n' : '\n' });
35
+ });
36
+ out.push({ kind: 'punct', text: `${pad}]` });
37
+ }
38
+ }
39
+ else if (typeof value === 'object') {
40
+ const obj = value;
41
+ if (seen.has(obj)) {
42
+ out.push({ kind: 'literal', text: '"[Circular]"' });
43
+ }
44
+ else {
45
+ seen.add(obj);
46
+ out.push({ kind: 'punct', text: '{\n' });
47
+ const entries = Object.entries(obj);
48
+ entries.forEach(([k, v], i) => {
49
+ out.push(...tokenize(v, level + 1, k, indent, seen));
50
+ out.push({ kind: 'punct', text: i < entries.length - 1 ? ',\n' : '\n' });
51
+ });
52
+ out.push({ kind: 'punct', text: `${pad}}` });
53
+ }
54
+ }
55
+ else {
56
+ out.push({ kind: 'literal', text: String(value) });
57
+ }
58
+ return out;
59
+ }
60
+ const KIND_CLASS = {
61
+ punct: 'text-muted-foreground',
62
+ key: 'text-primary',
63
+ string: 'text-emerald-500',
64
+ number: 'text-amber-500',
65
+ literal: 'text-amber-500',
66
+ };
67
+ export function InspectorJSON({ data, indent = 2, className }) {
68
+ const tokens = tokenize(data, 0, null, indent, new WeakSet());
69
+ return (_jsx("pre", { className: [
70
+ 'mt-1.5 overflow-x-auto rounded-md border border-border bg-background p-2.5 font-mono text-[11px] leading-relaxed text-foreground',
71
+ className ?? '',
72
+ ].join(' '), children: tokens.map((t, i) => (_jsx("span", { className: KIND_CLASS[t.kind], children: t.text }, i))) }));
73
+ }
@@ -0,0 +1,7 @@
1
+ import type { InspectorTab } from './types.js';
2
+ export interface InspectorProps {
3
+ tabs: InspectorTab[];
4
+ defaultTab?: string;
5
+ className?: string;
6
+ }
7
+ export declare function Inspector({ tabs, defaultTab, className }: InspectorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ export function Inspector({ tabs, defaultTab, className }) {
4
+ const [active, setActive] = useState(defaultTab ?? tabs[0]?.id ?? '');
5
+ const current = tabs.find((t) => t.id === active) ?? tabs[0];
6
+ return (_jsxs("aside", { className: ['flex h-full flex-col border-l border-border bg-card', className ?? ''].join(' '), children: [_jsx("div", { role: "tablist", className: "flex gap-0.5 border-b border-border px-3.5", children: tabs.map((t) => (_jsx("button", { type: "button", role: "tab", "aria-selected": t.id === active, onClick: () => setActive(t.id), className: [
7
+ 'border-b-2 px-2.5 py-3 text-xs',
8
+ t.id === active
9
+ ? 'border-primary text-foreground'
10
+ : 'border-transparent text-muted-foreground hover:text-foreground',
11
+ ].join(' '), children: t.label }, t.id))) }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 pb-8 pt-4", children: current?.content })] }));
12
+ }
@@ -0,0 +1 @@
1
+ export type { InspectorTab } from './types.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface InspectorTab {
3
+ id: string;
4
+ label: string;
5
+ content: ReactNode;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/styles.css CHANGED
@@ -2,3 +2,25 @@
2
2
  --meda-shell-panel-dock-radius: var(--radius-4xl, 1.5rem);
3
3
  --meda-shell-panel-dock-shadow: var(--shadow-panel-elevated, 0 32px 90px rgb(0 0 0 / 0.45));
4
4
  }
5
+
6
+ /* VoiceOrb (WebGL): minimal container styles — visual identity lives in the GLSL shader. */
7
+ .meda-voice-orb {
8
+ background: transparent;
9
+ border: 0;
10
+ padding: 0;
11
+ isolation: isolate;
12
+ overflow: hidden;
13
+ border-radius: 9999px;
14
+ cursor: pointer;
15
+ position: relative;
16
+ }
17
+ .meda-voice-orb[disabled] {
18
+ cursor: not-allowed;
19
+ opacity: 0.6;
20
+ }
21
+ .meda-voice-orb canvas {
22
+ display: block;
23
+ width: 100% !important;
24
+ height: 100% !important;
25
+ pointer-events: none;
26
+ }
@@ -0,0 +1,11 @@
1
+ export interface DateSwitcherProps {
2
+ /** Currently selected date. */
3
+ value: Date;
4
+ /** Reference for "today" comparison and forward-disable. */
5
+ now: Date;
6
+ /** IANA timezone for "same day" / display semantics. Defaults to local. */
7
+ tz?: string;
8
+ onChange: (next: Date) => void;
9
+ className?: string;
10
+ }
11
+ export declare function DateSwitcher({ value, now, tz, onChange, className }: DateSwitcherProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
3
+ // "YYYY-MM-DD" in the given tz (or local if undefined). Comparing these
4
+ // strings is a robust same-day check that doesn't depend on local-time math.
5
+ function ymdInTz(d, tz) {
6
+ return new Intl.DateTimeFormat('en-CA', {
7
+ year: 'numeric',
8
+ month: '2-digit',
9
+ day: '2-digit',
10
+ timeZone: tz,
11
+ }).format(d);
12
+ }
13
+ function isSameDay(a, b, tz) {
14
+ return ymdInTz(a, tz) === ymdInTz(b, tz);
15
+ }
16
+ // Returns the tz offset (in minutes) for `date` in `tz`. Positive = east of
17
+ // UTC, e.g. Europe/Stockholm CET → +60. Tolerates "GMT", "GMT+05", and
18
+ // "GMT+05:30" formats from Intl.DateTimeFormat.
19
+ function tzOffsetMinutes(date, tz) {
20
+ const parts = new Intl.DateTimeFormat('en-US', {
21
+ timeZone: tz,
22
+ timeZoneName: 'longOffset',
23
+ }).formatToParts(date);
24
+ const name = parts.find((p) => p.type === 'timeZoneName')?.value ?? 'GMT';
25
+ const m = /GMT(?:([+-])(\d{1,2})(?::?(\d{2}))?)?/.exec(name);
26
+ if (!m?.[1])
27
+ return 0;
28
+ const sign = m[1] === '+' ? 1 : -1;
29
+ const hours = Number.parseInt(m[2] ?? '0', 10);
30
+ const mins = Number.parseInt(m[3] ?? '0', 10);
31
+ return sign * (hours * 60 + mins);
32
+ }
33
+ function addDays(d, delta, tz) {
34
+ if (!tz) {
35
+ // No tz: step the calendar day in the browser's local timezone (DST-safe
36
+ // there because setDate honors local DST transitions).
37
+ const c = new Date(d);
38
+ c.setDate(c.getDate() + delta);
39
+ return c;
40
+ }
41
+ // tz-aware: advance 24h, then correct for any DST shift between the
42
+ // original day and the target day so the *wall clock* in `tz` lands on the
43
+ // same hour:minute on the next/previous calendar day.
44
+ const offsetBefore = tzOffsetMinutes(d, tz);
45
+ const candidate = new Date(d.getTime() + delta * 86_400_000);
46
+ const offsetAfter = tzOffsetMinutes(candidate, tz);
47
+ return new Date(candidate.getTime() + (offsetBefore - offsetAfter) * 60_000);
48
+ }
49
+ function fmtDate(d, tz) {
50
+ return new Intl.DateTimeFormat('en-US', {
51
+ day: 'numeric',
52
+ month: 'short',
53
+ year: 'numeric',
54
+ timeZone: tz,
55
+ }).format(d);
56
+ }
57
+ export function DateSwitcher({ value, now, tz, onChange, className }) {
58
+ const isToday = isSameDay(value, now, tz);
59
+ const goPrev = () => onChange(addDays(value, -1, tz));
60
+ const goNext = () => onChange(addDays(value, 1, tz));
61
+ return (_jsxs("div", { className: ['flex items-center gap-2', className ?? ''].join(' '), children: [_jsx("button", { type: "button", "aria-label": "Previous day", onClick: goPrev, className: "inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground", children: _jsx(ChevronLeft, { className: "size-4" }) }), _jsxs("div", { className: "flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-sm", children: [_jsx(Calendar, { className: "size-3.5 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: fmtDate(value, tz) }), isToday && (_jsx("span", { className: "ml-1 rounded-full bg-emerald-500/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-emerald-500", children: "today" }))] }), _jsx("button", { type: "button", "aria-label": "Next day", disabled: isToday, onClick: goNext, className: "inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent", children: _jsx(ChevronRight, { className: "size-4" }) })] }));
62
+ }
@@ -0,0 +1,9 @@
1
+ import type { TimelineEvent } from './types.js';
2
+ export type EventCardSize = 'default' | 'tiny';
3
+ export interface EventCardProps {
4
+ event: TimelineEvent;
5
+ size?: EventCardSize;
6
+ onSelect?: (event: TimelineEvent) => void;
7
+ className?: string;
8
+ }
9
+ export declare function EventCard({ event, size, onSelect, className }: EventCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function EventCard({ event, size = 'default', onSelect, className }) {
3
+ const isLive = event.isLive ?? false;
4
+ const isSelected = event.selected ?? false;
5
+ const isError = event.kind === 'error';
6
+ const liveClasses = isLive
7
+ ? 'border-emerald-500/40 bg-gradient-to-b from-emerald-500/10 to-card'
8
+ : '';
9
+ const errorClasses = isError ? 'border-destructive/40' : '';
10
+ const selectedClasses = isSelected
11
+ ? 'border-primary/60 bg-primary/[0.10] shadow-[0_4px_12px_rgba(0,0,0,0.15)]'
12
+ : '';
13
+ const sizeClasses = size === 'tiny' ? 'px-2 py-1 text-[11px]' : 'px-2.5 py-2 text-xs';
14
+ return (_jsxs("button", { type: "button", "data-live": String(isLive), "data-selected": String(isSelected), "data-kind": event.kind, onClick: () => onSelect?.(event), className: [
15
+ 'relative w-full cursor-pointer rounded-md border border-border bg-card text-left transition-[border-color,transform] duration-100',
16
+ 'hover:translate-x-px hover:border-primary/40',
17
+ sizeClasses,
18
+ liveClasses,
19
+ errorClasses,
20
+ selectedClasses,
21
+ className ?? '',
22
+ ].join(' '), children: [_jsx("span", { "aria-hidden": "true", className: "absolute -left-2.5 top-1/2 h-px w-2.5 bg-border" }), _jsxs("span", { className: "flex items-center gap-1.5 font-medium", children: [isLive && _jsx(LiveDot, {}), _jsx("span", { className: "min-w-0 flex-1 truncate", children: event.primary })] }), size === 'default' && event.secondary != null && (_jsx("span", { className: "mt-0.5 block text-[10px] text-muted-foreground tabular-nums", children: event.secondary }))] }));
23
+ }
24
+ function LiveDot() {
25
+ return (_jsx("span", { "aria-hidden": "true", className: "inline-block size-1.5 shrink-0 animate-pulse rounded-full bg-emerald-500 shadow-[0_0_0_3px_rgba(16,185,129,0.20)]" }));
26
+ }
@@ -0,0 +1,13 @@
1
+ export type { DateSwitcherProps } from './date-switcher.js';
2
+ export { DateSwitcher } from './date-switcher.js';
3
+ export type { EventCardProps, EventCardSize } from './event-card.js';
4
+ export { EventCard } from './event-card.js';
5
+ export type { LiveIndicatorProps } from './live-indicator.js';
6
+ export { LiveIndicator } from './live-indicator.js';
7
+ export type { ScrubBarProps } from './scrub-bar.js';
8
+ export { ScrubBar } from './scrub-bar.js';
9
+ export type { TimelineRailProps } from './timeline-rail.js';
10
+ export { TimelineRail } from './timeline-rail.js';
11
+ export type { TimelineTapeProps } from './timeline-tape.js';
12
+ export { TimelineTape } from './timeline-tape.js';
13
+ export type { ScrubMark, TimelineEvent, TimelineEventKind } from './types.js';