@ephia/dova-sdk 1.0.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 +89 -0
- package/dist/EphiaBinding-BvRmlqqC.d.ts +36 -0
- package/dist/EphiaFloatingButton-CxiF86VW.d.ts +65 -0
- package/dist/EphiaTextarea-B4_CAVUg.d.ts +183 -0
- package/dist/NativeBinding-ChG0GeSz.d.ts +53 -0
- package/dist/TargetBinding-BKGQwUMc.d.ts +89 -0
- package/dist/TiptapBinding-B-agfV2H.d.ts +45 -0
- package/dist/Transport-zdeA4Pou.d.ts +63 -0
- package/dist/audio-state-kZ3KSvux.d.ts +39 -0
- package/dist/chunk-35AJK2IO.js +1 -0
- package/dist/chunk-35AJK2IO.js.map +1 -0
- package/dist/chunk-3LXZODL4.js +886 -0
- package/dist/chunk-3LXZODL4.js.map +1 -0
- package/dist/chunk-5IK5TLSK.js +67 -0
- package/dist/chunk-5IK5TLSK.js.map +1 -0
- package/dist/chunk-7E43RY75.js +9 -0
- package/dist/chunk-7E43RY75.js.map +1 -0
- package/dist/chunk-A5UEXJ5R.js +183 -0
- package/dist/chunk-A5UEXJ5R.js.map +1 -0
- package/dist/chunk-AEE554FT.js +51 -0
- package/dist/chunk-AEE554FT.js.map +1 -0
- package/dist/chunk-DIEWY3IT.js +1332 -0
- package/dist/chunk-DIEWY3IT.js.map +1 -0
- package/dist/chunk-EGIAN7FH.js +18 -0
- package/dist/chunk-EGIAN7FH.js.map +1 -0
- package/dist/chunk-EMOEAPVU.js +486 -0
- package/dist/chunk-EMOEAPVU.js.map +1 -0
- package/dist/chunk-IDC7FHIZ.js +40 -0
- package/dist/chunk-IDC7FHIZ.js.map +1 -0
- package/dist/chunk-ITJFN3VM.js +601 -0
- package/dist/chunk-ITJFN3VM.js.map +1 -0
- package/dist/chunk-K24GNU27.js +22 -0
- package/dist/chunk-K24GNU27.js.map +1 -0
- package/dist/chunk-LXMCRXXF.js +778 -0
- package/dist/chunk-LXMCRXXF.js.map +1 -0
- package/dist/chunk-MJCEOOLW.js +122 -0
- package/dist/chunk-MJCEOOLW.js.map +1 -0
- package/dist/chunk-N7U5M3VZ.js +33 -0
- package/dist/chunk-N7U5M3VZ.js.map +1 -0
- package/dist/chunk-PSYX674B.js +27 -0
- package/dist/chunk-PSYX674B.js.map +1 -0
- package/dist/chunk-RFQRV7ML.js +33 -0
- package/dist/chunk-RFQRV7ML.js.map +1 -0
- package/dist/chunk-THNHRV2B.js +18 -0
- package/dist/chunk-THNHRV2B.js.map +1 -0
- package/dist/chunk-VSLGR64U.js +62 -0
- package/dist/chunk-VSLGR64U.js.map +1 -0
- package/dist/chunk-W2ZP674X.js +346 -0
- package/dist/chunk-W2ZP674X.js.map +1 -0
- package/dist/chunk-YWZUMUYE.js +695 -0
- package/dist/chunk-YWZUMUYE.js.map +1 -0
- package/dist/client-options-Uo6jXO8k.d.ts +64 -0
- package/dist/connection-state-Bk33YprE.d.ts +32 -0
- package/dist/core/bindings/index.d.ts +24 -0
- package/dist/core/bindings/index.js +1025 -0
- package/dist/core/bindings/index.js.map +1 -0
- package/dist/core/index.d.ts +383 -0
- package/dist/core/index.js +1284 -0
- package/dist/core/index.js.map +1 -0
- package/dist/createEphiaClient-BhdZ183V.d.ts +69 -0
- package/dist/devices/speechmike/index.d.ts +148 -0
- package/dist/devices/speechmike/index.js +40 -0
- package/dist/devices/speechmike/index.js.map +1 -0
- package/dist/headless/index.d.ts +10 -0
- package/dist/headless/index.js +25 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/rich-editor/index.d.ts +46 -0
- package/dist/rich-editor/index.js +13 -0
- package/dist/rich-editor/index.js.map +1 -0
- package/dist/schema-B2ycPlNB.d.ts +87 -0
- package/dist/session-APaXR48R.d.ts +12 -0
- package/dist/shared/index.d.ts +16 -0
- package/dist/shared/index.js +30 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/style.css +1093 -0
- package/dist/testing/index.d.ts +84 -0
- package/dist/testing/index.js +36 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-D5SXPSwR.d.ts +32 -0
- package/dist/ui/index.d.ts +30 -0
- package/dist/ui/index.js +34 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/useEphiaSpeechMike-CjD7DWnh.d.ts +64 -0
- package/package.json +110 -0
- package/src/core/audio/audio-worklet-source.ts +30 -0
- package/src/core/audio/index.ts +3 -0
- package/src/core/audio/voice-level-meter.test.ts +27 -0
- package/src/core/audio/voice-level-meter.ts +270 -0
- package/src/core/bindings/EphiaBinding.ts +41 -0
- package/src/core/bindings/SegmentBindingBridge.test.ts +422 -0
- package/src/core/bindings/SegmentBindingBridge.ts +377 -0
- package/src/core/bindings/TargetBinding.ts +142 -0
- package/src/core/bindings/adapters/NativeAdapter.test.ts +85 -0
- package/src/core/bindings/adapters/NativeAdapter.ts +216 -0
- package/src/core/bindings/adapters/ProseMirrorAdapter.ts +231 -0
- package/src/core/bindings/adapters/index.ts +2 -0
- package/src/core/bindings/binding-factory.ts +78 -0
- package/src/core/bindings/detect-editor-type.ts +87 -0
- package/src/core/bindings/index.ts +13 -0
- package/src/core/bindings/insertion-boundary.test.ts +38 -0
- package/src/core/bindings/insertion-boundary.ts +26 -0
- package/src/core/bindings/native/NativeBinding.test.ts +277 -0
- package/src/core/bindings/native/NativeBinding.ts +239 -0
- package/src/core/bindings/resolver.ts +18 -0
- package/src/core/bindings/targets/codemirror.binding.ts +293 -0
- package/src/core/bindings/targets/contenteditable.binding.ts +452 -0
- package/src/core/bindings/targets/index.ts +10 -0
- package/src/core/bindings/targets/monaco.binding.ts +315 -0
- package/src/core/bindings/targets/tiptap.binding.test.ts +417 -0
- package/src/core/bindings/targets/tiptap.binding.ts +1192 -0
- package/src/core/bindings/tiptap/TiptapBinding.test.ts +63 -0
- package/src/core/bindings/tiptap/TiptapBinding.ts +464 -0
- package/src/core/bindings/types.ts +41 -0
- package/src/core/client/EphiaAudioClient.ts +654 -0
- package/src/core/client/audio-capture.ts +263 -0
- package/src/core/client/client-options.ts +39 -0
- package/src/core/client/client-state.ts +18 -0
- package/src/core/client/constants.ts +23 -0
- package/src/core/client/session-api.ts +415 -0
- package/src/core/connection/connection-state.ts +78 -0
- package/src/core/connection/index.ts +6 -0
- package/src/core/index.ts +47 -0
- package/src/core/operations/textToDocumentOperations.test.ts +69 -0
- package/src/core/operations/textToDocumentOperations.ts +92 -0
- package/src/core/runtime/DictationRuntime.test.ts +578 -0
- package/src/core/runtime/DictationRuntime.ts +434 -0
- package/src/core/runtime/TranscriptApplier.test.ts +355 -0
- package/src/core/runtime/TranscriptApplier.ts +229 -0
- package/src/core/runtime/index.ts +18 -0
- package/src/core/session/index.ts +2 -0
- package/src/core/session/session-machine.test.ts +16 -0
- package/src/core/session/session-machine.ts +59 -0
- package/src/core/targets/EditorContextCollector.ts +71 -0
- package/src/core/targets/TargetManager.test.ts +194 -0
- package/src/core/targets/TargetManager.ts +194 -0
- package/src/core/targets/index.ts +10 -0
- package/src/core/text-processing/index.ts +11 -0
- package/src/core/text-processing/overlap.test.ts +35 -0
- package/src/core/text-processing/overlap.ts +101 -0
- package/src/core/text-processing/voice-formatting.normalizer.test.ts +132 -0
- package/src/core/text-processing/voice-formatting.normalizer.ts +284 -0
- package/src/core/transcript/client-transcript.reducer.ts +366 -0
- package/src/core/transcript/client-transcript.state.ts +25 -0
- package/src/core/transcript/index.ts +19 -0
- package/src/core/transcript/transcript.assembler.test.ts +205 -0
- package/src/core/transcript/transcript.assembler.ts +152 -0
- package/src/core/transcript/transcript.reducer.test.ts +199 -0
- package/src/core/transcript/transcript.reducer.ts +771 -0
- package/src/core/transcript/transcript.state.ts +123 -0
- package/src/core/transport/LiveKitTransport.publish.test.ts +226 -0
- package/src/core/transport/LiveKitTransport.ts +459 -0
- package/src/core/transport/MockTransport.ts +231 -0
- package/src/core/transport/Transport.ts +82 -0
- package/src/debug/sdk-debug-collector.ts +79 -0
- package/src/devices/index.ts +2 -0
- package/src/devices/speechmike/__tests__/EphiaSpeechMikeProvider.test.tsx +99 -0
- package/src/devices/speechmike/__tests__/speechmike-audio-resolver.test.ts +96 -0
- package/src/devices/speechmike/__tests__/speechmike-button-router.test.ts +66 -0
- package/src/devices/speechmike/__tests__/speechmike-device-manager.test.ts +201 -0
- package/src/devices/speechmike/__tests__/speechmike-led-controller.test.ts +68 -0
- package/src/devices/speechmike/browser.ts +80 -0
- package/src/devices/speechmike/constants.ts +74 -0
- package/src/devices/speechmike/dictation-support-loader.ts +81 -0
- package/src/devices/speechmike/index.ts +11 -0
- package/src/devices/speechmike/react/EphiaSpeechMikeContext.ts +34 -0
- package/src/devices/speechmike/react/EphiaSpeechMikeProvider.tsx +287 -0
- package/src/devices/speechmike/react/useEphiaSpeechMike.ts +26 -0
- package/src/devices/speechmike/speechmike-audio-resolver.ts +58 -0
- package/src/devices/speechmike/speechmike-button-router.ts +73 -0
- package/src/devices/speechmike/speechmike-device-manager.ts +461 -0
- package/src/devices/speechmike/speechmike-led-controller.ts +78 -0
- package/src/devices/speechmike/types.ts +96 -0
- package/src/dictation_support.d.ts +31 -0
- package/src/global.d.ts +10 -0
- package/src/headless/createEphiaClient.ts +220 -0
- package/src/headless/index.ts +18 -0
- package/src/index.ts +89 -0
- package/src/react/EphiaAuto.tsx +87 -0
- package/src/react/components/EphiaDictationButton.tsx +88 -0
- package/src/react/components/EphiaStatusBar.tsx +59 -0
- package/src/react/components/EphiaTextarea.tsx +295 -0
- package/src/react/ephia-react.css +318 -0
- package/src/react/hooks/targets/index.ts +3 -0
- package/src/react/hooks/targets/useEphiaCodemirror.ts +35 -0
- package/src/react/hooks/targets/useEphiaMonaco.ts +35 -0
- package/src/react/hooks/targets/useEphiaTiptap.ts +23 -0
- package/src/react/hooks/useEphia.lifecycle.test.tsx +389 -0
- package/src/react/hooks/useEphia.ts +367 -0
- package/src/react/hooks/useEphiaDiscardTarget.ts +53 -0
- package/src/react/hooks/useEphiaServerEvent.ts +33 -0
- package/src/react/hooks/useEphiaTarget.ts +47 -0
- package/src/react/hooks/useEphiaTranscript.ts +22 -0
- package/src/react/index.ts +58 -0
- package/src/react/provider/EphiaContext.ts +63 -0
- package/src/react/provider/EphiaInternalContext.ts +32 -0
- package/src/react/provider/EphiaProvider.tsx +373 -0
- package/src/react/registry/binding-factory.ts +7 -0
- package/src/react/registry/detect-editor-type.ts +2 -0
- package/src/react/registry/events.ts +37 -0
- package/src/react/registry/registries/CodeMirrorInstanceRegistry.ts +24 -0
- package/src/react/registry/registries/MonacoInstanceRegistry.ts +23 -0
- package/src/react/registry/registries/TargetRegistry.ts +327 -0
- package/src/react/registry/registries/TiptapInstanceRegistry.ts +43 -0
- package/src/react/registry/registries/index.ts +5 -0
- package/src/react/store/create-ephia-store.ts +36 -0
- package/src/react/store/types.ts +41 -0
- package/src/react/utils/flash-range.ts +24 -0
- package/src/react/utils/index.ts +1 -0
- package/src/rich-editor/adapters/tiptap.test.ts +86 -0
- package/src/rich-editor/adapters/tiptap.ts +23 -0
- package/src/rich-editor/index.ts +3 -0
- package/src/rich-editor/types.ts +24 -0
- package/src/rich-editor/use-ephia-rich-editor.test.tsx +202 -0
- package/src/rich-editor/use-ephia-rich-editor.ts +47 -0
- package/src/shared/config/endpoint.test.ts +45 -0
- package/src/shared/config/endpoint.ts +39 -0
- package/src/shared/config/schema.ts +32 -0
- package/src/shared/effective-text.ts +13 -0
- package/src/shared/errors/EphiaSdkError.ts +54 -0
- package/src/shared/errors/messages.ts +40 -0
- package/src/shared/index.ts +27 -0
- package/src/shared/state/audio-state.ts +45 -0
- package/src/shared/state/index.ts +2 -0
- package/src/shared/store/document-store.ts +32 -0
- package/src/shared/store/index.ts +2 -0
- package/src/shared/types/editors.ts +28 -0
- package/src/shared/types/session.ts +12 -0
- package/src/style.css +2 -0
- package/src/testing/index.tsx +60 -0
- package/src/ui/assets/ephia-logo.svg +4 -0
- package/src/ui/components/EphiaLogo.tsx +77 -0
- package/src/ui/index.ts +24 -0
- package/src/ui/primitives/Button.tsx +53 -0
- package/src/ui/primitives/Spinner.tsx +21 -0
- package/src/ui/primitives/index.ts +5 -0
- package/src/ui/recorder/EphiaFloatingButton.tsx +489 -0
- package/src/ui/recorder/MinimalProcessingBars.tsx +122 -0
- package/src/ui/recorder/StandardIntensityVisualizer.tsx +148 -0
- package/src/ui/recorder/appearance.ts +9 -0
- package/src/ui/recorder/index.ts +8 -0
- package/src/ui/theme.css +775 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { EphiaProvider } from "../../react/provider/EphiaProvider";
|
|
5
|
+
import { useOptionalEphiaContext } from "../../react/provider/EphiaContext";
|
|
6
|
+
import { useEphia, useEphiaAudioLevel } from "../../react/hooks/useEphia";
|
|
7
|
+
import type { EphiaStatus } from "../../react/store/types";
|
|
8
|
+
import type { EphiaSessionOptions } from "../../shared/types/session";
|
|
9
|
+
import type { Transport } from "../../core/transport/Transport";
|
|
10
|
+
import {
|
|
11
|
+
EphiaLogo,
|
|
12
|
+
EPHIA_BRAND_PURPLE,
|
|
13
|
+
logoWidthFromHeight,
|
|
14
|
+
} from "../components/EphiaLogo";
|
|
15
|
+
import {
|
|
16
|
+
EPHIA_FLOATING_BUTTON_RADIUS_CSS,
|
|
17
|
+
type EphiaFloatingButtonBorderRadius,
|
|
18
|
+
} from "./appearance";
|
|
19
|
+
import { MinimalProcessingBars } from "./MinimalProcessingBars";
|
|
20
|
+
import { StandardIntensityVisualizer } from "./StandardIntensityVisualizer";
|
|
21
|
+
|
|
22
|
+
export type { EphiaFloatingButtonBorderRadius } from "./appearance";
|
|
23
|
+
|
|
24
|
+
const EPHIA_APP_URL = "https://ephia.app";
|
|
25
|
+
|
|
26
|
+
function EphiaExternalLinkIcon() {
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
className="ephia-transcribe-poweredby-icon"
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
aria-hidden
|
|
32
|
+
focusable="false"
|
|
33
|
+
>
|
|
34
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
35
|
+
<path d="M15 3h6v6" />
|
|
36
|
+
<path d="M10 14 21 3" />
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ── Types ───────────────────────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
export type EphiaFloatingButtonPosition =
|
|
44
|
+
| "bottom-right"
|
|
45
|
+
| "bottom-left"
|
|
46
|
+
| "bottom-center"
|
|
47
|
+
| "top-right"
|
|
48
|
+
| "top-left"
|
|
49
|
+
| "top-center";
|
|
50
|
+
export type EphiaFloatingButtonTheme = "light" | "dark";
|
|
51
|
+
export type EphiaFloatingButtonSize = "S" | "M" | "L";
|
|
52
|
+
export type EphiaFloatingButtonVariant = "minimal" | "standard";
|
|
53
|
+
export type EphiaFloatingButtonInteractionMode = "toggle" | "push-to-talk";
|
|
54
|
+
export type EphiaFloatingButtonCustomColors = {
|
|
55
|
+
/** Ink color: logo, visualiseur Standard, texte. Any valid CSS color. */
|
|
56
|
+
primary: string;
|
|
57
|
+
/** Background color. Any valid CSS color. */
|
|
58
|
+
secondary: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface EphiaFloatingButtonProps {
|
|
62
|
+
/**
|
|
63
|
+
* API URL. Optional override (ex. http://localhost:8000 en dev).
|
|
64
|
+
* Sans override : EPHIA_SDK_ENDPOINT, sinon https://api.ephia.app.
|
|
65
|
+
*/
|
|
66
|
+
apiUrl?: string;
|
|
67
|
+
apiKey?: string;
|
|
68
|
+
bearerToken?: string;
|
|
69
|
+
clientType?: string;
|
|
70
|
+
transport?: Transport;
|
|
71
|
+
position?: EphiaFloatingButtonPosition;
|
|
72
|
+
theme?: EphiaFloatingButtonTheme;
|
|
73
|
+
size?: EphiaFloatingButtonSize;
|
|
74
|
+
variant?: EphiaFloatingButtonVariant;
|
|
75
|
+
colors?: EphiaFloatingButtonCustomColors;
|
|
76
|
+
/** Corner radius of the dictation control. Default `md`. */
|
|
77
|
+
borderRadius?: EphiaFloatingButtonBorderRadius;
|
|
78
|
+
/** Interaction model. `"toggle"` (default): click to start, click to stop. `"push-to-talk"`: hold to record, release to stop. */
|
|
79
|
+
interactionMode?: EphiaFloatingButtonInteractionMode;
|
|
80
|
+
className?: string;
|
|
81
|
+
style?: React.CSSProperties;
|
|
82
|
+
sessionOptions?: EphiaSessionOptions;
|
|
83
|
+
onError?: (error: { code: string; message: string }) => void;
|
|
84
|
+
onStatusChange?: (status: EphiaStatus) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const SIZE_CONFIG: Record<EphiaFloatingButtonSize, { logoHeightPx: number }> = {
|
|
88
|
+
S: { logoHeightPx: 24 },
|
|
89
|
+
M: { logoHeightPx: 32 },
|
|
90
|
+
L: { logoHeightPx: 44 },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Logo légèrement réduit en pilule (full) pour l’équilibre visuel. */
|
|
94
|
+
const LOGO_SCALE_WHEN_RADIUS_FULL = 0.92;
|
|
95
|
+
|
|
96
|
+
const POSITION_STYLE: Record<EphiaFloatingButtonPosition, React.CSSProperties> = {
|
|
97
|
+
"bottom-right": { bottom: 24, right: 24 },
|
|
98
|
+
"bottom-left": { bottom: 24, left: 24 },
|
|
99
|
+
"bottom-center": { bottom: 24, left: "50%", transform: "translateX(-50%)" },
|
|
100
|
+
"top-right": { top: 24, right: 24 },
|
|
101
|
+
"top-left": { top: 24, left: 24 },
|
|
102
|
+
"top-center": { top: 24, left: "50%", transform: "translateX(-50%)" },
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/* ── Inner button — requires Provider in scope ───────────────────── */
|
|
106
|
+
|
|
107
|
+
interface InnerProps {
|
|
108
|
+
position: EphiaFloatingButtonPosition;
|
|
109
|
+
theme: EphiaFloatingButtonTheme;
|
|
110
|
+
size: EphiaFloatingButtonSize;
|
|
111
|
+
variant: EphiaFloatingButtonVariant;
|
|
112
|
+
borderRadius: EphiaFloatingButtonBorderRadius;
|
|
113
|
+
interactionMode: EphiaFloatingButtonInteractionMode;
|
|
114
|
+
colors?: EphiaFloatingButtonCustomColors;
|
|
115
|
+
className?: string;
|
|
116
|
+
style?: React.CSSProperties;
|
|
117
|
+
onError?: (error: { code: string; message: string }) => void;
|
|
118
|
+
onStatusChange?: (status: EphiaStatus) => void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function EphiaFloatingButtonInner({
|
|
122
|
+
position,
|
|
123
|
+
theme,
|
|
124
|
+
size,
|
|
125
|
+
variant,
|
|
126
|
+
borderRadius,
|
|
127
|
+
interactionMode,
|
|
128
|
+
colors,
|
|
129
|
+
className,
|
|
130
|
+
style,
|
|
131
|
+
onError,
|
|
132
|
+
onStatusChange,
|
|
133
|
+
}: InnerProps) {
|
|
134
|
+
const { status, isRecording, isProcessing, start, stop, error, activeTargetId } = useEphia();
|
|
135
|
+
const audio = useEphiaAudioLevel();
|
|
136
|
+
|
|
137
|
+
// Traitement sans micro ouvert : bouton en attente.
|
|
138
|
+
const isArming = isProcessing;
|
|
139
|
+
const voiceLevel = Math.min(1, Math.max(0, audio?.level ?? audio?.rms ?? 0));
|
|
140
|
+
const isLiveSession =
|
|
141
|
+
isRecording || (!!audio?.localAudioPublished && !!audio?.micReady);
|
|
142
|
+
|
|
143
|
+
// Callbacks — compare by value, not reference, to catch same-code re-errors
|
|
144
|
+
const lastErrorRef = useRef<{ code: string; message: string } | null>(null);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (error && (lastErrorRef.current?.code !== error.code || lastErrorRef.current?.message !== error.message)) {
|
|
147
|
+
lastErrorRef.current = error;
|
|
148
|
+
onError?.(error);
|
|
149
|
+
}
|
|
150
|
+
}, [error, onError]);
|
|
151
|
+
|
|
152
|
+
const lastStatusRef = useRef(status);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (status !== lastStatusRef.current) {
|
|
155
|
+
lastStatusRef.current = status;
|
|
156
|
+
onStatusChange?.(status);
|
|
157
|
+
}
|
|
158
|
+
}, [status, onStatusChange]);
|
|
159
|
+
|
|
160
|
+
// Screen reader status
|
|
161
|
+
const statusAnnouncement =
|
|
162
|
+
isLiveSession ? "Dictée en cours" :
|
|
163
|
+
status === "processing" ? "Traitement" :
|
|
164
|
+
status === "error" ? "Erreur de dictée" : "";
|
|
165
|
+
|
|
166
|
+
// Colors — validate non-empty strings to avoid silent CSS failures
|
|
167
|
+
const isLight = theme === "light";
|
|
168
|
+
const isValidColor = (c: string | undefined): boolean => Boolean(c?.trim());
|
|
169
|
+
const hasCustomColors = isValidColor(colors?.primary) && isValidColor(colors?.secondary);
|
|
170
|
+
const swapForLight = hasCustomColors && isLight;
|
|
171
|
+
const effectivePrimary = hasCustomColors
|
|
172
|
+
? (swapForLight ? colors!.secondary.trim() : colors!.primary.trim())
|
|
173
|
+
: (isLight ? "#ffffff" : "#262626");
|
|
174
|
+
const effectiveSecondary = hasCustomColors
|
|
175
|
+
? (swapForLight ? colors!.primary.trim() : colors!.secondary.trim())
|
|
176
|
+
: null;
|
|
177
|
+
|
|
178
|
+
const safeSize = (size && size in SIZE_CONFIG) ? size : "M";
|
|
179
|
+
const { logoHeightPx: baseLogoHeightPx } = SIZE_CONFIG[safeSize];
|
|
180
|
+
const logoHeightPx =
|
|
181
|
+
borderRadius === "full"
|
|
182
|
+
? Math.round(baseLogoHeightPx * LOGO_SCALE_WHEN_RADIUS_FULL)
|
|
183
|
+
: baseLogoHeightPx;
|
|
184
|
+
const logoWidthPx = logoWidthFromHeight(logoHeightPx);
|
|
185
|
+
|
|
186
|
+
const isStandardVariant = variant === "standard";
|
|
187
|
+
const showTimer = isStandardVariant && isRecording;
|
|
188
|
+
const showProcessing = isStandardVariant && isArming && !isRecording;
|
|
189
|
+
const showStandardPrompt = isStandardVariant && !isRecording && !isArming;
|
|
190
|
+
const [sessionElapsedMs, setSessionElapsedMs] = useState(0);
|
|
191
|
+
const sessionStartRef = useRef<number | null>(null);
|
|
192
|
+
const timerMeasureRef = useRef<HTMLSpanElement | null>(null);
|
|
193
|
+
const [logoSlotWidthPx, setLogoSlotWidthPx] = useState(logoHeightPx);
|
|
194
|
+
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (showTimer) {
|
|
197
|
+
sessionStartRef.current = Date.now();
|
|
198
|
+
setSessionElapsedMs(0);
|
|
199
|
+
const id = setInterval(() => {
|
|
200
|
+
if (sessionStartRef.current != null) setSessionElapsedMs(Date.now() - sessionStartRef.current);
|
|
201
|
+
}, 200);
|
|
202
|
+
return () => clearInterval(id);
|
|
203
|
+
}
|
|
204
|
+
sessionStartRef.current = null;
|
|
205
|
+
}, [showTimer]);
|
|
206
|
+
|
|
207
|
+
const timerLabel = useMemo(() => {
|
|
208
|
+
const s = Math.max(0, Math.floor(sessionElapsedMs / 1000));
|
|
209
|
+
return `${Math.floor(s / 60).toString().padStart(2, "0")}:${(s % 60).toString().padStart(2, "0")}`;
|
|
210
|
+
}, [sessionElapsedMs]);
|
|
211
|
+
|
|
212
|
+
// useLayoutEffect is intentional: measures DOM before paint to avoid width flicker.
|
|
213
|
+
// Only runs when recording (showTimer=true), so SSR hydration mismatch is not a concern.
|
|
214
|
+
useLayoutEffect(() => {
|
|
215
|
+
if (!showTimer) { setLogoSlotWidthPx(logoHeightPx); return; }
|
|
216
|
+
const el = timerMeasureRef.current;
|
|
217
|
+
const measured = el ? Math.ceil(el.getBoundingClientRect().width) : 0;
|
|
218
|
+
setLogoSlotWidthPx(Math.max(logoHeightPx, measured + 12));
|
|
219
|
+
}, [showTimer, timerLabel, logoHeightPx]);
|
|
220
|
+
|
|
221
|
+
const logoVariant = hasCustomColors ? "mono" : "color";
|
|
222
|
+
const waveformColor = hasCustomColors ? effectivePrimary : EPHIA_BRAND_PURPLE;
|
|
223
|
+
const radiusCss =
|
|
224
|
+
EPHIA_FLOATING_BUTTON_RADIUS_CSS[borderRadius] ?? EPHIA_FLOATING_BUTTON_RADIUS_CSS.md;
|
|
225
|
+
|
|
226
|
+
// useState initializer so SSR gives false without crashing, client gets real value on mount
|
|
227
|
+
const [reducedMotion] = useState(
|
|
228
|
+
() => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div
|
|
233
|
+
className="ephia-transcribe-floating-root"
|
|
234
|
+
data-ephia-control="true"
|
|
235
|
+
style={{ ...POSITION_STYLE[position], ...style }}
|
|
236
|
+
>
|
|
237
|
+
{/* Screen-reader live region */}
|
|
238
|
+
<div
|
|
239
|
+
role="status"
|
|
240
|
+
aria-live="polite"
|
|
241
|
+
aria-atomic="true"
|
|
242
|
+
style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0,0,0,0)", whiteSpace: "nowrap" }}
|
|
243
|
+
>
|
|
244
|
+
{statusAnnouncement}
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{/* Inner Ephia button — same markup as the original TranscriptionButton */}
|
|
248
|
+
<div
|
|
249
|
+
className={`ephia-transcribe-root ephia-transcribe-root--${theme} ephia-transcribe-size--${size} ephia-transcribe-radius--${borderRadius}${className ? ` ${className}` : ""}`}
|
|
250
|
+
style={
|
|
251
|
+
{
|
|
252
|
+
["--ephia-transcribe-radius" as string]: radiusCss,
|
|
253
|
+
...(hasCustomColors
|
|
254
|
+
? {
|
|
255
|
+
backgroundColor: effectiveSecondary ?? undefined,
|
|
256
|
+
["--ephia-transcribe-ink" as string]: effectivePrimary,
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
} as React.CSSProperties
|
|
260
|
+
}
|
|
261
|
+
>
|
|
262
|
+
{/* Lien externe ephia.app */}
|
|
263
|
+
<div className={`ephia-transcribe-poweredby ephia-transcribe-poweredby--${theme}`}>
|
|
264
|
+
<a
|
|
265
|
+
href={EPHIA_APP_URL}
|
|
266
|
+
target="_blank"
|
|
267
|
+
rel="noopener noreferrer"
|
|
268
|
+
className="ephia-transcribe-poweredby-btn"
|
|
269
|
+
aria-label="En savoir plus sur Ephia — ouvre ephia.app dans un nouvel onglet"
|
|
270
|
+
onClick={(e) => e.stopPropagation()}
|
|
271
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
272
|
+
>
|
|
273
|
+
<EphiaExternalLinkIcon />
|
|
274
|
+
</a>
|
|
275
|
+
<div className="ephia-transcribe-poweredby-tooltip">ephia.app</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Recording glow ring */}
|
|
279
|
+
{isLiveSession && !reducedMotion && (
|
|
280
|
+
<div
|
|
281
|
+
className="ephia-transcribe-ring"
|
|
282
|
+
style={
|
|
283
|
+
hasCustomColors
|
|
284
|
+
? {
|
|
285
|
+
boxShadow: `0 0 20px color-mix(in srgb, ${effectivePrimary} 55%, transparent)`,
|
|
286
|
+
}
|
|
287
|
+
: {
|
|
288
|
+
boxShadow:
|
|
289
|
+
"0 0 24px rgba(114, 58, 222, 0.55), 0 0 14px rgba(0, 235, 208, 0.45)",
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
{...(interactionMode === "push-to-talk"
|
|
298
|
+
? {
|
|
299
|
+
onPointerDown: (e: React.PointerEvent) => {
|
|
300
|
+
e.preventDefault();
|
|
301
|
+
if (typeof window !== "undefined") {
|
|
302
|
+
(window as any).__ephia_record_click_ts = Date.now();
|
|
303
|
+
window.dispatchEvent(
|
|
304
|
+
new CustomEvent("ephia:sdk-debug", {
|
|
305
|
+
detail: {
|
|
306
|
+
type: "sdk.record.click",
|
|
307
|
+
sessionId: null,
|
|
308
|
+
payload: { message: "Push-to-talk press", ts: Date.now() },
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (!isLiveSession && !isArming) void start(activeTargetId ?? undefined, { startupBufferMs: 5000 });
|
|
314
|
+
},
|
|
315
|
+
onPointerUp: () => { if (isLiveSession) void stop(); },
|
|
316
|
+
onPointerLeave: () => { if (isLiveSession) void stop(); },
|
|
317
|
+
}
|
|
318
|
+
: {
|
|
319
|
+
onClick: () => {
|
|
320
|
+
if (isArming) return;
|
|
321
|
+
if (typeof window !== "undefined") {
|
|
322
|
+
(window as any).__ephia_record_click_ts = Date.now();
|
|
323
|
+
window.dispatchEvent(
|
|
324
|
+
new CustomEvent("ephia:sdk-debug", {
|
|
325
|
+
detail: {
|
|
326
|
+
type: "sdk.record.click",
|
|
327
|
+
sessionId: null,
|
|
328
|
+
payload: { message: "Clic sur Record", ts: Date.now() },
|
|
329
|
+
},
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (isLiveSession) { void stop(); } else { void start(activeTargetId ?? undefined, { startupBufferMs: 5000 }); }
|
|
334
|
+
},
|
|
335
|
+
})}
|
|
336
|
+
disabled={isArming}
|
|
337
|
+
className="ephia-transcribe-btn"
|
|
338
|
+
aria-pressed={isLiveSession}
|
|
339
|
+
aria-busy={isArming}
|
|
340
|
+
aria-label={
|
|
341
|
+
isLiveSession ? "Arrêter la dictée" :
|
|
342
|
+
isArming ? "Traitement en cours…" :
|
|
343
|
+
"Démarrer la dictée"
|
|
344
|
+
}
|
|
345
|
+
>
|
|
346
|
+
<span
|
|
347
|
+
className={`ephia-transcribe-btn-inner${isStandardVariant ? " ephia-transcribe-btn-inner--standard" : ""}${showProcessing ? " ephia-transcribe-btn-inner--processing" : ""}`}
|
|
348
|
+
>
|
|
349
|
+
{variant === "minimal" ? (
|
|
350
|
+
<span
|
|
351
|
+
className={`ephia-transcribe-minimal-swap${isRecording || isArming ? " ephia-transcribe-minimal-swap--recording" : ""}`}
|
|
352
|
+
style={{ width: logoWidthPx, height: logoHeightPx }}
|
|
353
|
+
>
|
|
354
|
+
<span className="ephia-transcribe-minimal-swap-layer ephia-transcribe-minimal-swap-layer--logo" aria-hidden>
|
|
355
|
+
<span className="ephia-transcribe-logo-clip ephia-transcribe-logo-slot" style={{ width: logoWidthPx, height: logoHeightPx }}>
|
|
356
|
+
<span className="ephia-transcribe-logo-slot-layer ephia-transcribe-logo-slot-layer--logo" aria-hidden>
|
|
357
|
+
<EphiaLogo
|
|
358
|
+
height={logoHeightPx}
|
|
359
|
+
width={logoWidthPx}
|
|
360
|
+
variant={logoVariant}
|
|
361
|
+
fill={effectivePrimary}
|
|
362
|
+
isRecording={isLiveSession}
|
|
363
|
+
/>
|
|
364
|
+
</span>
|
|
365
|
+
</span>
|
|
366
|
+
</span>
|
|
367
|
+
<span className="ephia-transcribe-minimal-swap-layer ephia-transcribe-minimal-swap-layer--activity" aria-hidden>
|
|
368
|
+
<MinimalProcessingBars
|
|
369
|
+
active={isRecording || isArming}
|
|
370
|
+
size={safeSize}
|
|
371
|
+
color={waveformColor}
|
|
372
|
+
/>
|
|
373
|
+
</span>
|
|
374
|
+
</span>
|
|
375
|
+
) : (
|
|
376
|
+
// standard variant
|
|
377
|
+
<span
|
|
378
|
+
className={`ephia-transcribe-logo-clip ephia-transcribe-logo-slot${showTimer ? " ephia-transcribe-logo-slot--recording" : ""}`}
|
|
379
|
+
style={{ width: logoSlotWidthPx, height: logoHeightPx }}
|
|
380
|
+
>
|
|
381
|
+
<span className="ephia-transcribe-logo-slot-layer ephia-transcribe-logo-slot-layer--logo" aria-hidden>
|
|
382
|
+
<EphiaLogo
|
|
383
|
+
height={logoHeightPx}
|
|
384
|
+
width={logoWidthPx}
|
|
385
|
+
variant={logoVariant}
|
|
386
|
+
fill={effectivePrimary}
|
|
387
|
+
isRecording={isLiveSession}
|
|
388
|
+
/>
|
|
389
|
+
</span>
|
|
390
|
+
{showTimer && (
|
|
391
|
+
<>
|
|
392
|
+
<span className="ephia-transcribe-logo-slot-layer ephia-transcribe-logo-slot-layer--timer" aria-hidden>
|
|
393
|
+
<span className="ephia-transcribe-session-timer">{timerLabel}</span>
|
|
394
|
+
</span>
|
|
395
|
+
<span className="ephia-transcribe-session-timer ephia-transcribe-session-timer--measure" ref={timerMeasureRef}>
|
|
396
|
+
{timerLabel}
|
|
397
|
+
</span>
|
|
398
|
+
</>
|
|
399
|
+
)}
|
|
400
|
+
</span>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{isStandardVariant && (
|
|
404
|
+
<span
|
|
405
|
+
className={`ephia-transcribe-standard-right-swap${showStandardPrompt ? " ephia-transcribe-standard-right-swap--prompt" : showProcessing ? " ephia-transcribe-standard-right-swap--processing" : " ephia-transcribe-standard-right-swap--waveform"}`}
|
|
406
|
+
aria-hidden
|
|
407
|
+
>
|
|
408
|
+
<span className="ephia-transcribe-standard-right-layer ephia-transcribe-standard-right-layer--prompt">
|
|
409
|
+
Commencer à dicter
|
|
410
|
+
</span>
|
|
411
|
+
<span className="ephia-transcribe-standard-right-layer ephia-transcribe-standard-right-layer--processing">
|
|
412
|
+
Traitement…
|
|
413
|
+
</span>
|
|
414
|
+
{!showProcessing ? (
|
|
415
|
+
<span className="ephia-transcribe-standard-right-layer ephia-transcribe-standard-right-layer--waveform">
|
|
416
|
+
<StandardIntensityVisualizer
|
|
417
|
+
enabled={isRecording}
|
|
418
|
+
level={voiceLevel}
|
|
419
|
+
/>
|
|
420
|
+
</span>
|
|
421
|
+
) : null}
|
|
422
|
+
</span>
|
|
423
|
+
)}
|
|
424
|
+
</span>
|
|
425
|
+
</button>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* ── Public component ────────────────────────────────────────────── */
|
|
432
|
+
|
|
433
|
+
export function EphiaFloatingButton({
|
|
434
|
+
apiUrl,
|
|
435
|
+
apiKey,
|
|
436
|
+
bearerToken,
|
|
437
|
+
clientType,
|
|
438
|
+
transport,
|
|
439
|
+
position = "bottom-center",
|
|
440
|
+
theme = "light",
|
|
441
|
+
size = "M",
|
|
442
|
+
variant = "standard",
|
|
443
|
+
borderRadius = "lg",
|
|
444
|
+
interactionMode = "toggle",
|
|
445
|
+
colors,
|
|
446
|
+
className,
|
|
447
|
+
style,
|
|
448
|
+
sessionOptions,
|
|
449
|
+
onError,
|
|
450
|
+
onStatusChange,
|
|
451
|
+
}: EphiaFloatingButtonProps) {
|
|
452
|
+
const ctx = useOptionalEphiaContext();
|
|
453
|
+
const safeRadius: EphiaFloatingButtonBorderRadius =
|
|
454
|
+
borderRadius && borderRadius in EPHIA_FLOATING_BUTTON_RADIUS_CSS ? borderRadius : "lg";
|
|
455
|
+
|
|
456
|
+
const innerProps = useMemo<InnerProps>(() => ({
|
|
457
|
+
position,
|
|
458
|
+
theme,
|
|
459
|
+
size,
|
|
460
|
+
variant,
|
|
461
|
+
borderRadius: safeRadius,
|
|
462
|
+
interactionMode,
|
|
463
|
+
colors,
|
|
464
|
+
className,
|
|
465
|
+
style,
|
|
466
|
+
onError,
|
|
467
|
+
onStatusChange,
|
|
468
|
+
}), [position, theme, size, variant, safeRadius, interactionMode, colors, className, style, onError, onStatusChange]);
|
|
469
|
+
|
|
470
|
+
if (ctx) {
|
|
471
|
+
return <EphiaFloatingButtonInner {...innerProps} />;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<EphiaProvider
|
|
476
|
+
apiUrl={apiUrl}
|
|
477
|
+
apiKey={apiKey}
|
|
478
|
+
bearerToken={bearerToken}
|
|
479
|
+
clientType={clientType}
|
|
480
|
+
transport={transport}
|
|
481
|
+
options={{
|
|
482
|
+
language: sessionOptions?.language as string | undefined,
|
|
483
|
+
sessionOptions: sessionOptions,
|
|
484
|
+
}}
|
|
485
|
+
>
|
|
486
|
+
<EphiaFloatingButtonInner {...innerProps} />
|
|
487
|
+
</EphiaProvider>
|
|
488
|
+
);
|
|
489
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
type MinimalBarsSize = "S" | "M" | "L";
|
|
6
|
+
|
|
7
|
+
const BAR_LAYOUT: Record<MinimalBarsSize, { barWidthPx: number; gapPx: number }> = {
|
|
8
|
+
S: { barWidthPx: 3, gapPx: 2.5 },
|
|
9
|
+
M: { barWidthPx: 3.5, gapPx: 3 },
|
|
10
|
+
L: { barWidthPx: 4, gapPx: 3.5 },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const TAU = Math.PI * 2;
|
|
14
|
+
|
|
15
|
+
/** Chaque barre : fréquences différentes + petite amplitude → pas de cycle min/max synchronisé. */
|
|
16
|
+
const BAR_MOTION = [
|
|
17
|
+
{ base: 0.62, a1: 0.14, a2: 0.09, f1: 1.25, f2: 2.05, p1: 0, p2: 0.7 },
|
|
18
|
+
{ base: 0.64, a1: 0.15, a2: 0.07, f1: 1.65, f2: 2.35, p1: 1.4, p2: 2.2 },
|
|
19
|
+
{ base: 0.6, a1: 0.12, a2: 0.1, f1: 1.05, f2: 1.85, p1: 2.5, p2: 0.4 },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
const LERP = 0.14;
|
|
23
|
+
|
|
24
|
+
function clamp01(x: number): number {
|
|
25
|
+
return Math.max(0.36, Math.min(0.9, x));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function targetHeight(phaseSec: number, spec: (typeof BAR_MOTION)[number]): number {
|
|
29
|
+
const t = phaseSec;
|
|
30
|
+
const v =
|
|
31
|
+
spec.base +
|
|
32
|
+
spec.a1 * Math.sin(t * spec.f1 * TAU + spec.p1) +
|
|
33
|
+
spec.a2 * Math.sin(t * spec.f2 * TAU + spec.p2);
|
|
34
|
+
return clamp01(v);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Indicateur minimal : 3 barres, même couleur, respiration douce (pas de balayage min→max).
|
|
39
|
+
*/
|
|
40
|
+
export function MinimalProcessingBars({
|
|
41
|
+
active,
|
|
42
|
+
size = "M",
|
|
43
|
+
color,
|
|
44
|
+
}: {
|
|
45
|
+
active: boolean;
|
|
46
|
+
size?: MinimalBarsSize;
|
|
47
|
+
color: string;
|
|
48
|
+
}) {
|
|
49
|
+
const rootRef = useRef<HTMLSpanElement>(null);
|
|
50
|
+
const heightsRef = useRef([0.55, 0.58, 0.54]);
|
|
51
|
+
const layout = BAR_LAYOUT[size] ?? BAR_LAYOUT.M;
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const root = rootRef.current;
|
|
55
|
+
if (!root) return;
|
|
56
|
+
|
|
57
|
+
const applyHeights = (h: number[]) => {
|
|
58
|
+
root.style.setProperty("--ephia-bar-0", String(h[0]));
|
|
59
|
+
root.style.setProperty("--ephia-bar-1", String(h[1]));
|
|
60
|
+
root.style.setProperty("--ephia-bar-2", String(h[2]));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (!active) {
|
|
64
|
+
heightsRef.current = [0.52, 0.56, 0.54];
|
|
65
|
+
applyHeights(heightsRef.current);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const reducedMotion =
|
|
70
|
+
typeof window !== "undefined" &&
|
|
71
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
72
|
+
|
|
73
|
+
if (reducedMotion) {
|
|
74
|
+
applyHeights([0.55, 0.58, 0.54]);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let raf = 0;
|
|
79
|
+
const start = performance.now();
|
|
80
|
+
|
|
81
|
+
const tick = (now: number) => {
|
|
82
|
+
const phase = (now - start) / 1000;
|
|
83
|
+
const current = heightsRef.current;
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < 3; i++) {
|
|
86
|
+
const target = targetHeight(phase, BAR_MOTION[i]);
|
|
87
|
+
current[i] += (target - current[i]) * LERP;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
applyHeights(current);
|
|
91
|
+
raf = requestAnimationFrame(tick);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
raf = requestAnimationFrame(tick);
|
|
95
|
+
return () => cancelAnimationFrame(raf);
|
|
96
|
+
}, [active]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<span
|
|
100
|
+
ref={rootRef}
|
|
101
|
+
className="ephia-transcribe-minimal-bars"
|
|
102
|
+
style={
|
|
103
|
+
{
|
|
104
|
+
["--ephia-minimal-bar-gap" as string]: `${layout.gapPx}px`,
|
|
105
|
+
} as React.CSSProperties
|
|
106
|
+
}
|
|
107
|
+
aria-hidden
|
|
108
|
+
>
|
|
109
|
+
{BAR_MOTION.map((_, i) => (
|
|
110
|
+
<span
|
|
111
|
+
key={i}
|
|
112
|
+
className="ephia-transcribe-minimal-bar"
|
|
113
|
+
style={{
|
|
114
|
+
width: layout.barWidthPx,
|
|
115
|
+
backgroundColor: color,
|
|
116
|
+
["--ephia-bar-scale" as string]: `var(--ephia-bar-${i}, 0.5)`,
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
))}
|
|
120
|
+
</span>
|
|
121
|
+
);
|
|
122
|
+
}
|