tempest-react-sdk 0.35.0 → 0.37.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/README.md +72 -70
- package/dist/audio/audio-output.cjs +2 -0
- package/dist/audio/audio-output.cjs.map +1 -0
- package/dist/audio/audio-output.js +17 -0
- package/dist/audio/audio-output.js.map +1 -0
- package/dist/audio/audio-player.cjs +1 -1
- package/dist/audio/audio-player.cjs.map +1 -1
- package/dist/audio/audio-player.js +21 -20
- package/dist/audio/audio-player.js.map +1 -1
- package/dist/audio/audio-recorder.cjs +2 -0
- package/dist/audio/audio-recorder.cjs.map +1 -0
- package/dist/audio/audio-recorder.js +30 -0
- package/dist/audio/audio-recorder.js.map +1 -0
- package/dist/audio/duration.cjs +2 -0
- package/dist/audio/duration.cjs.map +1 -0
- package/dist/audio/duration.js +10 -0
- package/dist/audio/duration.js.map +1 -0
- package/dist/audio/level-meter.cjs +2 -0
- package/dist/audio/level-meter.cjs.map +1 -0
- package/dist/audio/level-meter.js +34 -0
- package/dist/audio/level-meter.js.map +1 -0
- package/dist/audio/media-access.cjs +2 -0
- package/dist/audio/media-access.cjs.map +1 -0
- package/dist/audio/media-access.js +51 -0
- package/dist/audio/media-access.js.map +1 -0
- package/dist/audio/use-audio-recorder.cjs +2 -0
- package/dist/audio/use-audio-recorder.cjs.map +1 -0
- package/dist/audio/use-audio-recorder.js +94 -0
- package/dist/audio/use-audio-recorder.js.map +1 -0
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-audio.js.map +1 -1
- package/dist/audio/use-microphone.cjs +2 -0
- package/dist/audio/use-microphone.cjs.map +1 -0
- package/dist/audio/use-microphone.js +54 -0
- package/dist/audio/use-microphone.js.map +1 -0
- package/dist/audio/wav.cjs +2 -0
- package/dist/audio/wav.cjs.map +1 -0
- package/dist/audio/wav.js +49 -0
- package/dist/audio/wav.js.map +1 -0
- package/dist/auth/passkey.cjs +2 -0
- package/dist/auth/passkey.cjs.map +1 -0
- package/dist/auth/passkey.js +171 -0
- package/dist/auth/passkey.js.map +1 -0
- package/dist/auth/use-passkey.cjs +2 -0
- package/dist/auth/use-passkey.cjs.map +1 -0
- package/dist/auth/use-passkey.js +120 -0
- package/dist/auth/use-passkey.js.map +1 -0
- package/dist/br/PixQRCode.cjs +2 -0
- package/dist/br/PixQRCode.cjs.map +1 -0
- package/dist/br/PixQRCode.js +70 -0
- package/dist/br/PixQRCode.js.map +1 -0
- package/dist/br/PixQRCode.module.cjs +2 -0
- package/dist/br/PixQRCode.module.cjs.map +1 -0
- package/dist/br/PixQRCode.module.js +15 -0
- package/dist/br/PixQRCode.module.js.map +1 -0
- package/dist/br/boleto.cjs +2 -0
- package/dist/br/boleto.cjs.map +1 -0
- package/dist/br/boleto.js +225 -0
- package/dist/br/boleto.js.map +1 -0
- package/dist/br/holidays.cjs +2 -0
- package/dist/br/holidays.cjs.map +1 -0
- package/dist/br/holidays.js +148 -0
- package/dist/br/holidays.js.map +1 -0
- package/dist/br/nfe.cjs +2 -0
- package/dist/br/nfe.cjs.map +1 -0
- package/dist/br/nfe.js +104 -0
- package/dist/br/nfe.js.map +1 -0
- package/dist/br/pix.cjs +2 -0
- package/dist/br/pix.cjs.map +1 -0
- package/dist/br/pix.js +148 -0
- package/dist/br/pix.js.map +1 -0
- package/dist/br.cjs +1 -1
- package/dist/br.cjs.map +1 -1
- package/dist/br.d.ts +878 -0
- package/dist/br.js +7 -2
- package/dist/br.js.map +1 -1
- package/dist/capture/barcode.cjs +2 -0
- package/dist/capture/barcode.cjs.map +1 -0
- package/dist/capture/barcode.js +59 -0
- package/dist/capture/barcode.js.map +1 -0
- package/dist/capture/media-recorder.cjs +2 -0
- package/dist/capture/media-recorder.cjs.map +1 -0
- package/dist/capture/media-recorder.js +64 -0
- package/dist/capture/media-recorder.js.map +1 -0
- package/dist/capture/use-barcode-scanner.cjs +2 -0
- package/dist/capture/use-barcode-scanner.cjs.map +1 -0
- package/dist/capture/use-barcode-scanner.js +86 -0
- package/dist/capture/use-barcode-scanner.js.map +1 -0
- package/dist/capture/use-screen-capture.cjs +2 -0
- package/dist/capture/use-screen-capture.cjs.map +1 -0
- package/dist/capture/use-screen-capture.js +79 -0
- package/dist/capture/use-screen-capture.js.map +1 -0
- package/dist/capture/use-speech-recognition.cjs +2 -0
- package/dist/capture/use-speech-recognition.cjs.map +1 -0
- package/dist/capture/use-speech-recognition.js +118 -0
- package/dist/capture/use-speech-recognition.js.map +1 -0
- package/dist/capture/use-torch.cjs +2 -0
- package/dist/capture/use-torch.cjs.map +1 -0
- package/dist/capture/use-torch.js +33 -0
- package/dist/capture/use-torch.js.map +1 -0
- package/dist/capture/use-video-recorder.cjs +2 -0
- package/dist/capture/use-video-recorder.cjs.map +1 -0
- package/dist/capture/use-video-recorder.js +90 -0
- package/dist/capture/use-video-recorder.js.map +1 -0
- package/dist/capture/video-recorder.cjs +2 -0
- package/dist/capture/video-recorder.cjs.map +1 -0
- package/dist/capture/video-recorder.js +31 -0
- package/dist/capture/video-recorder.js.map +1 -0
- package/dist/components/AIChat/AIChat.cjs +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js +93 -97
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js +2 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs +2 -2
- package/dist/components/AIChat/AIChatTurn.js +6 -6
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Alert/Alert.module.cjs.map +1 -1
- package/dist/components/Alert/Alert.module.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs +2 -0
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.js +123 -0
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.cjs +2 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.js +12 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.js.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.cjs +2 -0
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.js +179 -0
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.cjs +2 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.cjs.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.js +19 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.js.map +1 -0
- package/dist/components/Badge/Badge.module.cjs.map +1 -1
- package/dist/components/Badge/Badge.module.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs +2 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.js +123 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs +2 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.js +22 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.js.map +1 -0
- package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
- package/dist/components/Button/Button.module.cjs.map +1 -1
- package/dist/components/Button/Button.module.js.map +1 -1
- package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.module.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +203 -66
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js +9 -3
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs +2 -0
- package/dist/components/DataTable/EditableCell.cjs.map +1 -0
- package/dist/components/DataTable/EditableCell.js +77 -0
- package/dist/components/DataTable/EditableCell.js.map +1 -0
- package/dist/components/DataTable/edit-labels.cjs +2 -0
- package/dist/components/DataTable/edit-labels.cjs.map +1 -0
- package/dist/components/DataTable/edit-labels.js +11 -0
- package/dist/components/DataTable/edit-labels.js.map +1 -0
- package/dist/components/DateRangePicker/DateRangePicker.module.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.module.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js +33 -32
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +1 -2
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/hooks/use-announce.cjs +2 -0
- package/dist/hooks/use-announce.cjs.map +1 -0
- package/dist/hooks/use-announce.js +50 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-media-devices.cjs +2 -0
- package/dist/hooks/use-media-devices.cjs.map +1 -0
- package/dist/hooks/use-media-devices.js +46 -0
- package/dist/hooks/use-media-devices.js.map +1 -0
- package/dist/hooks/use-media-permission.cjs +2 -0
- package/dist/hooks/use-media-permission.cjs.map +1 -0
- package/dist/hooks/use-media-permission.js +33 -0
- package/dist/hooks/use-media-permission.js.map +1 -0
- package/dist/http/resumable-upload.cjs +2 -0
- package/dist/http/resumable-upload.cjs.map +1 -0
- package/dist/http/resumable-upload.js +249 -0
- package/dist/http/resumable-upload.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +2485 -29
- package/dist/tempest-react-sdk.js +268 -244
- package/dist/vision/core/graph.cjs +2 -0
- package/dist/vision/core/graph.cjs.map +1 -0
- package/dist/vision/core/graph.js +17 -0
- package/dist/vision/core/graph.js.map +1 -0
- package/dist/vision/core/session.cjs +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js +19 -9
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +14 -13
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +33 -25
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detector.cjs +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js +28 -20
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js +23 -15
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-camera-stream.js +40 -57
- package/dist/vision/use-camera-stream.js.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +149 -8
- package/dist/vision.js +22 -21
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChatTurn.js","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["import { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n streaming && styles.streamingTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AIChatTurn.js","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["import { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n streaming && styles.streamingTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Alert.module.cjs","names":[],"sources":["../../../src/components/Alert/Alert.module.css"],"sourcesContent":[".alert {\n display: flex;\n align-items: flex-start;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border: 1px solid transparent;\n border-radius: var(--tempest-radius-lg);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n color: var(--tempest-text);\n width: 100%;\n}\n\n.content {\n flex: 1;\n min-width: 0;\n}\n\n.title {\n margin: 0;\n font-size: var(--tempest-text-base);\n font-weight: var(--tempest-weight-semibold);\n line-height: var(--tempest-leading-snug);\n}\n\n.description {\n margin: 2px 0 0 0;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-snug);\n color: var(--tempest-text-muted);\n}\n\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n margin-top: 1px;\n}\n\n.close {\n margin-left: auto;\n background: none;\n border: none;\n color: inherit;\n opacity: 0.7;\n padding: 4px;\n border-radius: var(--tempest-radius-sm);\n display: flex;\n align-items: center;\n transition:\n var(--tempest-transition-color),\n opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.close:hover {\n opacity: 1;\n background-color: rgba(15, 23, 42, 0.06);\n}\n\n.close:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n/* ============================================================\n * Tone × Appearance matrix\n * ============================================================ */\n\n/* neutral */\n.neutral {\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n border-color: var(--tempest-border);\n}\n.neutral.solid {\n background-color: var(--tempest-gray-700);\n color: #ffffff;\n border-color: transparent;\n}\n.neutral.outline {\n background-color: transparent;\n border-color: var(--tempest-border-strong);\n}\n\n/* info */\n.info {\n background-color: var(--tempest-info-bg);\n color: var(--tempest-info-fg);\n border-color: var(--tempest-info-border);\n}\n.info .description {\n color: var(--tempest-info-fg);\n opacity: 0.85;\n}\n.info.solid {\n background-color: var(--tempest-info-solid);\n color:
|
|
1
|
+
{"version":3,"file":"Alert.module.cjs","names":[],"sources":["../../../src/components/Alert/Alert.module.css"],"sourcesContent":[".alert {\n display: flex;\n align-items: flex-start;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border: 1px solid transparent;\n border-radius: var(--tempest-radius-lg);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n color: var(--tempest-text);\n width: 100%;\n}\n\n.content {\n flex: 1;\n min-width: 0;\n}\n\n.title {\n margin: 0;\n font-size: var(--tempest-text-base);\n font-weight: var(--tempest-weight-semibold);\n line-height: var(--tempest-leading-snug);\n}\n\n.description {\n margin: 2px 0 0 0;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-snug);\n color: var(--tempest-text-muted);\n}\n\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n margin-top: 1px;\n}\n\n.close {\n margin-left: auto;\n background: none;\n border: none;\n color: inherit;\n opacity: 0.7;\n padding: 4px;\n border-radius: var(--tempest-radius-sm);\n display: flex;\n align-items: center;\n transition:\n var(--tempest-transition-color),\n opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.close:hover {\n opacity: 1;\n background-color: rgba(15, 23, 42, 0.06);\n}\n\n.close:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n/* ============================================================\n * Tone × Appearance matrix\n * ============================================================ */\n\n/* neutral */\n.neutral {\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n border-color: var(--tempest-border);\n}\n.neutral.solid {\n background-color: var(--tempest-gray-700);\n color: #ffffff;\n border-color: transparent;\n}\n.neutral.outline {\n background-color: transparent;\n border-color: var(--tempest-border-strong);\n}\n\n/* info */\n.info {\n background-color: var(--tempest-info-bg);\n color: var(--tempest-info-fg);\n border-color: var(--tempest-info-border);\n}\n.info .description {\n color: var(--tempest-info-fg);\n opacity: 0.85;\n}\n.info.solid {\n background-color: var(--tempest-info-solid);\n color: var(--tempest-info-on-solid);\n border-color: transparent;\n}\n.info.solid .description {\n color: rgba(255, 255, 255, 0.9);\n}\n.info.outline {\n background-color: transparent;\n color: var(--tempest-info);\n border-color: var(--tempest-info);\n}\n\n/* success */\n.success {\n background-color: var(--tempest-success-bg);\n color: var(--tempest-success-fg);\n border-color: var(--tempest-success-border);\n}\n.success .description {\n color: var(--tempest-success-fg);\n opacity: 0.85;\n}\n.success.solid {\n background-color: var(--tempest-success-solid);\n color: var(--tempest-success-on-solid);\n border-color: transparent;\n}\n.success.solid .description {\n color: rgba(255, 255, 255, 0.9);\n}\n.success.outline {\n background-color: transparent;\n color: var(--tempest-success);\n border-color: var(--tempest-success);\n}\n\n/* warning */\n.warning {\n background-color: var(--tempest-warning-bg);\n color: var(--tempest-warning-fg);\n border-color: var(--tempest-warning-border);\n}\n.warning .description {\n color: var(--tempest-warning-fg);\n opacity: 0.85;\n}\n.warning.solid {\n background-color: var(--tempest-warning-solid);\n color: var(--tempest-warning-on-solid);\n border-color: transparent;\n}\n.warning.solid .description {\n color: rgba(255, 255, 255, 0.9);\n}\n.warning.outline {\n background-color: transparent;\n color: var(--tempest-warning);\n border-color: var(--tempest-warning);\n}\n\n/* danger */\n.danger {\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-danger-fg);\n border-color: var(--tempest-danger-border);\n}\n.danger .description {\n color: var(--tempest-danger-fg);\n opacity: 0.85;\n}\n.danger.solid {\n background-color: var(--tempest-danger-solid);\n color: var(--tempest-danger-on-solid);\n border-color: transparent;\n}\n.danger.solid .description {\n color: rgba(255, 255, 255, 0.9);\n}\n.danger.outline {\n background-color: transparent;\n color: var(--tempest-danger);\n border-color: var(--tempest-danger);\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Alert.module.js","names":[],"sources":["../../../src/components/Alert/Alert.module.css"],"sourcesContent":[".alert {\n display: flex;\n align-items: flex-start;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border: 1px solid transparent;\n border-radius: var(--tempest-radius-lg);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n color: var(--tempest-text);\n width: 100%;\n}\n\n.content {\n flex: 1;\n min-width: 0;\n}\n\n.title {\n margin: 0;\n font-size: var(--tempest-text-base);\n font-weight: var(--tempest-weight-semibold);\n line-height: var(--tempest-leading-snug);\n}\n\n.description {\n margin: 2px 0 0 0;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-snug);\n color: var(--tempest-text-muted);\n}\n\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n margin-top: 1px;\n}\n\n.close {\n margin-left: auto;\n background: none;\n border: none;\n color: inherit;\n opacity: 0.7;\n padding: 4px;\n border-radius: var(--tempest-radius-sm);\n display: flex;\n align-items: center;\n transition:\n var(--tempest-transition-color),\n opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.close:hover {\n opacity: 1;\n background-color: rgba(15, 23, 42, 0.06);\n}\n\n.close:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n/* ============================================================\n * Tone × Appearance matrix\n * ============================================================ */\n\n/* neutral */\n.neutral {\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n border-color: var(--tempest-border);\n}\n.neutral.solid {\n background-color: var(--tempest-gray-700);\n color: #ffffff;\n border-color: transparent;\n}\n.neutral.outline {\n background-color: transparent;\n border-color: var(--tempest-border-strong);\n}\n\n/* info */\n.info {\n background-color: var(--tempest-info-bg);\n color: var(--tempest-info-fg);\n border-color: var(--tempest-info-border);\n}\n.info .description {\n color: var(--tempest-info-fg);\n opacity: 0.85;\n}\n.info.solid {\n background-color: var(--tempest-info-solid);\n color:
|
|
1
|
+
{"version":3,"file":"Alert.module.js","names":[],"sources":["../../../src/components/Alert/Alert.module.css"],"sourcesContent":[".alert {\n display: flex;\n align-items: flex-start;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border: 1px solid transparent;\n border-radius: var(--tempest-radius-lg);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-snug);\n color: var(--tempest-text);\n width: 100%;\n}\n\n.content {\n flex: 1;\n min-width: 0;\n}\n\n.title {\n margin: 0;\n font-size: var(--tempest-text-base);\n font-weight: var(--tempest-weight-semibold);\n line-height: var(--tempest-leading-snug);\n}\n\n.description {\n margin: 2px 0 0 0;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-snug);\n color: var(--tempest-text-muted);\n}\n\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n margin-top: 1px;\n}\n\n.close {\n margin-left: auto;\n background: none;\n border: none;\n color: inherit;\n opacity: 0.7;\n padding: 4px;\n border-radius: var(--tempest-radius-sm);\n display: flex;\n align-items: center;\n transition:\n var(--tempest-transition-color),\n opacity var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.close:hover {\n opacity: 1;\n background-color: rgba(15, 23, 42, 0.06);\n}\n\n.close:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: var(--tempest-focus-ring-offset);\n}\n\n/* ============================================================\n * Tone × Appearance matrix\n * ============================================================ */\n\n/* neutral */\n.neutral {\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n border-color: var(--tempest-border);\n}\n.neutral.solid {\n background-color: var(--tempest-gray-700);\n color: #ffffff;\n border-color: transparent;\n}\n.neutral.outline {\n background-color: transparent;\n border-color: var(--tempest-border-strong);\n}\n\n/* info */\n.info {\n background-color: var(--tempest-info-bg);\n color: var(--tempest-info-fg);\n border-color: var(--tempest-info-border);\n}\n.info .description {\n color: var(--tempest-info-fg);\n opacity: 0.85;\n}\n.info.solid {\n background-color: var(--tempest-info-solid);\n color: var(--tempest-info-on-solid);\n border-color: transparent;\n}\n.info.solid .description {\n color: rgba(255, 255, 255, 0.9);\n}\n.info.outline {\n background-color: transparent;\n color: var(--tempest-info);\n border-color: var(--tempest-info);\n}\n\n/* success */\n.success {\n background-color: var(--tempest-success-bg);\n color: var(--tempest-success-fg);\n border-color: var(--tempest-success-border);\n}\n.success .description {\n color: var(--tempest-success-fg);\n opacity: 0.85;\n}\n.success.solid {\n background-color: var(--tempest-success-solid);\n color: var(--tempest-success-on-solid);\n border-color: transparent;\n}\n.success.solid .description {\n color: rgba(255, 255, 255, 0.9);\n}\n.success.outline {\n background-color: transparent;\n color: var(--tempest-success);\n border-color: var(--tempest-success);\n}\n\n/* warning */\n.warning {\n background-color: var(--tempest-warning-bg);\n color: var(--tempest-warning-fg);\n border-color: var(--tempest-warning-border);\n}\n.warning .description {\n color: var(--tempest-warning-fg);\n opacity: 0.85;\n}\n.warning.solid {\n background-color: var(--tempest-warning-solid);\n color: var(--tempest-warning-on-solid);\n border-color: transparent;\n}\n.warning.solid .description {\n color: rgba(255, 255, 255, 0.9);\n}\n.warning.outline {\n background-color: transparent;\n color: var(--tempest-warning);\n border-color: var(--tempest-warning);\n}\n\n/* danger */\n.danger {\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-danger-fg);\n border-color: var(--tempest-danger-border);\n}\n.danger .description {\n color: var(--tempest-danger-fg);\n opacity: 0.85;\n}\n.danger.solid {\n background-color: var(--tempest-danger-solid);\n color: var(--tempest-danger-on-solid);\n border-color: transparent;\n}\n.danger.solid .description {\n color: rgba(255, 255, 255, 0.9);\n}\n.danger.outline {\n background-color: transparent;\n color: var(--tempest-danger);\n border-color: var(--tempest-danger);\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-object-url.cjs"),n=require("../../audio/audio-output.cjs"),r=require("../../audio/duration.cjs"),i=require("./AudioPlayer.module.cjs");let a=require("react"),o=require("react/jsx-runtime"),s=require("lucide-react");var c={"pt-BR":{play:`Tocar`,pause:`Pausar`,seek:`Posição`},en:{play:`Play`,pause:`Pause`,seek:`Seek`}};function l({src:l,durationMs:u,sinkId:d,autoPlay:f=!1,loop:p=!1,locale:m=`pt-BR`,actions:h,onEnded:g,onError:_,disabled:v=!1,className:y,...b}){let x=c[m],S=(0,a.useRef)(null),[C,w]=(0,a.useState)(!1),[T,E]=(0,a.useState)(0),[D,O]=(0,a.useState)(1/0),k=(0,a.useRef)(!1),A=t.useObjectUrl(typeof l==`string`?null:l),j=typeof l==`string`?l:A,M=Number.isFinite(u)?u:D,N=Number.isFinite(M)&&M>0;(0,a.useEffect)(()=>{E(0),w(!1),O(1/0),k.current=!1},[j]),(0,a.useEffect)(()=>{d!==void 0&&n.setAudioOutput(S.current,d)},[d,j]);let P=()=>{let e=S.current;if(!e)return;if(Number.isFinite(e.duration)){O(e.duration*1e3);return}if(k.current||Number.isFinite(u))return;k.current=!0;let t=()=>{e.removeEventListener(`timeupdate`,t),Number.isFinite(e.duration)&&O(e.duration*1e3),e.currentTime=0};e.addEventListener(`timeupdate`,t),e.currentTime=1e101},F=()=>{let e=S.current;if(!(!e||!j)){if(e.paused){e.play().then(()=>w(!0)).catch(e=>_?.(e));return}e.pause(),w(!1)}},I=e=>{let t=S.current;!t||!N||(t.currentTime=e/1e3,E(e))};return(0,o.jsxs)(`div`,{className:e.cn(i.default.player,y),...b,children:[(0,o.jsx)(`audio`,{ref:S,src:j??void 0,loop:p,autoPlay:f,preload:`metadata`,onLoadedMetadata:P,onDurationChange:P,onTimeUpdate:()=>{let e=S.current;e&&E(e.currentTime*1e3)},onPlay:()=>w(!0),onPause:()=>w(!1),onEnded:()=>{w(!1),E(0),g?.()},onError:e=>_?.(e)}),(0,o.jsx)(`button`,{type:`button`,className:i.default.transport,onClick:F,disabled:v||!j,"aria-label":C?x.pause:x.play,title:C?x.pause:x.play,children:C?(0,o.jsx)(s.Pause,{size:16,"aria-hidden":!0}):(0,o.jsx)(s.Play,{size:16,"aria-hidden":!0})}),(0,o.jsx)(`input`,{type:`range`,className:i.default.seek,min:0,max:N?Math.round(M):0,step:100,value:Math.min(Math.round(T),N?Math.round(M):0),onChange:e=>I(Number(e.target.value)),disabled:v||!N,"aria-label":x.seek,"aria-valuetext":`${r.formatDuration(T)} / ${r.formatDuration(M)}`}),(0,o.jsxs)(`span`,{className:i.default.time,children:[(0,o.jsx)(`span`,{children:r.formatDuration(T)}),(0,o.jsx)(`span`,{"aria-hidden":`true`,children:`/`}),(0,o.jsx)(`span`,{children:r.formatDuration(M)})]}),h&&(0,o.jsx)(`span`,{className:i.default.actions,children:h})]})}exports.AudioPlayer=l;
|
|
2
|
+
//# sourceMappingURL=AudioPlayer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioPlayer.cjs","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["import { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { useObjectUrl as t } from "../../hooks/use-object-url.js";
|
|
3
|
+
import { setAudioOutput as n } from "../../audio/audio-output.js";
|
|
4
|
+
import { formatDuration as r } from "../../audio/duration.js";
|
|
5
|
+
import i from "./AudioPlayer.module.js";
|
|
6
|
+
import { useEffect as a, useRef as o, useState as s } from "react";
|
|
7
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
8
|
+
import { Pause as u, Play as d } from "lucide-react";
|
|
9
|
+
//#region src/components/AudioPlayer/AudioPlayer.tsx
|
|
10
|
+
var f = {
|
|
11
|
+
"pt-BR": {
|
|
12
|
+
play: "Tocar",
|
|
13
|
+
pause: "Pausar",
|
|
14
|
+
seek: "Posição"
|
|
15
|
+
},
|
|
16
|
+
en: {
|
|
17
|
+
play: "Play",
|
|
18
|
+
pause: "Pause",
|
|
19
|
+
seek: "Seek"
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function p({ src: p, durationMs: m, sinkId: h, autoPlay: g = !1, loop: _ = !1, locale: v = "pt-BR", actions: y, onEnded: b, onError: x, disabled: S = !1, className: C, ...w }) {
|
|
23
|
+
let T = f[v], E = o(null), [D, O] = s(!1), [k, A] = s(0), [j, M] = s(Infinity), N = o(!1), P = t(typeof p == "string" ? null : p), F = typeof p == "string" ? p : P, I = Number.isFinite(m) ? m : j, L = Number.isFinite(I) && I > 0;
|
|
24
|
+
a(() => {
|
|
25
|
+
A(0), O(!1), M(Infinity), N.current = !1;
|
|
26
|
+
}, [F]), a(() => {
|
|
27
|
+
h !== void 0 && n(E.current, h);
|
|
28
|
+
}, [h, F]);
|
|
29
|
+
let R = () => {
|
|
30
|
+
let e = E.current;
|
|
31
|
+
if (!e) return;
|
|
32
|
+
if (Number.isFinite(e.duration)) {
|
|
33
|
+
M(e.duration * 1e3);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (N.current || Number.isFinite(m)) return;
|
|
37
|
+
N.current = !0;
|
|
38
|
+
let t = () => {
|
|
39
|
+
e.removeEventListener("timeupdate", t), Number.isFinite(e.duration) && M(e.duration * 1e3), e.currentTime = 0;
|
|
40
|
+
};
|
|
41
|
+
e.addEventListener("timeupdate", t), e.currentTime = 1e101;
|
|
42
|
+
}, z = () => {
|
|
43
|
+
let e = E.current;
|
|
44
|
+
if (!(!e || !F)) {
|
|
45
|
+
if (e.paused) {
|
|
46
|
+
e.play().then(() => O(!0)).catch((e) => x?.(e));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
e.pause(), O(!1);
|
|
50
|
+
}
|
|
51
|
+
}, B = (e) => {
|
|
52
|
+
let t = E.current;
|
|
53
|
+
!t || !L || (t.currentTime = e / 1e3, A(e));
|
|
54
|
+
};
|
|
55
|
+
return /* @__PURE__ */ l("div", {
|
|
56
|
+
className: e(i.player, C),
|
|
57
|
+
...w,
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ c("audio", {
|
|
60
|
+
ref: E,
|
|
61
|
+
src: F ?? void 0,
|
|
62
|
+
loop: _,
|
|
63
|
+
autoPlay: g,
|
|
64
|
+
preload: "metadata",
|
|
65
|
+
onLoadedMetadata: R,
|
|
66
|
+
onDurationChange: R,
|
|
67
|
+
onTimeUpdate: () => {
|
|
68
|
+
let e = E.current;
|
|
69
|
+
e && A(e.currentTime * 1e3);
|
|
70
|
+
},
|
|
71
|
+
onPlay: () => O(!0),
|
|
72
|
+
onPause: () => O(!1),
|
|
73
|
+
onEnded: () => {
|
|
74
|
+
O(!1), A(0), b?.();
|
|
75
|
+
},
|
|
76
|
+
onError: (e) => x?.(e)
|
|
77
|
+
}),
|
|
78
|
+
/* @__PURE__ */ c("button", {
|
|
79
|
+
type: "button",
|
|
80
|
+
className: i.transport,
|
|
81
|
+
onClick: z,
|
|
82
|
+
disabled: S || !F,
|
|
83
|
+
"aria-label": D ? T.pause : T.play,
|
|
84
|
+
title: D ? T.pause : T.play,
|
|
85
|
+
children: c(D ? u : d, {
|
|
86
|
+
size: 16,
|
|
87
|
+
"aria-hidden": !0
|
|
88
|
+
})
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ c("input", {
|
|
91
|
+
type: "range",
|
|
92
|
+
className: i.seek,
|
|
93
|
+
min: 0,
|
|
94
|
+
max: L ? Math.round(I) : 0,
|
|
95
|
+
step: 100,
|
|
96
|
+
value: Math.min(Math.round(k), L ? Math.round(I) : 0),
|
|
97
|
+
onChange: (e) => B(Number(e.target.value)),
|
|
98
|
+
disabled: S || !L,
|
|
99
|
+
"aria-label": T.seek,
|
|
100
|
+
"aria-valuetext": `${r(k)} / ${r(I)}`
|
|
101
|
+
}),
|
|
102
|
+
/* @__PURE__ */ l("span", {
|
|
103
|
+
className: i.time,
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ c("span", { children: r(k) }),
|
|
106
|
+
/* @__PURE__ */ c("span", {
|
|
107
|
+
"aria-hidden": "true",
|
|
108
|
+
children: "/"
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ c("span", { children: r(I) })
|
|
111
|
+
]
|
|
112
|
+
}),
|
|
113
|
+
y && /* @__PURE__ */ c("span", {
|
|
114
|
+
className: i.actions,
|
|
115
|
+
children: y
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
export { p as AudioPlayer };
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=AudioPlayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioPlayer.js","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["import { Pause, Play } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\n\nimport { setAudioOutput } from \"@/audio/audio-output\";\nimport { formatDuration } from \"@/audio/duration\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./AudioPlayer.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface AudioPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a page that records twenty notes does not leak twenty\n * URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useAudioRecorder` always does.\n * See the note on the seek bar for why the element's own `duration` is not\n * trustworthy for a fresh recording.\n */\n durationMs?: number;\n /** Output device, from `useMediaDevices().audioOutputs`. Chromium only. */\n sinkId?: string;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the right of the times — a download button, a delete button. */\n actions?: React.ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": { play: \"Tocar\", pause: \"Pausar\", seek: \"Posição\" },\n en: { play: \"Play\", pause: \"Pause\", seek: \"Seek\" },\n} as const;\n\n/**\n * Playback transport for one clip: play/pause, a seek bar, elapsed and total time.\n *\n * Built around a real `<audio>` element rather than the SDK's `createAudioPlayer`,\n * which is a fire-and-forget handle for notification chimes and has no transport to\n * expose. Accepts a `Blob` directly, because the thing an app most often plays is the\n * recording it just made.\n *\n * The seek bar is a bare `<input type=\"range\">` rather than the SDK's `Slider`: that\n * component is a form field, with a label row and a value badge, and a transport wants\n * neither. The native input keeps the keyboard and screen-reader behaviour that\n * matters here for free.\n *\n * @example\n * const rec = useAudioRecorder(mic.stream);\n * {rec.recording && (\n * <AudioPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function AudioPlayer({\n src,\n durationMs,\n sinkId,\n autoPlay = false,\n loop = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioPlayerProps) {\n const strings = STRINGS[locale];\n const audio = useRef<HTMLAudioElement | null>(null);\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const probed = useRef(false);\n\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n /**\n * Total length, preferring what the caller knows.\n *\n * `MediaRecorder` writes WebM with no duration in the header, so a fresh recording\n * reports `Infinity` — which is why the recorder keeps its own clock and passes it\n * here. `durationMs` wins whenever it is finite; the element's value is the\n * fallback for a plain URL.\n */\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n useEffect(() => {\n if (sinkId === undefined) return;\n void setAudioOutput(audio.current, sinkId);\n }, [sinkId, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after which\n * it knows the real length. It is a hack with no alternative — the header simply\n * does not carry the value — and it runs at most once per source, only when the\n * caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = audio.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onSeeked = (): void => {\n node.removeEventListener(\"timeupdate\", onSeeked);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onSeeked);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = audio.current;\n if (!node || !url) return;\n if (node.paused) {\n void node\n .play()\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = audio.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n return (\n <div className={cn(styles.player, className)} {...rest}>\n <audio\n ref={audio}\n src={url ?? undefined}\n loop={loop}\n autoPlay={autoPlay}\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = audio.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onEnded={() => {\n setPlaying(false);\n setCurrentMs(0);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n />\n\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=`tempest_player_NEgFD`,t=`tempest_transport_-7WMK`,n=`tempest_seek_-iy4f`,r=`tempest_time_zNVm8`,i=`tempest_actions_QZ7Yn`,a={player:e,transport:t,seek:n,time:r,actions:i};exports.actions=i,exports.default=a,exports.player=e,exports.seek=n,exports.time=r,exports.transport=t;
|
|
2
|
+
//# sourceMappingURL=AudioPlayer.module.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioPlayer.module.cjs","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.module.css"],"sourcesContent":[".player {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.transport {\n position: relative;\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n.transport:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.transport:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .transport {\n transition: none;\n }\n}\n\n/*\n * The track is drawn on the input itself rather than with a wrapper and an overlay:\n * the native element already carries the keyboard behaviour and the value semantics,\n * and the only thing missing is the look.\n */\n.seek {\n flex: 1 1 auto;\n min-width: 3rem;\n height: 1.25rem;\n margin: 0;\n background: none;\n accent-color: var(--tempest-primary);\n cursor: pointer;\n}\n\n.seek:disabled {\n accent-color: var(--tempest-surface-3);\n cursor: not-allowed;\n}\n\n.seek:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n border-radius: var(--tempest-radius-sm);\n}\n\n/*\n * Tabular figures, so the elapsed time does not shift the seek bar sideways as the\n * digits change — a proportional `1` is narrower than a `0`, and at 10 updates a\n * second the whole row visibly jitters.\n */\n.time {\n display: inline-flex;\n flex: 0 0 auto;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-variant-numeric: tabular-nums;\n}\n\n.actions {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-1);\n}\n\n/* Touch hit-slop — 44×44 without changing the visual footprint, as `Button` does. */\n@media (pointer: coarse) {\n .transport::after {\n content: \"\";\n position: absolute;\n inset: -4px;\n }\n}\n\n@media (max-width: 480px) {\n .player {\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2);\n }\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/components/AudioPlayer/AudioPlayer.module.css
|
|
2
|
+
var e = "tempest_player_NEgFD", t = "tempest_transport_-7WMK", n = "tempest_seek_-iy4f", r = "tempest_time_zNVm8", i = "tempest_actions_QZ7Yn", a = {
|
|
3
|
+
player: e,
|
|
4
|
+
transport: t,
|
|
5
|
+
seek: n,
|
|
6
|
+
time: r,
|
|
7
|
+
actions: i
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { i as actions, a as default, e as player, n as seek, r as time, t as transport };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=AudioPlayer.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioPlayer.module.js","names":[],"sources":["../../../src/components/AudioPlayer/AudioPlayer.module.css"],"sourcesContent":[".player {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.transport {\n position: relative;\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n.transport:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.transport:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .transport {\n transition: none;\n }\n}\n\n/*\n * The track is drawn on the input itself rather than with a wrapper and an overlay:\n * the native element already carries the keyboard behaviour and the value semantics,\n * and the only thing missing is the look.\n */\n.seek {\n flex: 1 1 auto;\n min-width: 3rem;\n height: 1.25rem;\n margin: 0;\n background: none;\n accent-color: var(--tempest-primary);\n cursor: pointer;\n}\n\n.seek:disabled {\n accent-color: var(--tempest-surface-3);\n cursor: not-allowed;\n}\n\n.seek:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n border-radius: var(--tempest-radius-sm);\n}\n\n/*\n * Tabular figures, so the elapsed time does not shift the seek bar sideways as the\n * digits change — a proportional `1` is narrower than a `0`, and at 10 updates a\n * second the whole row visibly jitters.\n */\n.time {\n display: inline-flex;\n flex: 0 0 auto;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-variant-numeric: tabular-nums;\n}\n\n.actions {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-1);\n}\n\n/* Touch hit-slop — 44×44 without changing the visual footprint, as `Button` does. */\n@media (pointer: coarse) {\n .transport::after {\n content: \"\";\n position: absolute;\n inset: -4px;\n }\n}\n\n@media (max-width: 480px) {\n .player {\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2);\n }\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-media-permission.cjs"),n=require("../../audio/duration.cjs"),r=require("../AudioPlayer/AudioPlayer.cjs"),i=require("../../audio/audio-recorder.cjs"),a=require("../../audio/use-audio-recorder.cjs"),o=require("../../audio/use-microphone.cjs"),s=require("../../audio/wav.cjs"),c=require("./AudioRecorder.module.cjs");let l=require("react"),u=require("react/jsx-runtime"),d=require("lucide-react");var f={"pt-BR":{record:`Gravar`,stop:`Parar`,pause:`Pausar`,resume:`Continuar`,retake:`Gravar de novo`,level:`Nível do microfone`,asking:`Pedindo acesso ao microfone…`,denied:`Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.`,unsupported:`Este navegador não grava áudio.`,idleHint:`Toque em Gravar para começar.`,recording:`Gravando`,paused:`Pausado`,converting:`Convertendo…`},en:{record:`Record`,stop:`Stop`,pause:`Pause`,resume:`Resume`,retake:`Record again`,level:`Microphone level`,asking:`Requesting microphone access…`,denied:`Microphone access blocked. Allow it in site settings and reload.`,unsupported:`This browser cannot record audio.`,idleHint:`Press Record to start.`,recording:`Recording`,paused:`Paused`,converting:`Converting…`}};function p({onRecorded:p,maxDurationMs:m,deviceId:h,format:g=`native`,wavOptions:_,audioBitsPerSecond:v,review:y=!0,locale:b=`pt-BR`,footer:x,onError:S,disabled:C=!1,className:w,...T}){let E=f[b],D=t.useMediaPermission(`microphone`),O=o.useMicrophone({deviceId:h}),[k,A]=(0,l.useState)(!1),[j,M]=(0,l.useState)(null),[N,P]=(0,l.useState)(!0);(0,l.useEffect)(()=>P(i.isAudioRecordingSupported()),[]);let F=async e=>{if(g!==`wav`){M(e),p?.(e);return}A(!0);try{let t=await s.blobToWav(e.blob,_),n={...e,blob:t,mimeType:`audio/wav`};M(n),p?.(n)}catch(t){S?.(t),M(e),p?.(e)}finally{A(!1)}},I=a.useAudioRecorder(O.stream,{maxDurationMs:m,audioBitsPerSecond:v,onRecorded:e=>void F(e),onError:S}),[L,R]=(0,l.useState)(!1);(0,l.useEffect)(()=>{!L||!I.ready||I.status!==`idle`||(R(!1),I.start())},[L,I.ready,I.status,I]);let z=()=>{if(M(null),O.stream&&I.ready){I.start();return}R(!0),O.start()},B=()=>{M(null),I.cancel(),z()},V=I.status===`recording`||I.status===`paused`,H=D.state===`denied`;return N?(0,u.jsxs)(`div`,{className:e.cn(c.default.recorder,w),...T,children:[(0,u.jsxs)(`div`,{className:c.default.row,children:[!V&&(0,u.jsxs)(`button`,{type:`button`,className:c.default.record,onClick:j?B:z,disabled:C||H||k,"aria-label":j?E.retake:E.record,children:[j?(0,u.jsx)(d.RotateCcw,{size:16,"aria-hidden":!0}):(0,u.jsx)(d.Mic,{size:16,"aria-hidden":!0}),(0,u.jsx)(`span`,{children:j?E.retake:E.record})]}),V&&(0,u.jsxs)(u.Fragment,{children:[(0,u.jsxs)(`button`,{type:`button`,className:c.default.stop,onClick:()=>void I.stop(),"aria-label":E.stop,children:[(0,u.jsx)(d.Square,{size:14,"aria-hidden":!0}),(0,u.jsx)(`span`,{children:E.stop})]}),(0,u.jsx)(`button`,{type:`button`,className:c.default.secondary,onClick:I.status===`paused`?I.resume:I.pause,"aria-label":I.status===`paused`?E.resume:E.pause,children:I.status===`paused`?(0,u.jsx)(d.Play,{size:14,"aria-hidden":!0}):(0,u.jsx)(d.Pause,{size:14,"aria-hidden":!0})})]}),V&&(0,u.jsx)(`div`,{className:c.default.meter,role:`meter`,"aria-label":E.level,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(I.level*100),children:(0,u.jsx)(`span`,{className:c.default.meterFill,style:{transform:`scaleX(${I.level})`}})}),(0,u.jsxs)(`span`,{className:c.default.clock,children:[n.formatDuration(V?I.durationMs:j?.durationMs??0),m!==void 0&&(0,u.jsxs)(`span`,{className:c.default.max,children:[` / `,n.formatDuration(m)]})]})]}),(0,u.jsx)(`p`,{className:c.default.status,role:`status`,children:k?E.converting:O.status===`requesting`?E.asking:H?E.denied:O.error?O.error.message:I.status===`recording`?E.recording:I.status===`paused`?E.paused:j?``:E.idleHint}),y&&j&&(0,u.jsx)(r.AudioPlayer,{src:j.blob,durationMs:j.durationMs,locale:b,onError:S}),x&&(0,u.jsx)(`div`,{className:c.default.footer,children:x})]}):(0,u.jsx)(`div`,{className:e.cn(c.default.recorder,w),...T,children:(0,u.jsx)(`p`,{className:c.default.notice,children:E.unsupported})})}exports.AudioRecorder=p;
|
|
2
|
+
//# sourceMappingURL=AudioRecorder.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioRecorder.cjs","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["import { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { useMediaPermission as t } from "../../hooks/use-media-permission.js";
|
|
3
|
+
import { formatDuration as n } from "../../audio/duration.js";
|
|
4
|
+
import { AudioPlayer as r } from "../AudioPlayer/AudioPlayer.js";
|
|
5
|
+
import { isAudioRecordingSupported as i } from "../../audio/audio-recorder.js";
|
|
6
|
+
import { useAudioRecorder as a } from "../../audio/use-audio-recorder.js";
|
|
7
|
+
import { useMicrophone as o } from "../../audio/use-microphone.js";
|
|
8
|
+
import { blobToWav as s } from "../../audio/wav.js";
|
|
9
|
+
import c from "./AudioRecorder.module.js";
|
|
10
|
+
import { useEffect as l, useState as u } from "react";
|
|
11
|
+
import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
12
|
+
import { Mic as m, Pause as h, Play as g, RotateCcw as _, Square as v } from "lucide-react";
|
|
13
|
+
//#region src/components/AudioRecorder/AudioRecorder.tsx
|
|
14
|
+
var y = {
|
|
15
|
+
"pt-BR": {
|
|
16
|
+
record: "Gravar",
|
|
17
|
+
stop: "Parar",
|
|
18
|
+
pause: "Pausar",
|
|
19
|
+
resume: "Continuar",
|
|
20
|
+
retake: "Gravar de novo",
|
|
21
|
+
level: "Nível do microfone",
|
|
22
|
+
asking: "Pedindo acesso ao microfone…",
|
|
23
|
+
denied: "Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.",
|
|
24
|
+
unsupported: "Este navegador não grava áudio.",
|
|
25
|
+
idleHint: "Toque em Gravar para começar.",
|
|
26
|
+
recording: "Gravando",
|
|
27
|
+
paused: "Pausado",
|
|
28
|
+
converting: "Convertendo…"
|
|
29
|
+
},
|
|
30
|
+
en: {
|
|
31
|
+
record: "Record",
|
|
32
|
+
stop: "Stop",
|
|
33
|
+
pause: "Pause",
|
|
34
|
+
resume: "Resume",
|
|
35
|
+
retake: "Record again",
|
|
36
|
+
level: "Microphone level",
|
|
37
|
+
asking: "Requesting microphone access…",
|
|
38
|
+
denied: "Microphone access blocked. Allow it in site settings and reload.",
|
|
39
|
+
unsupported: "This browser cannot record audio.",
|
|
40
|
+
idleHint: "Press Record to start.",
|
|
41
|
+
recording: "Recording",
|
|
42
|
+
paused: "Paused",
|
|
43
|
+
converting: "Converting…"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function b({ onRecorded: b, maxDurationMs: x, deviceId: S, format: C = "native", wavOptions: w, audioBitsPerSecond: T, review: E = !0, locale: D = "pt-BR", footer: O, onError: k, disabled: A = !1, className: j, ...M }) {
|
|
47
|
+
let N = y[D], P = t("microphone"), F = o({ deviceId: S }), [I, L] = u(!1), [R, z] = u(null), [B, V] = u(!0);
|
|
48
|
+
l(() => V(i()), []);
|
|
49
|
+
let H = async (e) => {
|
|
50
|
+
if (C !== "wav") {
|
|
51
|
+
z(e), b?.(e);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
L(!0);
|
|
55
|
+
try {
|
|
56
|
+
let t = await s(e.blob, w), n = {
|
|
57
|
+
...e,
|
|
58
|
+
blob: t,
|
|
59
|
+
mimeType: "audio/wav"
|
|
60
|
+
};
|
|
61
|
+
z(n), b?.(n);
|
|
62
|
+
} catch (t) {
|
|
63
|
+
k?.(t), z(e), b?.(e);
|
|
64
|
+
} finally {
|
|
65
|
+
L(!1);
|
|
66
|
+
}
|
|
67
|
+
}, U = a(F.stream, {
|
|
68
|
+
maxDurationMs: x,
|
|
69
|
+
audioBitsPerSecond: T,
|
|
70
|
+
onRecorded: (e) => void H(e),
|
|
71
|
+
onError: k
|
|
72
|
+
}), [W, G] = u(!1);
|
|
73
|
+
l(() => {
|
|
74
|
+
!W || !U.ready || U.status !== "idle" || (G(!1), U.start());
|
|
75
|
+
}, [
|
|
76
|
+
W,
|
|
77
|
+
U.ready,
|
|
78
|
+
U.status,
|
|
79
|
+
U
|
|
80
|
+
]);
|
|
81
|
+
let K = () => {
|
|
82
|
+
if (z(null), F.stream && U.ready) {
|
|
83
|
+
U.start();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
G(!0), F.start();
|
|
87
|
+
}, q = () => {
|
|
88
|
+
z(null), U.cancel(), K();
|
|
89
|
+
}, J = U.status === "recording" || U.status === "paused", Y = P.state === "denied";
|
|
90
|
+
return B ? /* @__PURE__ */ p("div", {
|
|
91
|
+
className: e(c.recorder, j),
|
|
92
|
+
...M,
|
|
93
|
+
children: [
|
|
94
|
+
/* @__PURE__ */ p("div", {
|
|
95
|
+
className: c.row,
|
|
96
|
+
children: [
|
|
97
|
+
!J && /* @__PURE__ */ p("button", {
|
|
98
|
+
type: "button",
|
|
99
|
+
className: c.record,
|
|
100
|
+
onClick: R ? q : K,
|
|
101
|
+
disabled: A || Y || I,
|
|
102
|
+
"aria-label": R ? N.retake : N.record,
|
|
103
|
+
children: [f(R ? _ : m, {
|
|
104
|
+
size: 16,
|
|
105
|
+
"aria-hidden": !0
|
|
106
|
+
}), /* @__PURE__ */ f("span", { children: R ? N.retake : N.record })]
|
|
107
|
+
}),
|
|
108
|
+
J && /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ p("button", {
|
|
109
|
+
type: "button",
|
|
110
|
+
className: c.stop,
|
|
111
|
+
onClick: () => void U.stop(),
|
|
112
|
+
"aria-label": N.stop,
|
|
113
|
+
children: [/* @__PURE__ */ f(v, {
|
|
114
|
+
size: 14,
|
|
115
|
+
"aria-hidden": !0
|
|
116
|
+
}), /* @__PURE__ */ f("span", { children: N.stop })]
|
|
117
|
+
}), /* @__PURE__ */ f("button", {
|
|
118
|
+
type: "button",
|
|
119
|
+
className: c.secondary,
|
|
120
|
+
onClick: U.status === "paused" ? U.resume : U.pause,
|
|
121
|
+
"aria-label": U.status === "paused" ? N.resume : N.pause,
|
|
122
|
+
children: U.status === "paused" ? /* @__PURE__ */ f(g, {
|
|
123
|
+
size: 14,
|
|
124
|
+
"aria-hidden": !0
|
|
125
|
+
}) : /* @__PURE__ */ f(h, {
|
|
126
|
+
size: 14,
|
|
127
|
+
"aria-hidden": !0
|
|
128
|
+
})
|
|
129
|
+
})] }),
|
|
130
|
+
J && /* @__PURE__ */ f("div", {
|
|
131
|
+
className: c.meter,
|
|
132
|
+
role: "meter",
|
|
133
|
+
"aria-label": N.level,
|
|
134
|
+
"aria-valuemin": 0,
|
|
135
|
+
"aria-valuemax": 100,
|
|
136
|
+
"aria-valuenow": Math.round(U.level * 100),
|
|
137
|
+
children: /* @__PURE__ */ f("span", {
|
|
138
|
+
className: c.meterFill,
|
|
139
|
+
style: { transform: `scaleX(${U.level})` }
|
|
140
|
+
})
|
|
141
|
+
}),
|
|
142
|
+
/* @__PURE__ */ p("span", {
|
|
143
|
+
className: c.clock,
|
|
144
|
+
children: [n(J ? U.durationMs : R?.durationMs ?? 0), x !== void 0 && /* @__PURE__ */ p("span", {
|
|
145
|
+
className: c.max,
|
|
146
|
+
children: [" / ", n(x)]
|
|
147
|
+
})]
|
|
148
|
+
})
|
|
149
|
+
]
|
|
150
|
+
}),
|
|
151
|
+
/* @__PURE__ */ f("p", {
|
|
152
|
+
className: c.status,
|
|
153
|
+
role: "status",
|
|
154
|
+
children: I ? N.converting : F.status === "requesting" ? N.asking : Y ? N.denied : F.error ? F.error.message : U.status === "recording" ? N.recording : U.status === "paused" ? N.paused : R ? "" : N.idleHint
|
|
155
|
+
}),
|
|
156
|
+
E && R && /* @__PURE__ */ f(r, {
|
|
157
|
+
src: R.blob,
|
|
158
|
+
durationMs: R.durationMs,
|
|
159
|
+
locale: D,
|
|
160
|
+
onError: k
|
|
161
|
+
}),
|
|
162
|
+
O && /* @__PURE__ */ f("div", {
|
|
163
|
+
className: c.footer,
|
|
164
|
+
children: O
|
|
165
|
+
})
|
|
166
|
+
]
|
|
167
|
+
}) : /* @__PURE__ */ f("div", {
|
|
168
|
+
className: e(c.recorder, j),
|
|
169
|
+
...M,
|
|
170
|
+
children: /* @__PURE__ */ f("p", {
|
|
171
|
+
className: c.notice,
|
|
172
|
+
children: N.unsupported
|
|
173
|
+
})
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
export { b as AudioRecorder };
|
|
178
|
+
|
|
179
|
+
//# sourceMappingURL=AudioRecorder.js.map
|