@elabs-ai/components-ai 4.0.0 → 4.2.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 +62 -12
- package/dist/{_audio-player-media-chrome-KA5DY54G.js → _audio-player-media-chrome-T3XVXWRZ.js} +8 -4
- package/dist/_audio-player-media-chrome-T3XVXWRZ.js.map +1 -0
- package/dist/{_flow-boundary-D63PJ65S.js → _flow-boundary-SHNWLQG5.js} +32 -43
- package/dist/_flow-boundary-SHNWLQG5.js.map +1 -0
- package/dist/{_persona-rive-RFR2EUWP.js → _persona-rive-JEG44YHX.js} +9 -5
- package/dist/_persona-rive-JEG44YHX.js.map +1 -0
- package/dist/index.d.ts +1152 -218
- package/dist/index.js +5601 -3241
- package/dist/index.js.map +1 -1
- package/package.json +30 -15
- package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
- package/src/__contract__/chat-shell.contract.test.tsx +49 -0
- package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
- package/src/__contract__/image.contract.test.tsx +49 -0
- package/src/__contract__/markdown-view.contract.test.tsx +49 -0
- package/src/__contract__/message-feedback.contract.test.tsx +49 -0
- package/src/__contract__/message-form.contract.test.tsx +49 -0
- package/src/__contract__/message-table.contract.test.tsx +49 -0
- package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
- package/src/__contract__/persona.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
- package/src/_audio-player-media-chrome.tsx +102 -15
- package/src/_chat-shell-rail.tsx +2 -2
- package/src/_flow-boundary.tsx +68 -49
- package/src/_lazy-boundary-conformance.ts +38 -0
- package/src/_lazy-cjk.test.ts +43 -0
- package/src/_lazy-cjk.ts +73 -0
- package/src/_lazy-engine-boundary.tsx +61 -0
- package/src/_lazy-math.test.ts +63 -0
- package/src/_lazy-math.ts +90 -0
- package/src/_lazy-mermaid-absent.test.ts +53 -0
- package/src/_lazy-mermaid.test.ts +15 -0
- package/src/_lazy-mermaid.ts +24 -1
- package/src/_mermaid-error-panel.test.tsx +50 -0
- package/src/_mermaid-error-panel.tsx +66 -0
- package/src/_persona-rive.tsx +62 -10
- package/src/_streamdown-i18n.ts +94 -22
- package/src/_streamdown-safety.ts +170 -0
- package/src/_theme-scope-store.test.ts +83 -0
- package/src/_theme-scope-store.ts +103 -0
- package/src/agent-event.stories.tsx +97 -0
- package/src/agent-event.test.tsx +145 -0
- package/src/agent-event.tsx +187 -0
- package/src/agent-timeline.stories.tsx +9 -1
- package/src/agent.test.tsx +47 -0
- package/src/agent.tsx +43 -24
- package/src/agentic-workspace.stories.tsx +27 -20
- package/src/artifact.tsx +17 -14
- package/src/asset-preview.test.tsx +40 -0
- package/src/asset-preview.tsx +83 -12
- package/src/attachments.tsx +7 -4
- package/src/audio-player.test.tsx +78 -0
- package/src/audio-player.tsx +243 -56
- package/src/audio-visualizer.stories.tsx +126 -0
- package/src/audio-visualizer.test.tsx +438 -0
- package/src/audio-visualizer.tsx +367 -0
- package/src/canvas.stories.tsx +150 -1
- package/src/chat-shell.stories.tsx +18 -3
- package/src/chat.stories.tsx +16 -2
- package/src/code-block.stories.tsx +9 -1
- package/src/code-block.test.tsx +100 -1
- package/src/code-block.tsx +166 -103
- package/src/commit.tsx +30 -41
- package/src/composer.stories.tsx +372 -9
- package/src/composer.test.tsx +357 -6
- package/src/composer.tsx +217 -35
- package/src/confirmation.stories.tsx +72 -1
- package/src/confirmation.test.tsx +216 -2
- package/src/confirmation.tsx +264 -4
- package/src/context-panel.stories.tsx +9 -1
- package/src/context-panel.tsx +3 -2
- package/src/conversation.stories.tsx +86 -2
- package/src/conversation.test.tsx +66 -0
- package/src/conversation.tsx +52 -9
- package/src/diff-view.stories.tsx +196 -0
- package/src/diff-view.test.tsx +238 -0
- package/src/diff-view.tsx +697 -0
- package/src/environment-variables.tsx +20 -37
- package/src/file-tree.test.tsx +21 -0
- package/src/file-tree.tsx +12 -2
- package/src/gallery.tsx +4 -2
- package/src/index.ts +14 -4
- package/src/inline-citation.tsx +5 -5
- package/src/jsx-preview.stories.tsx +2 -2
- package/src/jsx-preview.tsx +151 -43
- package/src/markdown-view.stories.tsx +92 -1
- package/src/markdown-view.test.tsx +237 -2
- package/src/markdown-view.tsx +157 -6
- package/src/message-compare.stories.tsx +175 -0
- package/src/message-compare.test.tsx +207 -0
- package/src/message-compare.tsx +453 -0
- package/src/message-form.stories.tsx +65 -4
- package/src/message-form.test.tsx +8 -2
- package/src/message-form.tsx +15 -7
- package/src/message-table.stories.tsx +2 -2
- package/src/message-table.test.tsx +7 -0
- package/src/message-table.tsx +8 -4
- package/src/message.stories.tsx +9 -1
- package/src/message.test.tsx +247 -1
- package/src/message.tsx +125 -15
- package/src/microcopy.test.tsx +40 -0
- package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
- package/src/model-provider-logo.test.tsx +109 -0
- package/src/model-provider-logo.tsx +183 -0
- package/src/open-in-chat.tsx +50 -29
- package/src/package-info.tsx +12 -12
- package/src/permission-mode-select.stories.tsx +82 -0
- package/src/permission-mode-select.test.tsx +100 -0
- package/src/permission-mode-select.tsx +137 -0
- package/src/persona-missing-peer.test.tsx +54 -0
- package/src/persona.tsx +68 -22
- package/src/plan.stories.tsx +166 -0
- package/src/plan.test.tsx +267 -0
- package/src/plan.tsx +182 -20
- package/src/prompt-input-effort.stories.tsx +123 -0
- package/src/prompt-input-effort.test.tsx +83 -0
- package/src/prompt-input-effort.tsx +136 -0
- package/src/prompt-input-mode.stories.tsx +108 -0
- package/src/prompt-input-mode.test.tsx +99 -0
- package/src/prompt-input-mode.tsx +169 -0
- package/src/prompt-input-slash.stories.tsx +211 -0
- package/src/prompt-input-slash.test.tsx +262 -0
- package/src/prompt-input-slash.tsx +541 -0
- package/src/prompt-input.stories.tsx +2 -2
- package/src/prompt-input.test.tsx +67 -1
- package/src/prompt-input.tsx +42 -4
- package/src/queue.tsx +4 -4
- package/src/reasoning.tsx +42 -17
- package/src/sandbox.stories.tsx +9 -1
- package/src/sandbox.tsx +3 -3
- package/src/schema-display.test.tsx +56 -0
- package/src/schema-display.tsx +71 -37
- package/src/selection-toolbar.stories.tsx +9 -1
- package/src/session-header.stories.tsx +128 -0
- package/src/session-header.test.tsx +138 -0
- package/src/session-header.tsx +243 -0
- package/src/session-status-bar.stories.tsx +73 -0
- package/src/session-status-bar.test.tsx +94 -0
- package/src/session-status-bar.tsx +165 -0
- package/src/snippet.stories.tsx +9 -1
- package/src/snippet.test.tsx +6 -2
- package/src/snippet.tsx +14 -34
- package/src/speech-input.test.tsx +109 -0
- package/src/speech-input.tsx +31 -3
- package/src/stack-trace.tsx +24 -36
- package/src/streamdown-i18n.test.tsx +24 -1
- package/src/task.stories.tsx +13 -3
- package/src/templates-ai-assistant.stories.tsx +21 -1
- package/src/test-results.tsx +47 -27
- package/src/token-usage.stories.tsx +24 -0
- package/src/token-usage.test.tsx +92 -0
- package/src/{context.tsx → token-usage.tsx} +73 -59
- package/src/tool-result-card.stories.tsx +9 -1
- package/src/tool.stories.tsx +13 -3
- package/src/tool.test.tsx +65 -0
- package/src/tool.tsx +73 -30
- package/src/transcription.tsx +1 -1
- package/src/turn-status.stories.tsx +124 -0
- package/src/turn-status.test.tsx +74 -0
- package/src/turn-status.tsx +174 -0
- package/src/use-audio-level.ts +104 -0
- package/src/voice-selector.tsx +5 -5
- package/src/web-preview.test.tsx +51 -1
- package/src/web-preview.tsx +46 -10
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +0 -1
- package/dist/_flow-boundary-D63PJ65S.js.map +0 -1
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +0 -11
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +0 -1
- package/dist/_persona-rive-RFR2EUWP.js.map +0 -1
- package/src/_interactive-terminal-xterm.ts +0 -32
- package/src/blocks-ai-composer.stories.tsx +0 -83
- package/src/context.stories.tsx +0 -16
- package/src/interactive-terminal.stories.tsx +0 -165
- package/src/interactive-terminal.test.tsx +0 -448
- package/src/interactive-terminal.tsx +0 -444
- package/src/model-selector.test.tsx +0 -55
- package/src/model-selector.tsx +0 -225
- package/src/terminal.tsx +0 -244
package/src/asset-preview.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
TableHead,
|
|
31
31
|
TableHeader,
|
|
32
32
|
TableRow,
|
|
33
|
+
useLocale,
|
|
33
34
|
} from "@elabs-ai/components-ui";
|
|
34
35
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
35
36
|
import type { HTMLAttributes, ReactNode } from "react";
|
|
@@ -72,18 +73,87 @@ interface ParsedCsv {
|
|
|
72
73
|
rows: string[][];
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* A small RFC 4180-ish CSV tokenizer: handles quoted fields (commas and
|
|
78
|
+
* newlines inside quotes don't end the field/row) and the `""` escaped-quote
|
|
79
|
+
* convention. Single pass over the raw string — no line pre-split, so a
|
|
80
|
+
* quoted field spanning multiple lines survives intact. Still preview-grade
|
|
81
|
+
* (unquoted cells are trimmed for the glance table), not a full CSV grammar.
|
|
82
|
+
*/
|
|
83
|
+
function parseCsvRows(content: string): string[][] {
|
|
84
|
+
const rows: string[][] = [];
|
|
85
|
+
let row: string[] = [];
|
|
86
|
+
let field = "";
|
|
87
|
+
let wasQuoted = false;
|
|
88
|
+
let inQuotes = false;
|
|
89
|
+
|
|
90
|
+
const endField = () => {
|
|
91
|
+
row.push(wasQuoted ? field : field.trim());
|
|
92
|
+
field = "";
|
|
93
|
+
wasQuoted = false;
|
|
94
|
+
};
|
|
95
|
+
const endRow = () => {
|
|
96
|
+
endField();
|
|
97
|
+
rows.push(row);
|
|
98
|
+
row = [];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < content.length; i++) {
|
|
102
|
+
const ch = content[i];
|
|
103
|
+
|
|
104
|
+
if (inQuotes) {
|
|
105
|
+
if (ch === '"') {
|
|
106
|
+
if (content[i + 1] === '"') {
|
|
107
|
+
field += '"';
|
|
108
|
+
i += 1;
|
|
109
|
+
} else {
|
|
110
|
+
inQuotes = false;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
field += ch;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ch === '"' && field === "") {
|
|
119
|
+
inQuotes = true;
|
|
120
|
+
wasQuoted = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (ch === ",") {
|
|
125
|
+
endField();
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (ch === "\r") {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ch === "\n") {
|
|
134
|
+
endRow();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
field += ch;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A trailing field/row with no terminating newline.
|
|
142
|
+
if (field !== "" || row.length > 0) {
|
|
143
|
+
endRow();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return rows;
|
|
147
|
+
}
|
|
148
|
+
|
|
76
149
|
function parseCsv(content: string): ParsedCsv {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.filter((line) => line.length > 0);
|
|
81
|
-
const [head = "", ...rest] = lines;
|
|
82
|
-
const split = (line: string) => line.split(",").map((cell) => cell.trim());
|
|
83
|
-
return { header: split(head), rows: rest.map(split) };
|
|
150
|
+
const rows = parseCsvRows(content.trim()).filter((r) => !(r.length === 1 && r[0] === ""));
|
|
151
|
+
const [header = [], ...rest] = rows;
|
|
152
|
+
return { header, rows: rest };
|
|
84
153
|
}
|
|
85
154
|
|
|
86
155
|
const CsvPreview = ({ content }: { content: string }) => {
|
|
156
|
+
const { t } = useLocale();
|
|
87
157
|
const { header, rows } = useMemo(() => parseCsv(content), [content]);
|
|
88
158
|
return (
|
|
89
159
|
<div className="flex flex-col gap-2">
|
|
@@ -109,7 +179,7 @@ const CsvPreview = ({ content }: { content: string }) => {
|
|
|
109
179
|
</TableBody>
|
|
110
180
|
</Table>
|
|
111
181
|
<p className="text-meta text-muted-foreground">
|
|
112
|
-
{
|
|
182
|
+
{t("ai.assetPreview.rowCount", { count: rows.length })}
|
|
113
183
|
</p>
|
|
114
184
|
</div>
|
|
115
185
|
);
|
|
@@ -135,6 +205,7 @@ export const AssetPreview = ({
|
|
|
135
205
|
className,
|
|
136
206
|
...props
|
|
137
207
|
}: AssetPreviewProps) => {
|
|
208
|
+
const { t } = useLocale();
|
|
138
209
|
const [mode, setMode] = useState<AssetPreviewMode>(defaultMode);
|
|
139
210
|
const injectedRenderer = useAssetPreviewRenderer();
|
|
140
211
|
const content = asset.content ?? "";
|
|
@@ -150,7 +221,7 @@ export const AssetPreview = ({
|
|
|
150
221
|
if (injected !== null) {
|
|
151
222
|
body = injected;
|
|
152
223
|
} else if (asset.type !== "image" && content.length === 0) {
|
|
153
|
-
body = <p className="text-body text-muted-foreground">
|
|
224
|
+
body = <p className="text-body text-muted-foreground">{t("ai.assetPreview.noPreview")}</p>;
|
|
154
225
|
} else if (showRaw) {
|
|
155
226
|
body = <CodeBlock code={content} language={assetLanguage(asset)} wrap />;
|
|
156
227
|
} else {
|
|
@@ -197,7 +268,7 @@ export const AssetPreview = ({
|
|
|
197
268
|
className={cn(!showRaw && "bg-accent text-accent-foreground")}
|
|
198
269
|
onClick={() => setMode("preview")}
|
|
199
270
|
>
|
|
200
|
-
|
|
271
|
+
{t("ai.assetPreview.preview")}
|
|
201
272
|
</Button>
|
|
202
273
|
<Button
|
|
203
274
|
type="button"
|
|
@@ -207,7 +278,7 @@ export const AssetPreview = ({
|
|
|
207
278
|
className={cn(showRaw && "bg-accent text-accent-foreground")}
|
|
208
279
|
onClick={() => setMode("raw")}
|
|
209
280
|
>
|
|
210
|
-
|
|
281
|
+
{t("ai.assetPreview.raw")}
|
|
211
282
|
</Button>
|
|
212
283
|
</ArtifactActions>
|
|
213
284
|
) : null}
|
package/src/attachments.tsx
CHANGED
|
@@ -192,8 +192,8 @@ export const Attachment = ({ data, onRemove, className, children, ...props }: At
|
|
|
192
192
|
variant === "inline" && [
|
|
193
193
|
"flex h-8 cursor-pointer select-none items-center gap-1.5",
|
|
194
194
|
"rounded-md border border-border px-1.5",
|
|
195
|
-
"font-medium text-
|
|
196
|
-
"hover:bg-accent hover:text-accent-foreground
|
|
195
|
+
"font-medium text-body transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
196
|
+
"hover:bg-accent/50 hover:text-accent-foreground",
|
|
197
197
|
],
|
|
198
198
|
variant === "list" && [
|
|
199
199
|
"flex w-full items-center gap-3 rounded-lg border p-3",
|
|
@@ -283,7 +283,7 @@ export const AttachmentInfo = ({
|
|
|
283
283
|
<div className={cn("min-w-0 flex-1", className)} {...props}>
|
|
284
284
|
<span className="block truncate">{label}</span>
|
|
285
285
|
{showMediaType && data.mediaType && (
|
|
286
|
-
<span className="block truncate text-muted-foreground text-
|
|
286
|
+
<span className="block truncate text-muted-foreground text-meta">{data.mediaType}</span>
|
|
287
287
|
)}
|
|
288
288
|
</div>
|
|
289
289
|
);
|
|
@@ -385,7 +385,10 @@ export type AttachmentEmptyProps = HTMLAttributes<HTMLDivElement>;
|
|
|
385
385
|
|
|
386
386
|
export const AttachmentEmpty = ({ className, children, ...props }: AttachmentEmptyProps) => (
|
|
387
387
|
<div
|
|
388
|
-
className={cn(
|
|
388
|
+
className={cn(
|
|
389
|
+
"flex items-center justify-center p-4 text-muted-foreground text-body",
|
|
390
|
+
className,
|
|
391
|
+
)}
|
|
389
392
|
{...props}
|
|
390
393
|
>
|
|
391
394
|
{children ?? "No attachments"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #33 — `media-chrome` is an OPTIONAL peer of `@elabs-ai/components-ai`,
|
|
3
|
+
* reached only through `lazy(() => import("./_audio-player-media-chrome"))`
|
|
4
|
+
* (ADR 0019, `lazyPart` in `audio-player.tsx`). A REJECTED `lazy()` import
|
|
5
|
+
* throws during render — `Suspense` alone does not catch that (it only covers
|
|
6
|
+
* the pending state) — so every exported part is wrapped in
|
|
7
|
+
* `LazyEngineBoundary` (`_lazy-engine-boundary.tsx`). This locks that the
|
|
8
|
+
* boundary falls back gracefully for the top-level `AudioPlayer` AND for a
|
|
9
|
+
* sub-control, instead of taking the tree down.
|
|
10
|
+
*
|
|
11
|
+
* `vi.mock` factories are hoisted, so the mock itself must resolve
|
|
12
|
+
* successfully (a factory that throws or rejects gets intercepted by
|
|
13
|
+
* Vitest's OWN "error when mocking a module" diagnostic instead of the
|
|
14
|
+
* message this test needs — see `interactive-terminal-missing-peer.test.tsx`
|
|
15
|
+
* for the same finding). So the module resolves fine and the simulated
|
|
16
|
+
* failure instead comes from each named export THROWING when React renders
|
|
17
|
+
* it — a real render-phase error, which is exactly what `LazyEngineBoundary`
|
|
18
|
+
* exists to catch.
|
|
19
|
+
*
|
|
20
|
+
* The top-level `AudioPlayer` case asserts a SETTLED, non-loading notice
|
|
21
|
+
* (never a permanent `Skeleton`) — a loading placeholder left in place after
|
|
22
|
+
* the load has definitively failed reads as "still loading, forever", which
|
|
23
|
+
* `.claude/rules/loading-states.md` treats as a defect, not a graceful
|
|
24
|
+
* fallback. See `AudioPlayerMissing` in `audio-player.tsx`.
|
|
25
|
+
*/
|
|
26
|
+
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
27
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
28
|
+
|
|
29
|
+
vi.mock("./_audio-player-media-chrome", () => ({
|
|
30
|
+
AudioPlayer: () => {
|
|
31
|
+
throw new Error("Cannot find module 'media-chrome'");
|
|
32
|
+
},
|
|
33
|
+
AudioPlayerPlayButton: () => {
|
|
34
|
+
throw new Error("Cannot find module 'media-chrome'");
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
import { AudioPlayer, AudioPlayerPlayButton } from "./audio-player";
|
|
39
|
+
|
|
40
|
+
afterEach(cleanup);
|
|
41
|
+
|
|
42
|
+
const flush = () => act(async () => void (await new Promise((resolve) => setTimeout(resolve, 0))));
|
|
43
|
+
|
|
44
|
+
describe("AudioPlayer — missing optional peer (#33)", () => {
|
|
45
|
+
it("falls back to a settled, actionable notice — never a permanent skeleton — when media-chrome fails to load", async () => {
|
|
46
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
47
|
+
|
|
48
|
+
render(<AudioPlayer />);
|
|
49
|
+
await flush();
|
|
50
|
+
|
|
51
|
+
// The tree survived, and it settled: no permanent loading placeholder
|
|
52
|
+
// (a `Skeleton` left in place after a definitive failure would read as
|
|
53
|
+
// "still loading, forever" — the bug this locks against), and the
|
|
54
|
+
// notice names the exact missing package via `role="status"` (a missing
|
|
55
|
+
// optional peer is a capability gap, not a broken component).
|
|
56
|
+
const notice = screen.getByRole("status");
|
|
57
|
+
expect(notice).toHaveTextContent(/media-chrome/i);
|
|
58
|
+
expect(document.querySelector('[class*="animate-pulse"]')).toBeNull();
|
|
59
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
60
|
+
|
|
61
|
+
errorSpy.mockRestore();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("degrades a sub-control to nothing rather than crashing", async () => {
|
|
65
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
66
|
+
|
|
67
|
+
const { container } = render(<AudioPlayerPlayButton />);
|
|
68
|
+
await flush();
|
|
69
|
+
|
|
70
|
+
// `renderMissing={() => null}` for sub-controls — an empty, but intact,
|
|
71
|
+
// tree beats a crash.
|
|
72
|
+
expect(container).toBeEmptyDOMElement();
|
|
73
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
74
|
+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
75
|
+
|
|
76
|
+
errorSpy.mockRestore();
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/audio-player.tsx
CHANGED
|
@@ -1,46 +1,191 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Skeleton } from "@elabs-ai/components-ui";
|
|
3
|
+
import { Skeleton, isOptionalPeerMissing, useLocale } from "@elabs-ai/components-ui";
|
|
4
4
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
5
5
|
import type { Experimental_SpeechResult as SpeechResult } from "ai";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
MediaController,
|
|
9
|
-
MediaDurationDisplay,
|
|
10
|
-
MediaMuteButton,
|
|
11
|
-
MediaPlayButton,
|
|
12
|
-
MediaSeekBackwardButton,
|
|
13
|
-
MediaSeekForwardButton,
|
|
14
|
-
MediaTimeDisplay,
|
|
15
|
-
MediaTimeRange,
|
|
16
|
-
MediaVolumeRange,
|
|
17
|
-
} from "media-chrome/react";
|
|
18
|
-
import type { ComponentProps, ComponentType } from "react";
|
|
6
|
+
import { VolumeOffIcon } from "lucide-react";
|
|
7
|
+
import type { ComponentProps, ComponentType, HTMLAttributes } from "react";
|
|
19
8
|
import { lazy, Suspense } from "react";
|
|
20
9
|
|
|
10
|
+
import { LazyEngineBoundary } from "./_lazy-engine-boundary";
|
|
11
|
+
|
|
21
12
|
/**
|
|
22
13
|
* media-chrome lives behind a dynamic import — it declares no `sideEffects`, so
|
|
23
14
|
* a static import would put the whole custom-element library in every consumer's
|
|
24
15
|
* entry chunk, `AudioPlayer` rendered or not. Every value import lives in
|
|
25
|
-
* `_audio-player-media-chrome.tsx
|
|
26
|
-
* erase. See ADR 0019 and `pnpm heavy-deps:check`.
|
|
16
|
+
* `_audio-player-media-chrome.tsx`. See ADR 0019 and `pnpm heavy-deps:check`.
|
|
27
17
|
*
|
|
28
18
|
* All ten parts resolve from the SAME module specifier, so they share one chunk:
|
|
29
19
|
* once `AudioPlayer` has loaded it, the controls inside it resolve from cache.
|
|
20
|
+
*
|
|
21
|
+
* Issue #101: `media-chrome` is an OPTIONAL peer, so no PUBLIC export's type
|
|
22
|
+
* may structurally reference `media-chrome/react`'s own types (previously
|
|
23
|
+
* `ComponentProps<typeof MediaController>` and nine siblings, imported as
|
|
24
|
+
* TYPE-only above) — doing so names `media-chrome/react`'s module specifier in
|
|
25
|
+
* this package's generated root `.d.ts` and hands a `skipLibCheck: false`
|
|
26
|
+
* consumer who has correctly omitted the peer a `TS2307` just for importing
|
|
27
|
+
* the barrel — not just for using `AudioPlayer`. `AudioPlayerPartProps` below
|
|
28
|
+
* is an OWNED base type: `media-chrome/react`'s wrapper
|
|
29
|
+
* (`media-chrome/react`'s `createComponent`, from the `ce-la-react` helper it
|
|
30
|
+
* is built on) types every part as `Omit<HTMLAttributes<I>, …> &
|
|
31
|
+
* Partial<the element's own instance members>` — i.e. ordinary HTML attributes
|
|
32
|
+
* plus a handful of optional custom attributes. `AudioPlayerPartProps` itself
|
|
33
|
+
* stays ordinary HTML attributes only; the PRIMITIVE-typed (`string` /
|
|
34
|
+
* `boolean` / `number`) extra instance members of `<media-controller>` and
|
|
35
|
+
* `<media-seek-forward-button>` — the two elements measured by the round-2
|
|
36
|
+
* validator — are mirrored one-by-one on `AudioPlayerProps` and
|
|
37
|
+
* `AudioPlayerSeekForwardButtonProps` below (each a single declaration line
|
|
38
|
+
* referencing nothing peer-owned, the same technique `seekOffset` already
|
|
39
|
+
* used). What stays narrowed to `AudioPlayerPartProps` alone are the
|
|
40
|
+
* PEER/COMPLEX-typed members (`MediaStore`, `AttributeTokenList`,
|
|
41
|
+
* `MediaTooltip`, `TooltipPlacement`, DOM-element and `HTMLMediaElement`
|
|
42
|
+
* callbacks, methods) and every member of the other eight parts — restoring
|
|
43
|
+
* those would re-import the peer type graph this fix exists to avoid. See the
|
|
44
|
+
* CHANGELOG's "Breaking (types)" entry for the exact narrowed set.
|
|
45
|
+
* `AudioPlayerPartProps` is therefore NOT a full replica of media-chrome's
|
|
46
|
+
* surface, only the base every part actually uses and re-exports (the same
|
|
47
|
+
* scoping `TerminalColorTheme` uses for xterm's `ITheme` in
|
|
48
|
+
* `packages/terminal/src/interactive-terminal.tsx`, also issue #101). The ten
|
|
49
|
+
* conformance assertions at the bottom of `_audio-player-media-chrome.tsx`
|
|
50
|
+
* (which still imports the real peer types — that module is reached only
|
|
51
|
+
* through `lazy()` and never sits in the barrel's declaration graph) prove
|
|
52
|
+
* every owned type here stays assignable to its real media-chrome counterpart;
|
|
53
|
+
* a peer version bump that narrows a prop incompatibly fails
|
|
54
|
+
* `pnpm --filter @elabs-ai/components-ai typecheck` locally instead of
|
|
55
|
+
* reaching a consumer as silent drift.
|
|
56
|
+
*
|
|
57
|
+
* Deliberately no `ref` field: `ComponentProps<typeof MediaController>` (and
|
|
58
|
+
* its nine siblings) included one via `RefAttributes<I>`, but `RefObject<T>`'s
|
|
59
|
+
* mutable `current` makes ref types INVARIANT in `T` — a `Ref<HTMLElement>`
|
|
60
|
+
* can never be assignable to a real element's own `Ref<MediaController>` (the
|
|
61
|
+
* custom element subclass carries private/extra members `HTMLElement` lacks),
|
|
62
|
+
* so keeping it would make every owned type fail its own conformance
|
|
63
|
+
* assertion below. This IS a real-world break for a React 19 consumer: none
|
|
64
|
+
* of the ten parts this package renders is wrapped in `forwardRef`, but
|
|
65
|
+
* `MediaController` (and its siblings) are genuine `forwardRef` components,
|
|
66
|
+
* and React 19's ref-as-prop carries a `ref` straight through an ordinary
|
|
67
|
+
* function component's `{...props}` spread to the real underlying element —
|
|
68
|
+
* see the CHANGELOG's "Breaking (types)" entry for the measured detail. It is
|
|
69
|
+
* inert only under React 18.
|
|
30
70
|
*/
|
|
71
|
+
export type AudioPlayerPartProps = HTMLAttributes<HTMLElement>;
|
|
72
|
+
|
|
31
73
|
type MediaChromeModule = typeof import("./_audio-player-media-chrome");
|
|
32
74
|
|
|
33
75
|
const lazyPart = <P,>(pick: (module: MediaChromeModule) => ComponentType<P>) =>
|
|
34
76
|
lazy(() => import("./_audio-player-media-chrome").then((m) => ({ default: pick(m) })));
|
|
35
77
|
|
|
36
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The primitive-typed (`string`/`boolean`/`number`) extra instance members of
|
|
80
|
+
* `media-chrome/react`'s `MediaController` — restored per issue #101's round-2
|
|
81
|
+
* validation (R2): each is a single declaration line mirroring the real peer
|
|
82
|
+
* type's name and shape, referencing nothing peer-owned, so it costs nothing
|
|
83
|
+
* against the `.d.ts` leak this fix exists to prevent. Deliberately EXCLUDED:
|
|
84
|
+
* `audio` (already narrowed out — `_audio-player-media-chrome.tsx`'s
|
|
85
|
+
* conformance assertion Omits it from the real `ComponentProps`, since this
|
|
86
|
+
* package renders its own `<AudioPlayerElement>` instead of passing one
|
|
87
|
+
* through), and every PEER/COMPLEX-typed member (`mediaStore`, `hotkeys`
|
|
88
|
+
* (`AttributeTokenList`), `media`/`fullscreenElement` (DOM elements),
|
|
89
|
+
* `mediaStateReceivers`, `associatedElementSubscriptions`, every
|
|
90
|
+
* `HTMLMediaElement` callback and every method) — those stay narrowed to
|
|
91
|
+
* `AudioPlayerPartProps` alone; restoring them would re-import the peer type
|
|
92
|
+
* graph. See `_audio-player-media-chrome.tsx`'s
|
|
93
|
+
* `_AudioPlayerPropsConformance` assertion, which fails typecheck if this
|
|
94
|
+
* interface ever drifts wider than the real element's own props.
|
|
95
|
+
*
|
|
96
|
+
* Doc comments below are inferred from each member's name and media-chrome's
|
|
97
|
+
* public attribute-naming convention (`no*` = boolean opt-out of the
|
|
98
|
+
* corresponding default-on behavior) — media-chrome ships no per-property doc
|
|
99
|
+
* comments in its own `.d.ts` or README to copy from. Treat them as a reading
|
|
100
|
+
* aid, not an authoritative spec; verify against media-chrome's source before
|
|
101
|
+
* relying on exact semantics.
|
|
102
|
+
*/
|
|
103
|
+
export interface AudioPlayerProps extends AudioPlayerPartProps {
|
|
104
|
+
/** Seconds of inactivity before controls hide; a string because it mirrors the HTML attribute value. */
|
|
105
|
+
autohide?: string;
|
|
106
|
+
/** Also autohide while the pointer is over the control elements themselves, not just the media. */
|
|
107
|
+
autohideOverControls?: boolean;
|
|
108
|
+
/** Breakpoint definitions (space-separated `name:width` pairs) driving `breakpointsComputed`. */
|
|
109
|
+
breakpoints?: string;
|
|
110
|
+
/** Whether the controller has computed its current breakpoint(s) at least once. */
|
|
111
|
+
breakpointsComputed?: boolean;
|
|
112
|
+
/** Fallback duration (seconds) to display before the real media duration is known. */
|
|
113
|
+
defaultDuration?: number;
|
|
114
|
+
/** Default stream type ("on-demand" or "live") before the media has resolved its own. */
|
|
115
|
+
defaultStreamType?: string;
|
|
116
|
+
/** Whether subtitles/captions are enabled by default. */
|
|
117
|
+
defaultSubtitles?: boolean;
|
|
118
|
+
/** Disables the controller's built-in tap/gesture handling (e.g. tap-to-play, double-tap-to-seek). */
|
|
119
|
+
gesturesDisabled?: boolean;
|
|
120
|
+
/** Whether hotkey keyboard control is currently active on the controller. */
|
|
121
|
+
keyboardControl?: boolean;
|
|
122
|
+
/** Space-separated list of hotkey names currently in use, for conflict detection with other listeners. */
|
|
123
|
+
keysUsed?: string;
|
|
124
|
+
/** Seconds behind the live edge still considered "at" live, for a live stream's seek range. */
|
|
125
|
+
liveEdgeOffset?: number;
|
|
126
|
+
/** Disables the controller's autohide-controls-on-inactivity behavior entirely. */
|
|
127
|
+
noAutohide?: boolean;
|
|
128
|
+
/** Disables automatically seeking back to the live edge after a manual seek on a live stream. */
|
|
129
|
+
noAutoSeekToLive?: boolean;
|
|
130
|
+
/** Opts out of the controller creating its own default internal media store. */
|
|
131
|
+
noDefaultStore?: boolean;
|
|
132
|
+
/** Disables the controller's built-in keyboard hotkeys entirely. */
|
|
133
|
+
noHotkeys?: boolean;
|
|
134
|
+
/** Opts out of remembering the user's mute preference across sessions. */
|
|
135
|
+
noMutedPref?: boolean;
|
|
136
|
+
/** Opts out of remembering the user's subtitles-language preference across sessions. */
|
|
137
|
+
noSubtitlesLangPref?: boolean;
|
|
138
|
+
/** Opts out of remembering the user's volume preference across sessions. */
|
|
139
|
+
noVolumePref?: boolean;
|
|
140
|
+
/** The language the controller resolved for its UI text, after applying any language preference. */
|
|
141
|
+
resolvedLang?: string;
|
|
142
|
+
/** Whether the user has interacted with the controller yet (affects autoplay-adjacent UI). */
|
|
143
|
+
userInteractive?: boolean;
|
|
144
|
+
}
|
|
37
145
|
|
|
38
146
|
const AudioPlayerImpl = lazyPart<AudioPlayerProps>((m) => m.AudioPlayer);
|
|
39
147
|
|
|
148
|
+
/**
|
|
149
|
+
* The settled-failure stand-in for the `LazyEngineBoundary` above — rendered
|
|
150
|
+
* once the `media-chrome` load has genuinely failed, never while it is still
|
|
151
|
+
* pending (that stays `Skeleton`, via the `Suspense fallback` below). A
|
|
152
|
+
* loading skeleton left in place after a settled failure reads as "still
|
|
153
|
+
* loading, forever" (loading-states.md) — this is a terminal state, so it
|
|
154
|
+
* gets a real (if compact) notice instead, sized to the same `h-10` slot so
|
|
155
|
+
* nothing shifts when the boundary trips.
|
|
156
|
+
*/
|
|
157
|
+
const AudioPlayerMissing = ({ className, error }: { className?: string; error: unknown }) => {
|
|
158
|
+
const { t } = useLocale();
|
|
159
|
+
const isPeerMissing = isOptionalPeerMissing(error);
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
className={cn(
|
|
163
|
+
"flex h-10 w-full items-center gap-2 rounded-md border border-dashed border-border bg-muted/30 px-3 text-caption text-muted-foreground",
|
|
164
|
+
className,
|
|
165
|
+
)}
|
|
166
|
+
role={isPeerMissing ? "status" : "alert"}
|
|
167
|
+
>
|
|
168
|
+
<VolumeOffIcon aria-hidden="true" className="size-4 shrink-0" />
|
|
169
|
+
<span className="truncate">
|
|
170
|
+
{isPeerMissing
|
|
171
|
+
? t("ai.error.engineMissingBody", {
|
|
172
|
+
feature: t("ai.audioPlayer.feature"),
|
|
173
|
+
packages: "media-chrome",
|
|
174
|
+
})
|
|
175
|
+
: t("ai.audioPlayer.renderError")}
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
40
181
|
export const AudioPlayer = ({ className, ...props }: AudioPlayerProps) => (
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
182
|
+
<LazyEngineBoundary
|
|
183
|
+
renderMissing={(error) => <AudioPlayerMissing className={className} error={error} />}
|
|
184
|
+
>
|
|
185
|
+
<Suspense fallback={<Skeleton className={cn("h-10 w-full rounded-md", className)} />}>
|
|
186
|
+
<AudioPlayerImpl className={className} data-slot="audio-player" {...props} />
|
|
187
|
+
</Suspense>
|
|
188
|
+
</LazyEngineBoundary>
|
|
44
189
|
);
|
|
45
190
|
|
|
46
191
|
export type AudioPlayerElementProps = Omit<ComponentProps<"audio">, "src"> &
|
|
@@ -63,108 +208,150 @@ export const AudioPlayerElement = ({ ...props }: AudioPlayerElementProps) => (
|
|
|
63
208
|
/>
|
|
64
209
|
);
|
|
65
210
|
|
|
66
|
-
export type AudioPlayerControlBarProps =
|
|
211
|
+
export type AudioPlayerControlBarProps = AudioPlayerPartProps;
|
|
67
212
|
|
|
68
213
|
const AudioPlayerControlBarImpl = lazyPart<AudioPlayerControlBarProps>(
|
|
69
214
|
(m) => m.AudioPlayerControlBar,
|
|
70
215
|
);
|
|
71
216
|
|
|
72
217
|
export const AudioPlayerControlBar = (props: AudioPlayerControlBarProps) => (
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
|
|
218
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
219
|
+
<Suspense fallback={null}>
|
|
220
|
+
<AudioPlayerControlBarImpl data-slot="audio-player-control-bar" {...props} />
|
|
221
|
+
</Suspense>
|
|
222
|
+
</LazyEngineBoundary>
|
|
76
223
|
);
|
|
77
224
|
|
|
78
|
-
export type AudioPlayerPlayButtonProps =
|
|
225
|
+
export type AudioPlayerPlayButtonProps = AudioPlayerPartProps;
|
|
79
226
|
|
|
80
227
|
const AudioPlayerPlayButtonImpl = lazyPart<AudioPlayerPlayButtonProps>(
|
|
81
228
|
(m) => m.AudioPlayerPlayButton,
|
|
82
229
|
);
|
|
83
230
|
|
|
84
231
|
export const AudioPlayerPlayButton = (props: AudioPlayerPlayButtonProps) => (
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
|
|
232
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
233
|
+
<Suspense fallback={null}>
|
|
234
|
+
<AudioPlayerPlayButtonImpl data-slot="audio-player-play-button" {...props} />
|
|
235
|
+
</Suspense>
|
|
236
|
+
</LazyEngineBoundary>
|
|
88
237
|
);
|
|
89
238
|
|
|
90
|
-
export
|
|
239
|
+
export interface AudioPlayerSeekBackwardButtonProps extends AudioPlayerPartProps {
|
|
240
|
+
/** Skip distance in seconds. Mirrors the element's own default (10). */
|
|
241
|
+
seekOffset?: number;
|
|
242
|
+
}
|
|
91
243
|
|
|
92
244
|
const AudioPlayerSeekBackwardButtonImpl = lazyPart<AudioPlayerSeekBackwardButtonProps>(
|
|
93
245
|
(m) => m.AudioPlayerSeekBackwardButton,
|
|
94
246
|
);
|
|
95
247
|
|
|
96
248
|
export const AudioPlayerSeekBackwardButton = (props: AudioPlayerSeekBackwardButtonProps) => (
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
|
|
249
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
250
|
+
<Suspense fallback={null}>
|
|
251
|
+
<AudioPlayerSeekBackwardButtonImpl data-slot="audio-player-seek-backward-button" {...props} />
|
|
252
|
+
</Suspense>
|
|
253
|
+
</LazyEngineBoundary>
|
|
100
254
|
);
|
|
101
255
|
|
|
102
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Primitive-typed extra members restored per issue #101's round-2 validation
|
|
258
|
+
* (R2) — same technique and same inferred-from-naming caveat as
|
|
259
|
+
* `AudioPlayerProps` above. Deliberately still narrowed out:
|
|
260
|
+
* `keysUsed` (here typed `string[]`, unlike the controller's own `string`
|
|
261
|
+
* member of the same name — an array is not primitive) and the DOM/method
|
|
262
|
+
* members `disable`/`enable`/`handleClick`/`tooltipEl`/`tooltipPlacement`.
|
|
263
|
+
*/
|
|
264
|
+
export interface AudioPlayerSeekForwardButtonProps extends AudioPlayerPartProps {
|
|
265
|
+
/** Whether the button is disabled. */
|
|
266
|
+
disabled?: boolean;
|
|
267
|
+
/** ID of the `<media-controller>` element this button controls, when it is not an ancestor. */
|
|
268
|
+
mediaController?: string;
|
|
269
|
+
/** The media's current playback time (seconds), as reflected by the controller's media store. */
|
|
270
|
+
mediaCurrentTime?: number;
|
|
271
|
+
/** Skip distance in seconds. Mirrors the element's own default (10). */
|
|
272
|
+
seekOffset?: number;
|
|
273
|
+
/** Suppresses the built-in hover tooltip on this button. */
|
|
274
|
+
noTooltip?: boolean;
|
|
275
|
+
/** Prevents the button's default click handling (for a consumer that wants to fully own the behavior). */
|
|
276
|
+
preventClick?: boolean;
|
|
277
|
+
}
|
|
103
278
|
|
|
104
279
|
const AudioPlayerSeekForwardButtonImpl = lazyPart<AudioPlayerSeekForwardButtonProps>(
|
|
105
280
|
(m) => m.AudioPlayerSeekForwardButton,
|
|
106
281
|
);
|
|
107
282
|
|
|
108
283
|
export const AudioPlayerSeekForwardButton = (props: AudioPlayerSeekForwardButtonProps) => (
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
284
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
285
|
+
<Suspense fallback={null}>
|
|
286
|
+
<AudioPlayerSeekForwardButtonImpl data-slot="audio-player-seek-forward-button" {...props} />
|
|
287
|
+
</Suspense>
|
|
288
|
+
</LazyEngineBoundary>
|
|
112
289
|
);
|
|
113
290
|
|
|
114
|
-
export type AudioPlayerTimeDisplayProps =
|
|
291
|
+
export type AudioPlayerTimeDisplayProps = AudioPlayerPartProps;
|
|
115
292
|
|
|
116
293
|
const AudioPlayerTimeDisplayImpl = lazyPart<AudioPlayerTimeDisplayProps>(
|
|
117
294
|
(m) => m.AudioPlayerTimeDisplay,
|
|
118
295
|
);
|
|
119
296
|
|
|
120
297
|
export const AudioPlayerTimeDisplay = (props: AudioPlayerTimeDisplayProps) => (
|
|
121
|
-
<
|
|
122
|
-
<
|
|
123
|
-
|
|
298
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
299
|
+
<Suspense fallback={null}>
|
|
300
|
+
<AudioPlayerTimeDisplayImpl data-slot="audio-player-time-display" {...props} />
|
|
301
|
+
</Suspense>
|
|
302
|
+
</LazyEngineBoundary>
|
|
124
303
|
);
|
|
125
304
|
|
|
126
|
-
export type AudioPlayerTimeRangeProps =
|
|
305
|
+
export type AudioPlayerTimeRangeProps = AudioPlayerPartProps;
|
|
127
306
|
|
|
128
307
|
const AudioPlayerTimeRangeImpl = lazyPart<AudioPlayerTimeRangeProps>((m) => m.AudioPlayerTimeRange);
|
|
129
308
|
|
|
130
309
|
export const AudioPlayerTimeRange = (props: AudioPlayerTimeRangeProps) => (
|
|
131
|
-
<
|
|
132
|
-
<
|
|
133
|
-
|
|
310
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
311
|
+
<Suspense fallback={null}>
|
|
312
|
+
<AudioPlayerTimeRangeImpl data-slot="audio-player-time-range" {...props} />
|
|
313
|
+
</Suspense>
|
|
314
|
+
</LazyEngineBoundary>
|
|
134
315
|
);
|
|
135
316
|
|
|
136
|
-
export type AudioPlayerDurationDisplayProps =
|
|
317
|
+
export type AudioPlayerDurationDisplayProps = AudioPlayerPartProps;
|
|
137
318
|
|
|
138
319
|
const AudioPlayerDurationDisplayImpl = lazyPart<AudioPlayerDurationDisplayProps>(
|
|
139
320
|
(m) => m.AudioPlayerDurationDisplay,
|
|
140
321
|
);
|
|
141
322
|
|
|
142
323
|
export const AudioPlayerDurationDisplay = (props: AudioPlayerDurationDisplayProps) => (
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
|
|
324
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
325
|
+
<Suspense fallback={null}>
|
|
326
|
+
<AudioPlayerDurationDisplayImpl data-slot="audio-player-duration-display" {...props} />
|
|
327
|
+
</Suspense>
|
|
328
|
+
</LazyEngineBoundary>
|
|
146
329
|
);
|
|
147
330
|
|
|
148
|
-
export type AudioPlayerMuteButtonProps =
|
|
331
|
+
export type AudioPlayerMuteButtonProps = AudioPlayerPartProps;
|
|
149
332
|
|
|
150
333
|
const AudioPlayerMuteButtonImpl = lazyPart<AudioPlayerMuteButtonProps>(
|
|
151
334
|
(m) => m.AudioPlayerMuteButton,
|
|
152
335
|
);
|
|
153
336
|
|
|
154
337
|
export const AudioPlayerMuteButton = (props: AudioPlayerMuteButtonProps) => (
|
|
155
|
-
<
|
|
156
|
-
<
|
|
157
|
-
|
|
338
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
339
|
+
<Suspense fallback={null}>
|
|
340
|
+
<AudioPlayerMuteButtonImpl data-slot="audio-player-mute-button" {...props} />
|
|
341
|
+
</Suspense>
|
|
342
|
+
</LazyEngineBoundary>
|
|
158
343
|
);
|
|
159
344
|
|
|
160
|
-
export type AudioPlayerVolumeRangeProps =
|
|
345
|
+
export type AudioPlayerVolumeRangeProps = AudioPlayerPartProps;
|
|
161
346
|
|
|
162
347
|
const AudioPlayerVolumeRangeImpl = lazyPart<AudioPlayerVolumeRangeProps>(
|
|
163
348
|
(m) => m.AudioPlayerVolumeRange,
|
|
164
349
|
);
|
|
165
350
|
|
|
166
351
|
export const AudioPlayerVolumeRange = (props: AudioPlayerVolumeRangeProps) => (
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
|
|
352
|
+
<LazyEngineBoundary renderMissing={() => null}>
|
|
353
|
+
<Suspense fallback={null}>
|
|
354
|
+
<AudioPlayerVolumeRangeImpl data-slot="audio-player-volume-range" {...props} />
|
|
355
|
+
</Suspense>
|
|
356
|
+
</LazyEngineBoundary>
|
|
170
357
|
);
|