cursedbelt 5.92.3 β 5.94.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/dist/react/components/ComboboxField.d.ts +10 -0
- package/dist/react/components/ComboboxField.d.ts.map +1 -1
- package/dist/react/components/ComboboxField.js +4 -4
- package/dist/react/components/ComboboxField.js.map +1 -1
- package/dist/react/components/CommentThread.d.ts +9 -3
- package/dist/react/components/CommentThread.d.ts.map +1 -1
- package/dist/react/components/CommentThread.js +25 -11
- package/dist/react/components/CommentThread.js.map +1 -1
- package/dist/react/components/VoiceNote.d.ts +20 -0
- package/dist/react/components/VoiceNote.d.ts.map +1 -0
- package/dist/react/components/VoiceNote.js +54 -0
- package/dist/react/components/VoiceNote.js.map +1 -0
- package/dist/react/components/VoiceRecorder.d.ts +35 -0
- package/dist/react/components/VoiceRecorder.d.ts.map +1 -0
- package/dist/react/components/VoiceRecorder.js +155 -0
- package/dist/react/components/VoiceRecorder.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/map-frame/MapFrame.d.ts +219 -0
- package/dist/react/map-frame/MapFrame.d.ts.map +1 -0
- package/dist/react/map-frame/MapFrame.js +726 -0
- package/dist/react/map-frame/MapFrame.js.map +1 -0
- package/dist/react/map-frame/geo.d.ts +210 -0
- package/dist/react/map-frame/geo.d.ts.map +1 -0
- package/dist/react/map-frame/geo.js +345 -0
- package/dist/react/map-frame/geo.js.map +1 -0
- package/dist/react/map-frame/index.d.ts +5 -0
- package/dist/react/map-frame/index.d.ts.map +1 -0
- package/dist/react/map-frame/index.js +3 -0
- package/dist/react/map-frame/index.js.map +1 -0
- package/dist/styles-areas/canvas-hud.css +5 -5
- package/dist/styles-areas/workbench.css +17 -17
- package/package.json +13 -1
- package/scripts/guardrails-baseline.json +1 -1
- package/scripts/styleAreas.ts +1 -1
- package/src/react/components/ComboboxField.spec.tsx +39 -0
- package/src/react/components/ComboboxField.tsx +20 -4
- package/src/react/components/CommentThread.spec.tsx +25 -0
- package/src/react/components/CommentThread.tsx +60 -10
- package/src/react/components/VoiceNote.tsx +92 -0
- package/src/react/components/VoiceRecorder.spec.tsx +116 -0
- package/src/react/components/VoiceRecorder.tsx +224 -0
- package/src/react/index.ts +9 -0
- package/src/react/map-frame/MapFrame.spec.ts +168 -0
- package/src/react/map-frame/MapFrame.tsx +954 -0
- package/src/react/map-frame/geo.spec.ts +393 -0
- package/src/react/map-frame/geo.ts +421 -0
- package/src/react/map-frame/index.ts +34 -0
- package/src/react/media/HoverScrubVideoThumb.spec.tsx +4 -0
- package/src/styles-areas/canvas-hud.css +5 -5
- package/src/styles-areas/workbench.css +17 -17
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
1
2
|
import { type ControlSize } from '../lib/field.js';
|
|
2
3
|
/** A single option in the combobox list. */
|
|
3
4
|
export interface ComboboxOption {
|
|
@@ -15,6 +16,15 @@ interface ComboboxBase {
|
|
|
15
16
|
loadOptions?: (query: string) => Promise<ComboboxOption[]>;
|
|
16
17
|
/** Allow typing a value that isn't in the list and creating it on the fly. */
|
|
17
18
|
creatable?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* What the list says when nothing matches β *"Nobody in the tree by that name"*.
|
|
21
|
+
*
|
|
22
|
+
* π΄ Required in practice for a combobox whose options come from DATA (`loadOptions`, or a
|
|
23
|
+
* list read from a store): the generic line cannot say what belongs here, and the
|
|
24
|
+
* `choice-needs-empty-state` guardrail reports one without it. Rendered as plain text, not
|
|
25
|
+
* as an item, so it can never be picked or keyboard-focused. Default "No options found".
|
|
26
|
+
*/
|
|
27
|
+
emptyLabel?: ReactNode;
|
|
18
28
|
placeholder?: string;
|
|
19
29
|
disabled?: boolean;
|
|
20
30
|
id?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComboboxField.d.ts","sourceRoot":"","sources":["../../../src/react/components/ComboboxField.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ComboboxField.d.ts","sourceRoot":"","sources":["../../../src/react/components/ComboboxField.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAA4C,MAAM,OAAO,CAAC;AAEjF,OAAO,EAGL,KAAK,WAAW,EAGjB,MAAM,iBAAiB,CAAC;AAKzB,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B;oEACgE;IAChE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC3D,8EAA8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;uCAEmC;IACnC,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,MAAM,WAAW,wBAAyB,SAAQ,YAAY;IAC5D,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,uBAAwB,SAAQ,YAAY;IAC3D,QAAQ,EAAE,IAAI,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,GAAG,uBAAuB,CAAC;AA6EpF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,+BA8MtD"}
|
|
@@ -10,12 +10,12 @@ import { TAP_TARGET, TAP_TARGET_HEIGHT } from '../lib/tapTarget.js';
|
|
|
10
10
|
const DEBOUNCE_MS = 250;
|
|
11
11
|
/** Sentinel value used to represent the "Create new" option inside Base UI. */
|
|
12
12
|
const CREATE_SENTINEL = '__cb_create__';
|
|
13
|
-
function ComboboxList({ creatable, query, onCreateClick }) {
|
|
13
|
+
function ComboboxList({ creatable, query, onCreateClick, emptyLabel }) {
|
|
14
14
|
const filtered = Combobox.useFilteredItems();
|
|
15
15
|
const trimmed = query.trim();
|
|
16
16
|
const hasExact = filtered.some((o) => o.label.toLowerCase() === trimmed.toLowerCase() || o.value === trimmed);
|
|
17
17
|
const showCreate = creatable && trimmed !== '' && !hasExact;
|
|
18
|
-
return (_jsxs(Combobox.List, { className: 'py-1', children: [filtered.map((item, index) => (_jsxs(Combobox.Item, { value: item.value, index: index, disabled: item.disabled, className: cn(FLOATING_ITEM_BASE, 'pr-8', 'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong'), children: [item.color ? (_jsx("span", { "aria-hidden": true, className: 'size-2 shrink-0 rounded-full', style: { backgroundColor: item.color } })) : null, _jsx("span", { className: 'flex-1 truncate', children: item.label }), _jsx(Combobox.ItemIndicator, { className: 'absolute right-2 flex size-4 items-center justify-center', children: _jsx(Check, { className: 'size-3' }) })] }, item.value))), showCreate && (_jsxs(Combobox.Item, { value: CREATE_SENTINEL, className: cn(FLOATING_ITEM_BASE, 'gap-1.5', 'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong'), onClick: () => onCreateClick(trimmed), children: [_jsx(Plus, { className: 'size-3', "aria-hidden": true }), "Create \u201C", trimmed, "\u201D"] })), _jsx(Combobox.Empty, { className: 'py-2 text-center text-sm text-muted-foreground', children:
|
|
18
|
+
return (_jsxs(Combobox.List, { className: 'py-1', children: [filtered.map((item, index) => (_jsxs(Combobox.Item, { value: item.value, index: index, disabled: item.disabled, className: cn(FLOATING_ITEM_BASE, 'pr-8', 'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong'), children: [item.color ? (_jsx("span", { "aria-hidden": true, className: 'size-2 shrink-0 rounded-full', style: { backgroundColor: item.color } })) : null, _jsx("span", { className: 'flex-1 truncate', children: item.label }), _jsx(Combobox.ItemIndicator, { className: 'absolute right-2 flex size-4 items-center justify-center', children: _jsx(Check, { className: 'size-3' }) })] }, item.value))), showCreate && (_jsxs(Combobox.Item, { value: CREATE_SENTINEL, className: cn(FLOATING_ITEM_BASE, 'gap-1.5', 'data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong'), onClick: () => onCreateClick(trimmed), children: [_jsx(Plus, { className: 'size-3', "aria-hidden": true }), "Create \u201C", trimmed, "\u201D"] })), _jsx(Combobox.Empty, { className: 'py-2 text-center text-sm text-muted-foreground', children: emptyLabel })] }));
|
|
19
19
|
}
|
|
20
20
|
/* -------------------------------------------------------------------------- */
|
|
21
21
|
/* Public component */
|
|
@@ -31,7 +31,7 @@ function ComboboxList({ creatable, query, onCreateClick }) {
|
|
|
31
31
|
* type so it renders inside a {@link Field} with label + validation.
|
|
32
32
|
*/
|
|
33
33
|
export function ComboboxField(props) {
|
|
34
|
-
const { options = [], loadOptions, creatable = false, placeholder = 'Selectβ¦', disabled, id, name, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, className, size = 'md',
|
|
34
|
+
const { options = [], loadOptions, creatable = false, emptyLabel = 'No options found', placeholder = 'Selectβ¦', disabled, id, name, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, className, size = 'md',
|
|
35
35
|
// Named so they stay off the input: the discriminated union's own shape, read via
|
|
36
36
|
// `(props as β¦).multiple`/`.value`/`.onChange` below, never off this destructuring.
|
|
37
37
|
multiple: _multiple, value: _value, onChange: _onChange, ...rest } = props;
|
|
@@ -120,7 +120,7 @@ export function ComboboxField(props) {
|
|
|
120
120
|
// preflight (desk) would otherwise add the UA's inset border and control face (task 2757).
|
|
121
121
|
'm-0 min-w-24 flex-1 border-0 bg-transparent px-3 font-[inherit] text-base text-field-fg outline-none placeholder:text-field-placeholder sm:pointer-fine:text-sm', TAP_TARGET_HEIGHT, multiple && 'py-1.5') }), loading ? (_jsx(Loader2, { className: 'mr-2 size-4 shrink-0 animate-spin opacity-50', "aria-label": 'Loading options' })) : (_jsx(Combobox.Trigger, { "aria-label": 'Toggle options', className: `mr-2 flex size-5 shrink-0 items-center justify-center text-muted-foreground transition hover:text-foreground ${TAP_TARGET}`, children: _jsx(ChevronDown, { className: 'size-4' }) }))] }), _jsx(Combobox.Portal, { children: _jsx(Combobox.Positioner, { sideOffset: 4, collisionPadding: FLOATING_COLLISION_PADDING, className: 'z-[var(--z-popover)]', children: _jsx(Combobox.Popup, { className: cn(FLOATING_CONTENT_BASE,
|
|
122
122
|
// guardrails-ignore no-raw-overflow: floating popover panel outside the page's ScrollArea, height-capped by max-h-64 β no keyboard-inset concern.
|
|
123
|
-
'max-h-64 min-w-[var(--anchor-width)] overflow-y-auto p-0', 'transition-[transform,opacity] duration-100', 'data-[starting-style]:scale-95 data-[starting-style]:opacity-0', 'data-[ending-style]:scale-95 data-[ending-style]:opacity-0'), children: _jsx(ComboboxList, { creatable: creatable, query: query, onCreateClick: handleCreate }) }) }) })] }));
|
|
123
|
+
'max-h-64 min-w-[var(--anchor-width)] overflow-y-auto p-0', 'transition-[transform,opacity] duration-100', 'data-[starting-style]:scale-95 data-[starting-style]:opacity-0', 'data-[ending-style]:scale-95 data-[ending-style]:opacity-0'), children: _jsx(ComboboxList, { creatable: creatable, query: query, onCreateClick: handleCreate, emptyLabel: emptyLabel }) }) }) })] }));
|
|
124
124
|
}
|
|
125
125
|
// FRM-3: a closed list is sized by its longest option, as SelectField is. A list that can hold
|
|
126
126
|
// a value nobody has seen yet β fetched (`loadOptions`) or typed (`creatable`) β keeps a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComboboxField.js","sourceRoot":"","sources":["../../../src/react/components/ComboboxField.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,
|
|
1
|
+
{"version":3,"file":"ComboboxField.js","sourceRoot":"","sources":["../../../src/react/components/ComboboxField.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactionGroup } from './ReactionBar.js';
|
|
2
|
+
import { type VoiceRecording } from './VoiceRecorder.js';
|
|
2
3
|
export interface CommentAuthor {
|
|
3
4
|
id: string;
|
|
4
5
|
name?: string;
|
|
@@ -22,6 +23,8 @@ export interface CommentData {
|
|
|
22
23
|
/** Whether the current user may delete this comment. */
|
|
23
24
|
canDelete?: boolean;
|
|
24
25
|
reactions?: CommentReactionSummary;
|
|
26
|
+
/** A voice comment's audio, played inline above its (optional) text. */
|
|
27
|
+
audioUrl?: string | null;
|
|
25
28
|
}
|
|
26
29
|
export interface CommentThreadProps {
|
|
27
30
|
comments?: CommentData[];
|
|
@@ -32,6 +35,8 @@ export interface CommentThreadProps {
|
|
|
32
35
|
onDelete?: (id: string) => void;
|
|
33
36
|
/** Toggle a reaction on a comment. */
|
|
34
37
|
onReact?: (commentId: string, emoji: string) => void;
|
|
38
|
+
/** Post a voice comment (parentId for a reply). When set, every composer grows a record button. */
|
|
39
|
+
onPostVoice?: (recording: VoiceRecording, parentId?: string) => void;
|
|
35
40
|
className?: string;
|
|
36
41
|
emptyMessage?: string;
|
|
37
42
|
/** Placeholder for the root-level composer. */
|
|
@@ -42,9 +47,10 @@ export interface CommentThreadProps {
|
|
|
42
47
|
* query/mutation hooks bundled β wire up your own TanStack Query mutations and pass
|
|
43
48
|
* the results as props.
|
|
44
49
|
*
|
|
45
|
-
* Supports one level of replies, edit, delete (with confirm),
|
|
46
|
-
* via {@link ReactionBar}
|
|
50
|
+
* Supports one level of replies, edit, delete (with confirm), emoji reactions
|
|
51
|
+
* via {@link ReactionBar}, and β with `onPostVoice` β voice comments recorded by
|
|
52
|
+
* {@link VoiceRecorder} and played inline by {@link VoiceNote}. Each interaction calls its corresponding handler; the
|
|
47
53
|
* caller re-renders with fresh data.
|
|
48
54
|
*/
|
|
49
|
-
export declare function CommentThread({ comments, isLoading, onPost, onEdit, onDelete, onReact, className, emptyMessage, placeholder, ...rest }: CommentThreadProps): import("react").JSX.Element;
|
|
55
|
+
export declare function CommentThread({ comments, isLoading, onPost, onEdit, onDelete, onReact, onPostVoice, className, emptyMessage, placeholder, ...rest }: CommentThreadProps): import("react").JSX.Element;
|
|
50
56
|
//# sourceMappingURL=CommentThread.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentThread.d.ts","sourceRoot":"","sources":["../../../src/react/components/CommentThread.tsx"],"names":[],"mappings":"AASA,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"CommentThread.d.ts","sourceRoot":"","sources":["../../../src/react/components/CommentThread.tsx"],"names":[],"mappings":"AASA,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGnE,OAAO,EAAE,KAAK,cAAc,EAAiB,MAAM,oBAAoB,CAAC;AAIxE,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,sCAAsC;IACtC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,mGAAmG;IACnG,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAiTD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,EAC5B,QAAa,EACb,SAAS,EACT,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,EACP,WAAW,EACX,SAAS,EACT,YAA0D,EAC1D,WAAqD,EACrD,GAAG,IAAI,EACR,EAAE,kBAAkB,+BA+BpB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Edit2, Send, Trash2 } from 'lucide-react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Edit2, Mic, Send, Trash2 } from 'lucide-react';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { cn } from '../lib/cn.js';
|
|
5
5
|
import { FIELD_CLASS } from '../lib/field.js';
|
|
@@ -10,6 +10,8 @@ import { Button } from './Button.js';
|
|
|
10
10
|
import { IconButton } from './IconButton.js';
|
|
11
11
|
import { ReactionBar } from './ReactionBar.js';
|
|
12
12
|
import { Skeleton } from './Skeleton.js';
|
|
13
|
+
import { VoiceNote } from './VoiceNote.js';
|
|
14
|
+
import { VoiceRecorder } from './VoiceRecorder.js';
|
|
13
15
|
// ββ Internal helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
14
16
|
const MS_MIN = 60_000;
|
|
15
17
|
const MS_DAY = 86_400_000;
|
|
@@ -34,8 +36,9 @@ function formatRelative(target, from = new Date()) {
|
|
|
34
36
|
return past ? `${phrase} ago` : `in ${phrase}`;
|
|
35
37
|
}
|
|
36
38
|
// ββ Composer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
37
|
-
function Composer({ onSubmit, placeholder, autoFocus, onCancel, }) {
|
|
39
|
+
function Composer({ onSubmit, onVoice, placeholder, autoFocus, onCancel, }) {
|
|
38
40
|
const [body, setBody] = useState('');
|
|
41
|
+
const [recording, setRecording] = useState(false);
|
|
39
42
|
const submit = () => {
|
|
40
43
|
const trimmed = body.trim();
|
|
41
44
|
if (!trimmed)
|
|
@@ -43,6 +46,12 @@ function Composer({ onSubmit, placeholder, autoFocus, onCancel, }) {
|
|
|
43
46
|
onSubmit(trimmed);
|
|
44
47
|
setBody('');
|
|
45
48
|
};
|
|
49
|
+
if (recording && onVoice) {
|
|
50
|
+
return (_jsx(VoiceRecorder, { onCancel: () => setRecording(false), onDone: (r) => {
|
|
51
|
+
setRecording(false);
|
|
52
|
+
onVoice(r);
|
|
53
|
+
} }));
|
|
54
|
+
}
|
|
46
55
|
return (_jsxs("div", { className: 'flex items-end gap-2', children: [_jsx(AutoGrowTextarea, { autoFocus: autoFocus, value: body, placeholder: placeholder, rows: 1, onChange: (e) => setBody(e.target.value), onKeyDown: (e) => {
|
|
47
56
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey))
|
|
48
57
|
submit();
|
|
@@ -51,7 +60,7 @@ function Composer({ onSubmit, placeholder, autoFocus, onCancel, }) {
|
|
|
51
60
|
},
|
|
52
61
|
// `TAP_TARGET_HEIGHT`: at `min-h-9` (36px) the composer fell short of the 44px tap
|
|
53
62
|
// floor on a coarse pointer (DESIGN.md LAY-13).
|
|
54
|
-
className: cn(FIELD_CLASS, 'min-h-9 flex-1', TAP_TARGET_HEIGHT) }), _jsx(Button, { variant: 'primary', size: 'sm', "aria-label": 'Send', disabled: !body.trim(), onClick: submit, tooltip: 'Post comment', children: _jsx(Send, { className: 'h-4 w-4', "aria-hidden": true }) }), onCancel && (_jsx(Button, { variant: 'ghost', size: 'sm', onClick: onCancel, tooltip: 'Cancel comment', children: "Cancel" }))] }));
|
|
63
|
+
className: cn(FIELD_CLASS, 'min-h-9 flex-1', TAP_TARGET_HEIGHT) }), onVoice && (_jsx(IconButton, { label: 'Record a voice comment', variant: 'secondary', onClick: () => setRecording(true), children: _jsx(Mic, {}) })), _jsx(Button, { variant: 'primary', size: 'sm', "aria-label": 'Send', disabled: !body.trim(), onClick: submit, tooltip: 'Post comment', children: _jsx(Send, { className: 'h-4 w-4', "aria-hidden": true }) }), onCancel && (_jsx(Button, { variant: 'ghost', size: 'sm', onClick: onCancel, tooltip: 'Cancel comment', children: "Cancel" }))] }));
|
|
55
64
|
}
|
|
56
65
|
// ββ CommentItem βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
57
66
|
/** Two placeholder comments, in a comment's own shape: avatar, then a body bubble. */
|
|
@@ -60,7 +69,7 @@ function CommentSkeleton() {
|
|
|
60
69
|
}
|
|
61
70
|
/** A comment's secondary icon actions β muted until hovered, at the Reply link's weight. */
|
|
62
71
|
const QUIET_ACTION = 'text-muted-foreground hover:text-foreground [&_svg]:size-4';
|
|
63
|
-
function CommentItem({ comment, replies, onPost, onEdit, onDelete, onReact, }) {
|
|
72
|
+
function CommentItem({ comment, replies, onPost, onEdit, onDelete, onReact, onPostVoice, }) {
|
|
64
73
|
const [replying, setReplying] = useState(false);
|
|
65
74
|
const [editing, setEditing] = useState(false);
|
|
66
75
|
const [confirmDel, setConfirmDel] = useState(false);
|
|
@@ -69,11 +78,15 @@ function CommentItem({ comment, replies, onPost, onEdit, onDelete, onReact, }) {
|
|
|
69
78
|
return (_jsxs("div", { className: 'flex gap-2', children: [_jsx(Avatar, { name: comment.author.name, src: comment.author.avatarUrl, size: 'sm', className: 'mt-0.5 shrink-0' }), _jsxs("div", { className: 'min-w-0 flex-1', children: [_jsxs("div", { className: 'rounded-lg bg-muted px-3 py-2', children: [_jsxs("div", { className: 'flex items-baseline gap-2', children: [_jsx("span", { className: 'text-sm font-medium text-foreground', children: comment.author.name || 'Someone' }), _jsx("span", { className: 'text-xs text-muted-foreground', children: formatRelative(createdAt) })] }), editing ? (_jsxs("div", { className: 'mt-1 flex items-end gap-2', children: [_jsx(AutoGrowTextarea, { value: draft, rows: 1, onChange: (e) => setDraft(e.target.value), className: cn(FIELD_CLASS, 'min-h-9 flex-1', TAP_TARGET_HEIGHT) }), _jsx(Button, { variant: 'primary', size: 'sm', disabled: !draft.trim(), onClick: () => {
|
|
70
79
|
onEdit?.(comment.id, draft.trim());
|
|
71
80
|
setEditing(false);
|
|
72
|
-
}, tooltip: 'Save edited comment', children: "Save" }), _jsx(Button, { variant: 'ghost', size: 'sm', onClick: () => setEditing(false), tooltip: 'Cancel editing', children: "Cancel" })] })) : (_jsx("p", { className: 'mt-0.5
|
|
81
|
+
}, tooltip: 'Save edited comment', children: "Save" }), _jsx(Button, { variant: 'ghost', size: 'sm', onClick: () => setEditing(false), tooltip: 'Cancel editing', children: "Cancel" })] })) : comment.deleted ? (_jsx("p", { className: 'mt-0.5 text-sm italic text-muted-foreground', children: "deleted" })) : (_jsxs(_Fragment, { children: [comment.audioUrl && (_jsx(VoiceNote, { src: comment.audioUrl, label: `Play voice comment by ${comment.author.name || 'someone'}`, className: 'mt-1 max-w-80' })), comment.body && (_jsx("p", { className: 'mt-0.5 whitespace-pre-wrap break-words text-sm text-foreground', children: comment.body }))] }))] }), !comment.deleted && (_jsxs("div", { className: 'mt-1 flex flex-wrap items-center gap-2 pl-1', children: [comment.reactions && (_jsx(ReactionBar, { emojis: comment.reactions.emojis, myReaction: comment.reactions.myReaction, onReact: (emoji) => onReact?.(comment.id, emoji) })), _jsx(Button, { variant: 'link', onClick: () => setReplying((v) => !v), className: 'text-xs text-muted-foreground hover:text-foreground', tooltip: replying ? 'Cancel reply' : 'Reply to comment', children: "Reply" }), comment.canEdit && !editing && (_jsx(IconButton, { label: 'Edit comment', size: 'sm', className: QUIET_ACTION, onClick: () => setEditing(true), children: _jsx(Edit2, {}) })), comment.canDelete &&
|
|
73
82
|
(confirmDel ? (_jsxs("span", { className: 'flex items-center gap-1 text-xs', children: [_jsx(Button, { variant: 'link', className: 'text-destructive', onClick: () => onDelete?.(comment.id), tooltip: 'Confirm delete comment', children: "Delete?" }), _jsx(Button, { variant: 'link', className: 'text-muted-foreground', onClick: () => setConfirmDel(false), tooltip: 'Cancel delete', children: "No" })] })) : (_jsx(IconButton, { label: 'Delete comment', size: 'sm', className: QUIET_ACTION, onClick: () => setConfirmDel(true), children: _jsx(Trash2, {}) })))] })), replying && (_jsx("div", { className: 'mt-2', children: _jsx(Composer, { autoFocus: true, placeholder: 'Write a reply\u2026', onCancel: () => setReplying(false), onSubmit: (body) => {
|
|
74
83
|
onPost(body, comment.id);
|
|
75
84
|
setReplying(false);
|
|
76
|
-
}
|
|
85
|
+
}, onVoice: onPostVoice &&
|
|
86
|
+
((r) => {
|
|
87
|
+
onPostVoice(r, comment.id);
|
|
88
|
+
setReplying(false);
|
|
89
|
+
}) }) })), replies.length > 0 && (_jsx("div", { className: 'mt-2 space-y-3 border-l border-border pl-3', children: replies.map((r) => (_jsx(CommentItem, { comment: r, replies: [], onPost: onPost, onEdit: onEdit, onDelete: onDelete, onReact: onReact, onPostVoice: onPostVoice }, r.id))) }))] })] }));
|
|
77
90
|
}
|
|
78
91
|
// ββ CommentThread βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
79
92
|
/**
|
|
@@ -81,14 +94,15 @@ function CommentItem({ comment, replies, onPost, onEdit, onDelete, onReact, }) {
|
|
|
81
94
|
* query/mutation hooks bundled β wire up your own TanStack Query mutations and pass
|
|
82
95
|
* the results as props.
|
|
83
96
|
*
|
|
84
|
-
* Supports one level of replies, edit, delete (with confirm),
|
|
85
|
-
* via {@link ReactionBar}
|
|
97
|
+
* Supports one level of replies, edit, delete (with confirm), emoji reactions
|
|
98
|
+
* via {@link ReactionBar}, and β with `onPostVoice` β voice comments recorded by
|
|
99
|
+
* {@link VoiceRecorder} and played inline by {@link VoiceNote}. Each interaction calls its corresponding handler; the
|
|
86
100
|
* caller re-renders with fresh data.
|
|
87
101
|
*/
|
|
88
|
-
export function CommentThread({ comments = [], isLoading, onPost, onEdit, onDelete, onReact, className, emptyMessage = 'No comments yet β start the conversation.', placeholder = 'Add a commentβ¦ (β/Ctrl+Enter to send)', ...rest }) {
|
|
102
|
+
export function CommentThread({ comments = [], isLoading, onPost, onEdit, onDelete, onReact, onPostVoice, className, emptyMessage = 'No comments yet β start the conversation.', placeholder = 'Add a commentβ¦ (β/Ctrl+Enter to send)', ...rest }) {
|
|
89
103
|
const topLevel = comments.filter((c) => !c.parentId);
|
|
90
104
|
const repliesOf = (id) => comments.filter((c) => c.parentId === id);
|
|
91
105
|
return (_jsxs("div", { ...rest, className: cn('space-y-3', className), children: [!isLoading &&
|
|
92
|
-
topLevel.map((c) => (_jsx(CommentItem, { comment: c, replies: repliesOf(c.id), onPost: onPost, onEdit: onEdit, onDelete: onDelete, onReact: onReact }, c.id))), isLoading && _jsx(CommentSkeleton, {}), !isLoading && topLevel.length === 0 && (_jsx("p", { className: 'text-xs text-muted-foreground', children: emptyMessage })), _jsx(Composer, { placeholder: placeholder, onSubmit: (body) => onPost(body) })] }));
|
|
106
|
+
topLevel.map((c) => (_jsx(CommentItem, { comment: c, replies: repliesOf(c.id), onPost: onPost, onEdit: onEdit, onDelete: onDelete, onReact: onReact, onPostVoice: onPostVoice }, c.id))), isLoading && _jsx(CommentSkeleton, {}), !isLoading && topLevel.length === 0 && (_jsx("p", { className: 'text-xs text-muted-foreground', children: emptyMessage })), _jsx(Composer, { placeholder: placeholder, onSubmit: (body) => onPost(body), onVoice: onPostVoice && ((r) => onPostVoice(r)) })] }));
|
|
93
107
|
}
|
|
94
108
|
//# sourceMappingURL=CommentThread.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentThread.js","sourceRoot":"","sources":["../../../src/react/components/CommentThread.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"CommentThread.js","sourceRoot":"","sources":["../../../src/react/components/CommentThread.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `m:ss` β a voice note is minutes long at most. */
|
|
2
|
+
export declare function formatVoiceTime(ms: number): string;
|
|
3
|
+
export interface VoiceNoteProps {
|
|
4
|
+
/** The audio's URL β a signed media URL, or an object URL for a recording not yet sent. */
|
|
5
|
+
src: string;
|
|
6
|
+
/** The length when the caller already knows it; otherwise the element's own metadata says. */
|
|
7
|
+
durationMs?: number;
|
|
8
|
+
/** What the play button names: "Play voice comment by Alice". */
|
|
9
|
+
label?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A voice note: play/pause, a seek track and the time β the compact pill a message thread draws
|
|
14
|
+
* (iMessage, WhatsApp, Slack huddles), not the browser's own `<audio controls>`, whose look and
|
|
15
|
+
* height differ in every engine and which ignores the theme.
|
|
16
|
+
*
|
|
17
|
+
* `preload='metadata'`: a thread of ten notes fetches ten headers, not ten files.
|
|
18
|
+
*/
|
|
19
|
+
export declare function VoiceNote({ src, durationMs, label, className, ...rest }: VoiceNoteProps): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=VoiceNote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceNote.d.ts","sourceRoot":"","sources":["../../../src/react/components/VoiceNote.tsx"],"names":[],"mappings":"AAKA,qDAAqD;AACrD,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED,MAAM,WAAW,cAAc;IAC7B,2FAA2F;IAC3F,GAAG,EAAE,MAAM,CAAC;IACZ,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,KAAyB,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,cAAc,+BA+D3G"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Pause, Play } from 'lucide-react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { cn } from '../lib/cn.js';
|
|
5
|
+
import { IconButton } from './IconButton.js';
|
|
6
|
+
/** `m:ss` β a voice note is minutes long at most. */
|
|
7
|
+
export function formatVoiceTime(ms) {
|
|
8
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
9
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A voice note: play/pause, a seek track and the time β the compact pill a message thread draws
|
|
13
|
+
* (iMessage, WhatsApp, Slack huddles), not the browser's own `<audio controls>`, whose look and
|
|
14
|
+
* height differ in every engine and which ignores the theme.
|
|
15
|
+
*
|
|
16
|
+
* `preload='metadata'`: a thread of ten notes fetches ten headers, not ten files.
|
|
17
|
+
*/
|
|
18
|
+
export function VoiceNote({ src, durationMs, label = 'Play voice note', className, ...rest }) {
|
|
19
|
+
const audio = useRef(null);
|
|
20
|
+
const [playing, setPlaying] = useState(false);
|
|
21
|
+
const [at, setAt] = useState(0);
|
|
22
|
+
const [length, setLength] = useState(durationMs ?? 0);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (durationMs)
|
|
25
|
+
setLength(durationMs);
|
|
26
|
+
}, [durationMs]);
|
|
27
|
+
const toggle = () => {
|
|
28
|
+
const el = audio.current;
|
|
29
|
+
if (!el)
|
|
30
|
+
return;
|
|
31
|
+
if (el.paused)
|
|
32
|
+
void el.play().catch(() => setPlaying(false));
|
|
33
|
+
else
|
|
34
|
+
el.pause();
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs("div", { ...rest, "data-cb-voice-note": '', className: cn('flex min-w-0 items-center gap-2', className), children: [_jsx("audio", { ref: audio, src: src, preload: 'metadata', onPlay: () => setPlaying(true), onPause: () => setPlaying(false), onEnded: () => {
|
|
37
|
+
setPlaying(false);
|
|
38
|
+
setAt(0);
|
|
39
|
+
}, onTimeUpdate: (e) => setAt(e.currentTarget.currentTime * 1000), onLoadedMetadata: (e) => {
|
|
40
|
+
// A MediaRecorder WebM says `Infinity` until it has been read through; keep the caller's.
|
|
41
|
+
const d = e.currentTarget.duration;
|
|
42
|
+
if (Number.isFinite(d) && d > 0)
|
|
43
|
+
setLength(d * 1000);
|
|
44
|
+
} }), _jsx(IconButton, { label: playing ? 'Pause' : label, size: 'sm', variant: 'secondary', className: 'shrink-0 rounded-full', onClick: toggle, children: playing ? _jsx(Pause, {}) : _jsx(Play, {}) }), _jsx("input", {
|
|
45
|
+
// guardrails-ignore no-raw-inputs: perf β a voice note's own seek track (type=range), not a form
|
|
46
|
+
// field β the native range keeps Radix's slider (~6 kB gzip) out of every comment thread.
|
|
47
|
+
type: 'range', "aria-label": 'Position', "aria-valuetext": formatVoiceTime(at), min: 0, max: Math.max(1, length), step: 100, value: Math.min(at, Math.max(1, length)), disabled: !length, onChange: (e) => {
|
|
48
|
+
const v = Number(e.target.value);
|
|
49
|
+
setAt(v);
|
|
50
|
+
if (audio.current)
|
|
51
|
+
audio.current.currentTime = v / 1000;
|
|
52
|
+
}, className: 'h-1 min-w-16 flex-1 cursor-pointer accent-[var(--accent-solid)] disabled:cursor-not-allowed' }), _jsx("span", { className: 'shrink-0 text-xs tabular-nums text-muted-foreground', children: formatVoiceTime(playing || at > 0 ? at : length) })] }));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=VoiceNote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceNote.js","sourceRoot":"","sources":["../../../src/react/components/VoiceNote.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Two minutes: a spoken memory, not a podcast. Opus at speech rates is ~0.5 MB, AAC ~2 MB. */
|
|
2
|
+
export declare const VOICE_MAX_MS = 120000;
|
|
3
|
+
/**
|
|
4
|
+
* The container a recording is written in, best first: Opus in WebM where Chromium and Firefox
|
|
5
|
+
* take it, AAC in MP4 for WebKit (Safari records no WebM). `null` when the browser records none.
|
|
6
|
+
*/
|
|
7
|
+
export declare const VOICE_MIME_CANDIDATES: readonly ["audio/webm;codecs=opus", "audio/mp4", "audio/webm", "audio/ogg;codecs=opus"];
|
|
8
|
+
export declare function pickVoiceMime(isSupported?: (type: string) => boolean): string | null;
|
|
9
|
+
export interface VoiceRecording {
|
|
10
|
+
blob: Blob;
|
|
11
|
+
/** The container without its codec parameter β `audio/webm`, `audio/mp4` β what a server keys on. */
|
|
12
|
+
mime: string;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
}
|
|
15
|
+
export interface VoiceRecorderProps {
|
|
16
|
+
/** The recording, when the person sends it. */
|
|
17
|
+
onDone: (recording: VoiceRecording) => void;
|
|
18
|
+
/** Canceled, discarded, or the microphone could not be had. */
|
|
19
|
+
onCancel: () => void;
|
|
20
|
+
/** Recording stops itself here and waits to be sent or discarded. Default {@link VOICE_MAX_MS}. */
|
|
21
|
+
maxMs?: number;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Records one voice note from the microphone. It starts on mount β the person already tapped
|
|
26
|
+
* "record" to show it β and shows a running time and a level meter so they can see it hears them.
|
|
27
|
+
* **Send** stops and hands the recording over in one tap (WhatsApp's flow, not stop β review β
|
|
28
|
+
* send); **Cancel** throws it away. At `maxMs` it stops by itself and offers the take back to
|
|
29
|
+
* listen to, send or discard, rather than sending something nobody chose to send.
|
|
30
|
+
*
|
|
31
|
+
* The level meter reads an `AnalyserNode` when the browser has Web Audio; without one the
|
|
32
|
+
* meter stays dark and the recording still works.
|
|
33
|
+
*/
|
|
34
|
+
export declare function VoiceRecorder({ onDone, onCancel, maxMs, className, ...rest }: VoiceRecorderProps): import("react").JSX.Element;
|
|
35
|
+
//# sourceMappingURL=VoiceRecorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceRecorder.d.ts","sourceRoot":"","sources":["../../../src/react/components/VoiceRecorder.tsx"],"names":[],"mappings":"AAMA,+FAA+F;AAC/F,eAAO,MAAM,YAAY,SAAU,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,yFAA0F,CAAC;AAE7H,wBAAgB,aAAa,CAC3B,WAAW,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAyF,GACvH,MAAM,GAAG,IAAI,CAEf;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,qGAAqG;IACrG,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,MAAM,EAAE,CAAC,SAAS,EAAE,cAAc,KAAK,IAAI,CAAC;IAC5C,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAoB,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,kBAAkB,+BA+J/G"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Send, Trash2 } from 'lucide-react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { cn } from '../lib/cn.js';
|
|
5
|
+
import { Button } from './Button.js';
|
|
6
|
+
import { formatVoiceTime, VoiceNote } from './VoiceNote.js';
|
|
7
|
+
/** Two minutes: a spoken memory, not a podcast. Opus at speech rates is ~0.5 MB, AAC ~2 MB. */
|
|
8
|
+
export const VOICE_MAX_MS = 120_000;
|
|
9
|
+
/**
|
|
10
|
+
* The container a recording is written in, best first: Opus in WebM where Chromium and Firefox
|
|
11
|
+
* take it, AAC in MP4 for WebKit (Safari records no WebM). `null` when the browser records none.
|
|
12
|
+
*/
|
|
13
|
+
export const VOICE_MIME_CANDIDATES = ['audio/webm;codecs=opus', 'audio/mp4', 'audio/webm', 'audio/ogg;codecs=opus'];
|
|
14
|
+
export function pickVoiceMime(isSupported = (t) => typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported(t)) {
|
|
15
|
+
return VOICE_MIME_CANDIDATES.find((t) => isSupported(t)) ?? null;
|
|
16
|
+
}
|
|
17
|
+
/** The meter's five bars, lit from the left as the voice gets louder. */
|
|
18
|
+
const BARS = ['h-1.5', 'h-2.5', 'h-3.5', 'h-2.5', 'h-1.5'];
|
|
19
|
+
function refusal(error) {
|
|
20
|
+
const name = error instanceof DOMException ? error.name : '';
|
|
21
|
+
if (name === 'NotAllowedError' || name === 'SecurityError')
|
|
22
|
+
return 'Microphone access was refused. Allow it in the browser to record.';
|
|
23
|
+
if (name === 'NotFoundError' || name === 'OverconstrainedError')
|
|
24
|
+
return 'No microphone was found.';
|
|
25
|
+
return 'The microphone could not be started.';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Records one voice note from the microphone. It starts on mount β the person already tapped
|
|
29
|
+
* "record" to show it β and shows a running time and a level meter so they can see it hears them.
|
|
30
|
+
* **Send** stops and hands the recording over in one tap (WhatsApp's flow, not stop β review β
|
|
31
|
+
* send); **Cancel** throws it away. At `maxMs` it stops by itself and offers the take back to
|
|
32
|
+
* listen to, send or discard, rather than sending something nobody chose to send.
|
|
33
|
+
*
|
|
34
|
+
* The level meter reads an `AnalyserNode` when the browser has Web Audio; without one the
|
|
35
|
+
* meter stays dark and the recording still works.
|
|
36
|
+
*/
|
|
37
|
+
export function VoiceRecorder({ onDone, onCancel, maxMs = VOICE_MAX_MS, className, ...rest }) {
|
|
38
|
+
const [phase, setPhase] = useState({ kind: 'starting' });
|
|
39
|
+
const [elapsed, setElapsed] = useState(0);
|
|
40
|
+
const [level, setLevel] = useState(0);
|
|
41
|
+
const recorder = useRef(null);
|
|
42
|
+
/** What the next `stop` event does with the take: send it, keep it for review, or drop it. */
|
|
43
|
+
const then = useRef('drop');
|
|
44
|
+
const callbacks = useRef({ onDone, onCancel });
|
|
45
|
+
callbacks.current = { onDone, onCancel };
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
let live = true;
|
|
48
|
+
let stream = null;
|
|
49
|
+
let audioContext = null;
|
|
50
|
+
let tick;
|
|
51
|
+
let frame = 0;
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let startedAt = 0;
|
|
54
|
+
const mimeType = pickVoiceMime();
|
|
55
|
+
if (!navigator.mediaDevices?.getUserMedia || !mimeType) {
|
|
56
|
+
setPhase({ kind: 'error', message: 'This browser cannot record audio.' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
navigator.mediaDevices
|
|
60
|
+
.getUserMedia({ audio: true })
|
|
61
|
+
.then((s) => {
|
|
62
|
+
stream = s;
|
|
63
|
+
if (!live) {
|
|
64
|
+
for (const t of s.getTracks())
|
|
65
|
+
t.stop();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const rec = new MediaRecorder(s, { mimeType });
|
|
69
|
+
recorder.current = rec;
|
|
70
|
+
rec.ondataavailable = (e) => {
|
|
71
|
+
if (e.data.size > 0)
|
|
72
|
+
chunks.push(e.data);
|
|
73
|
+
};
|
|
74
|
+
rec.onstop = () => {
|
|
75
|
+
clearInterval(tick);
|
|
76
|
+
cancelAnimationFrame(frame);
|
|
77
|
+
for (const t of s.getTracks())
|
|
78
|
+
t.stop();
|
|
79
|
+
if (!live || then.current === 'drop')
|
|
80
|
+
return;
|
|
81
|
+
const mime = (rec.mimeType || mimeType).split(';')[0] ?? 'audio/webm';
|
|
82
|
+
const recording = { blob: new Blob(chunks, { type: mime }), mime, durationMs: Math.min(maxMs, performance.now() - startedAt) };
|
|
83
|
+
if (then.current === 'send')
|
|
84
|
+
callbacks.current.onDone(recording);
|
|
85
|
+
else
|
|
86
|
+
setPhase({ kind: 'ready', recording, url: URL.createObjectURL(recording.blob) });
|
|
87
|
+
};
|
|
88
|
+
rec.start(250);
|
|
89
|
+
startedAt = performance.now();
|
|
90
|
+
setPhase({ kind: 'recording' });
|
|
91
|
+
tick = setInterval(() => {
|
|
92
|
+
const ms = performance.now() - startedAt;
|
|
93
|
+
setElapsed(Math.min(ms, maxMs));
|
|
94
|
+
if (ms >= maxMs && rec.state === 'recording') {
|
|
95
|
+
then.current = 'review';
|
|
96
|
+
rec.stop();
|
|
97
|
+
}
|
|
98
|
+
}, 200);
|
|
99
|
+
const Ctx = typeof AudioContext === 'undefined' ? null : AudioContext;
|
|
100
|
+
if (Ctx) {
|
|
101
|
+
audioContext = new Ctx();
|
|
102
|
+
const analyser = audioContext.createAnalyser();
|
|
103
|
+
analyser.fftSize = 512;
|
|
104
|
+
audioContext.createMediaStreamSource(s).connect(analyser);
|
|
105
|
+
const samples = new Uint8Array(analyser.fftSize);
|
|
106
|
+
const read = () => {
|
|
107
|
+
analyser.getByteTimeDomainData(samples);
|
|
108
|
+
let sum = 0;
|
|
109
|
+
for (const v of samples)
|
|
110
|
+
sum += ((v - 128) / 128) ** 2;
|
|
111
|
+
// RMS of speech sits around 0.05β0.3; five steps across that range.
|
|
112
|
+
setLevel(Math.min(BARS.length, Math.round(Math.sqrt(sum / samples.length) * 20)));
|
|
113
|
+
frame = requestAnimationFrame(read);
|
|
114
|
+
};
|
|
115
|
+
frame = requestAnimationFrame(read);
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
.catch((error) => {
|
|
119
|
+
if (live)
|
|
120
|
+
setPhase({ kind: 'error', message: refusal(error) });
|
|
121
|
+
});
|
|
122
|
+
return () => {
|
|
123
|
+
live = false;
|
|
124
|
+
clearInterval(tick);
|
|
125
|
+
cancelAnimationFrame(frame);
|
|
126
|
+
if (recorder.current?.state === 'recording')
|
|
127
|
+
recorder.current.stop();
|
|
128
|
+
if (stream)
|
|
129
|
+
for (const t of stream.getTracks())
|
|
130
|
+
t.stop();
|
|
131
|
+
void audioContext?.close();
|
|
132
|
+
};
|
|
133
|
+
}, [maxMs]);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (phase.kind !== 'ready')
|
|
136
|
+
return;
|
|
137
|
+
return () => URL.revokeObjectURL(phase.url);
|
|
138
|
+
}, [phase]);
|
|
139
|
+
const stopAnd = (what) => {
|
|
140
|
+
then.current = what;
|
|
141
|
+
if (recorder.current?.state === 'recording')
|
|
142
|
+
recorder.current.stop();
|
|
143
|
+
if (what === 'drop')
|
|
144
|
+
onCancel();
|
|
145
|
+
};
|
|
146
|
+
if (phase.kind === 'error') {
|
|
147
|
+
return (_jsxs("div", { ...rest, role: 'alert', className: cn('flex items-center gap-2 rounded-lg bg-muted px-3 py-2 text-sm', className), children: [_jsx("span", { className: 'min-w-0 flex-1 text-muted-foreground', children: phase.message }), _jsx(Button, { variant: 'ghost', size: 'sm', onClick: onCancel, tooltip: 'Close the recorder', children: "Close" })] }));
|
|
148
|
+
}
|
|
149
|
+
if (phase.kind === 'ready') {
|
|
150
|
+
return (_jsxs("div", { ...rest, "data-cb-voice-recorder": 'ready', className: cn('flex items-center gap-2 rounded-lg bg-muted px-2 py-1.5', className), children: [_jsx(VoiceNote, { src: phase.url, durationMs: phase.recording.durationMs, label: 'Play your recording', className: 'flex-1' }), _jsx(Button, { variant: 'ghost', size: 'sm', leftIcon: _jsx(Trash2, { className: 'h-4 w-4', "aria-hidden": true }), onClick: onCancel, tooltip: 'Discard the recording', children: "Discard" }), _jsx(Button, { variant: 'primary', size: 'sm', leftIcon: _jsx(Send, { className: 'h-4 w-4', "aria-hidden": true }), onClick: () => onDone(phase.recording), tooltip: 'Send the voice comment', children: "Send" })] }));
|
|
151
|
+
}
|
|
152
|
+
const recording = phase.kind === 'recording';
|
|
153
|
+
return (_jsxs("div", { ...rest, "data-cb-voice-recorder": phase.kind, className: cn('flex items-center gap-3 rounded-lg bg-muted px-3 py-1.5', className), children: [_jsx("span", { "aria-hidden": true, className: cn('size-2.5 shrink-0 rounded-full bg-destructive', recording && 'animate-pulse') }), _jsx("span", { role: 'timer', "aria-label": 'Recording time', className: 'shrink-0 text-sm font-medium tabular-nums text-foreground', children: recording ? formatVoiceTime(elapsed) : 'Startingβ¦' }), _jsx("span", { "aria-hidden": true, className: 'flex h-4 items-center gap-0.5', children: BARS.map((h, i) => (_jsx("span", { className: cn('w-1 rounded-full', h, i < level ? 'bg-accent-solid' : 'bg-border-strong') }, i))) }), _jsx("span", { className: 'min-w-0 flex-1 truncate text-xs text-muted-foreground', children: recording ? `up to ${formatVoiceTime(maxMs)}` : '' }), _jsx(Button, { variant: 'ghost', size: 'sm', onClick: () => stopAnd('drop'), tooltip: 'Cancel the recording', children: "Cancel" }), _jsx(Button, { variant: 'primary', size: 'sm', disabled: !recording, leftIcon: _jsx(Send, { className: 'h-4 w-4', "aria-hidden": true }), onClick: () => stopAnd('send'), tooltip: 'Send the voice comment', children: "Send" })] }));
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=VoiceRecorder.js.map
|