@elabs-ai/components-ai 5.3.1 → 5.5.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 +4 -6
- package/dist/_lazy-engine-boundary.js +1 -1
- package/dist/_persona-rive.js.map +1 -1
- package/dist/_streamdown-i18n.js +2 -2
- package/dist/agent.js +2 -2
- package/dist/asset-preview.js +5 -5
- package/dist/attachments.js +1 -1
- package/dist/audio-player.js +1 -2
- package/dist/{chunk-7RL6KZDF.js → chunk-24M6XWVJ.js} +25 -17
- package/dist/chunk-24M6XWVJ.js.map +1 -0
- package/dist/{chunk-WU5BLGTF.js → chunk-3KXIWGHW.js} +3 -3
- package/dist/{chunk-TWPHAJNM.js → chunk-3UHR3RLZ.js} +12 -6
- package/dist/chunk-3UHR3RLZ.js.map +1 -0
- package/dist/{chunk-V4DORGWJ.js → chunk-5AMSNG2T.js} +2 -2
- package/dist/chunk-D7BZMRI3.js +2 -0
- package/dist/chunk-D7BZMRI3.js.map +1 -0
- package/dist/{chunk-PQBF6ZIQ.js → chunk-DI645WR4.js} +4 -4
- package/dist/{chunk-NCL2UZKI.js → chunk-FFA7OPEH.js} +2 -2
- package/dist/{chunk-34CQ2VHM.js → chunk-FO44KY7Y.js} +10 -7
- package/dist/chunk-FO44KY7Y.js.map +1 -0
- package/dist/{chunk-RB7G65JC.js → chunk-GP7ZZCID.js} +7 -5
- package/dist/chunk-GP7ZZCID.js.map +1 -0
- package/dist/{chunk-6PB2MXCI.js → chunk-HLODND65.js} +2 -2
- package/dist/{chunk-ADOYAGGK.js → chunk-IFVO7LQJ.js} +2 -2
- package/dist/{chunk-2XOIG5TG.js → chunk-JSEV46SH.js} +18 -12
- package/dist/chunk-JSEV46SH.js.map +1 -0
- package/dist/{chunk-CZP3RHC4.js → chunk-LY7O4BMP.js} +2 -2
- package/dist/{chunk-PTWCTSZL.js → chunk-OCZNA5SO.js} +4 -4
- package/dist/chunk-OCZNA5SO.js.map +1 -0
- package/dist/{chunk-S3ISPW3A.js → chunk-PWQPVOZJ.js} +1 -1
- package/dist/chunk-PWQPVOZJ.js.map +1 -0
- package/dist/chunk-RLV64EWK.js +41 -0
- package/dist/chunk-RLV64EWK.js.map +1 -0
- package/dist/chunk-SSES3F4Q.js +126 -0
- package/dist/chunk-SSES3F4Q.js.map +1 -0
- package/dist/{chunk-3XNPMDZY.js → chunk-TBCMCF5O.js} +9 -56
- package/dist/chunk-TBCMCF5O.js.map +1 -0
- package/dist/{chunk-FERYDFZA.js → chunk-WXUXFDJQ.js} +2 -2
- package/dist/{chunk-X47QMM6K.js → chunk-YLN7E4OV.js} +2 -2
- package/dist/code-block.js +1 -1
- package/dist/diff-view.js +2 -2
- package/dist/gallery.js +1 -1
- package/dist/generated-image.js +9 -0
- package/dist/generated-image.js.map +1 -0
- package/dist/grouped-parts.js +6 -6
- package/dist/image.js +4 -4
- package/dist/index.d.ts +92 -150
- package/dist/index.js +67 -65
- package/dist/markdown-view.js +3 -3
- package/dist/message.js +3 -3
- package/dist/model-provider-logo.js +1 -1
- package/dist/persona.js +2 -2
- package/dist/queue.js +1 -1
- package/dist/reasoning.js +3 -3
- package/dist/sandbox.js +3 -3
- package/dist/tool.js +2 -2
- package/package.json +8 -13
- package/schemas/a2ui-surface.v1.schema.json +5 -2
- package/src/__contract__/{image.contract.test.tsx → attachments.contract.test.tsx} +7 -7
- package/src/__contract__/generated-image.contract.test.tsx +49 -0
- package/src/_lazy-boundary-conformance.ts +12 -18
- package/src/_lazy-engine-boundary.tsx +1 -2
- package/src/_lazy-mermaid-absent.test.ts +2 -2
- package/src/_media-fixtures.ts +47 -0
- package/src/_persona-rive.tsx +1 -2
- package/src/a2ui/catalog.source.json +3 -2
- package/src/asset-preview.stories.tsx +12 -0
- package/src/asset-preview.test.tsx +14 -0
- package/src/asset-preview.tsx +4 -2
- package/src/attachments.stories.tsx +147 -0
- package/src/attachments.test.tsx +104 -0
- package/src/attachments.tsx +28 -20
- package/src/audio-player.stories.tsx +10 -45
- package/src/audio-player.test.tsx +166 -61
- package/src/audio-player.tsx +175 -266
- package/src/chat.stories.tsx +75 -3
- package/src/code-block.test.tsx +32 -0
- package/src/code-block.tsx +22 -5
- package/src/gallery.tsx +13 -77
- package/src/generated-image.stories.tsx +67 -0
- package/src/generated-image.test.tsx +107 -0
- package/src/generated-image.tsx +57 -0
- package/src/image.tsx +2 -115
- package/src/index.ts +1 -0
- package/src/model-provider-logo.tsx +20 -18
- package/src/queue.test.tsx +24 -0
- package/src/queue.tsx +8 -5
- package/src/tool.test.tsx +26 -0
- package/src/tool.tsx +2 -2
- package/dist/_audio-player-media-chrome.js +0 -85
- package/dist/_audio-player-media-chrome.js.map +0 -1
- package/dist/chunk-2XOIG5TG.js.map +0 -1
- package/dist/chunk-34CQ2VHM.js.map +0 -1
- package/dist/chunk-3XNPMDZY.js.map +0 -1
- package/dist/chunk-7RL6KZDF.js.map +0 -1
- package/dist/chunk-NSCLRHES.js +0 -102
- package/dist/chunk-NSCLRHES.js.map +0 -1
- package/dist/chunk-PTWCTSZL.js.map +0 -1
- package/dist/chunk-RB7G65JC.js.map +0 -1
- package/dist/chunk-S3ISPW3A.js.map +0 -1
- package/dist/chunk-TWPHAJNM.js.map +0 -1
- package/dist/chunk-V6E5DYZD.js +0 -80
- package/dist/chunk-V6E5DYZD.js.map +0 -1
- package/src/_audio-player-media-chrome.tsx +0 -233
- package/src/image.stories.tsx +0 -67
- package/src/image.test.tsx +0 -105
- /package/dist/{chunk-WU5BLGTF.js.map → chunk-3KXIWGHW.js.map} +0 -0
- /package/dist/{chunk-V4DORGWJ.js.map → chunk-5AMSNG2T.js.map} +0 -0
- /package/dist/{chunk-PQBF6ZIQ.js.map → chunk-DI645WR4.js.map} +0 -0
- /package/dist/{chunk-NCL2UZKI.js.map → chunk-FFA7OPEH.js.map} +0 -0
- /package/dist/{chunk-6PB2MXCI.js.map → chunk-HLODND65.js.map} +0 -0
- /package/dist/{chunk-ADOYAGGK.js.map → chunk-IFVO7LQJ.js.map} +0 -0
- /package/dist/{chunk-CZP3RHC4.js.map → chunk-LY7O4BMP.js.map} +0 -0
- /package/dist/{chunk-FERYDFZA.js.map → chunk-WXUXFDJQ.js.map} +0 -0
- /package/dist/{chunk-X47QMM6K.js.map → chunk-YLN7E4OV.js.map} +0 -0
|
@@ -1,78 +1,183 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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`.
|
|
2
|
+
* `AudioPlayer*` are presets over ui's `MediaPlayer*` parts (ADR 0041). These
|
|
3
|
+
* lock the surface: every preset emits the shared `media-player*` slot (and no
|
|
4
|
+
* `audio-player*` one), the former legacy pass-through props never reach the
|
|
5
|
+
* DOM, and the controls
|
|
6
|
+
* really drive the `<audio>` (play/pause, seek, mute, keyboard map).
|
|
25
7
|
*/
|
|
26
|
-
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
27
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
28
10
|
|
|
29
|
-
|
|
30
|
-
AudioPlayer
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
11
|
+
import {
|
|
12
|
+
AudioPlayer,
|
|
13
|
+
AudioPlayerControlBar,
|
|
14
|
+
AudioPlayerDurationDisplay,
|
|
15
|
+
AudioPlayerElement,
|
|
16
|
+
AudioPlayerMuteButton,
|
|
17
|
+
AudioPlayerPlayButton,
|
|
18
|
+
AudioPlayerSeekBackwardButton,
|
|
19
|
+
AudioPlayerSeekForwardButton,
|
|
20
|
+
AudioPlayerTimeDisplay,
|
|
21
|
+
AudioPlayerTimeRange,
|
|
22
|
+
AudioPlayerVolumeRange,
|
|
23
|
+
type AudioPlayerProps,
|
|
24
|
+
} from "./audio-player";
|
|
37
25
|
|
|
38
|
-
|
|
26
|
+
// jsdom has no media pipeline: stub per file (never in vitest.setup.ts).
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
Object.defineProperty(HTMLMediaElement.prototype, "duration", {
|
|
29
|
+
configurable: true,
|
|
30
|
+
get: () => 60,
|
|
31
|
+
});
|
|
32
|
+
vi.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(function (
|
|
33
|
+
this: HTMLMediaElement,
|
|
34
|
+
) {
|
|
35
|
+
Object.defineProperty(this, "paused", { configurable: true, value: false });
|
|
36
|
+
this.dispatchEvent(new Event("play"));
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
});
|
|
39
|
+
vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(function (
|
|
40
|
+
this: HTMLMediaElement,
|
|
41
|
+
) {
|
|
42
|
+
Object.defineProperty(this, "paused", { configurable: true, value: true });
|
|
43
|
+
this.dispatchEvent(new Event("pause"));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
cleanup();
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
// Drop the per-file duration stub so jsdom's own getter is back for other files.
|
|
50
|
+
delete (HTMLMediaElement.prototype as { duration?: number }).duration;
|
|
51
|
+
});
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
/** Shared ui slots the presets emit; the seek and time presets share one each. */
|
|
54
|
+
const SLOTS = [
|
|
55
|
+
"media-player",
|
|
56
|
+
"media-player-element",
|
|
57
|
+
"media-player-controls",
|
|
58
|
+
"media-player-seek-button",
|
|
59
|
+
"media-player-play-button",
|
|
60
|
+
"media-player-time",
|
|
61
|
+
"media-player-time-slider",
|
|
62
|
+
"media-player-mute-button",
|
|
63
|
+
"media-player-volume-slider",
|
|
64
|
+
];
|
|
65
|
+
const BACKWARD = '[data-slot="media-player-seek-button"][data-direction="backward"]';
|
|
66
|
+
const FORWARD = '[data-slot="media-player-seek-button"][data-direction="forward"]';
|
|
41
67
|
|
|
42
|
-
|
|
68
|
+
function renderPlayer(props: Partial<AudioPlayerProps> = {}) {
|
|
69
|
+
const utils = render(
|
|
70
|
+
<AudioPlayer {...props}>
|
|
71
|
+
<AudioPlayerElement src="/speech.mp3" />
|
|
72
|
+
<AudioPlayerControlBar>
|
|
73
|
+
<AudioPlayerSeekBackwardButton />
|
|
74
|
+
<AudioPlayerPlayButton />
|
|
75
|
+
<AudioPlayerSeekForwardButton seekOffset={5} />
|
|
76
|
+
<AudioPlayerTimeDisplay />
|
|
77
|
+
<AudioPlayerTimeRange />
|
|
78
|
+
<AudioPlayerDurationDisplay />
|
|
79
|
+
<AudioPlayerMuteButton />
|
|
80
|
+
<AudioPlayerVolumeRange />
|
|
81
|
+
</AudioPlayerControlBar>
|
|
82
|
+
</AudioPlayer>,
|
|
83
|
+
);
|
|
84
|
+
const audio = utils.container.querySelector("audio") as HTMLAudioElement;
|
|
85
|
+
// Let `useMediaState` read the stubbed duration.
|
|
86
|
+
act(() => {
|
|
87
|
+
audio.dispatchEvent(new Event("loadedmetadata"));
|
|
88
|
+
audio.dispatchEvent(new Event("durationchange"));
|
|
89
|
+
});
|
|
90
|
+
return { ...utils, audio };
|
|
91
|
+
}
|
|
43
92
|
|
|
44
|
-
describe("AudioPlayer
|
|
45
|
-
it("
|
|
46
|
-
const
|
|
93
|
+
describe("AudioPlayer presets", () => {
|
|
94
|
+
it("emits the shared media-player* slots and none of the old audio-player* ones", () => {
|
|
95
|
+
const { container } = renderPlayer();
|
|
96
|
+
for (const slot of SLOTS) {
|
|
97
|
+
expect(container.querySelector(`[data-slot="${slot}"]`), slot).not.toBeNull();
|
|
98
|
+
}
|
|
99
|
+
expect(container.querySelectorAll('[data-slot="media-player-seek-button"]')).toHaveLength(2);
|
|
100
|
+
expect(container.querySelectorAll('[data-slot="media-player-time"]')).toHaveLength(2);
|
|
101
|
+
expect(container.querySelector('[data-slot^="audio-player"]')).toBeNull();
|
|
102
|
+
expect(container.querySelector("[slot]")).toBeNull();
|
|
103
|
+
});
|
|
47
104
|
|
|
48
|
-
|
|
49
|
-
|
|
105
|
+
it("renders a speech result as a data URL", () => {
|
|
106
|
+
const { container } = render(
|
|
107
|
+
<AudioPlayer>
|
|
108
|
+
<AudioPlayerElement data={{ base64: "AAAA", mediaType: "audio/wav" } as never} />
|
|
109
|
+
</AudioPlayer>,
|
|
110
|
+
);
|
|
111
|
+
const audio = container.querySelector("audio");
|
|
112
|
+
expect(audio).toHaveAttribute("src", "data:audio/wav;base64,AAAA");
|
|
113
|
+
expect(audio).not.toHaveAttribute("data");
|
|
114
|
+
});
|
|
50
115
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
116
|
+
it("never puts the deprecated legacy controller props on the DOM", () => {
|
|
117
|
+
const { container } = renderPlayer({
|
|
118
|
+
autohide: "2",
|
|
119
|
+
breakpoints: "sm:300",
|
|
120
|
+
defaultDuration: 10,
|
|
121
|
+
gesturesDisabled: true,
|
|
122
|
+
noAutohide: true,
|
|
123
|
+
noVolumePref: true,
|
|
124
|
+
resolvedLang: "en",
|
|
125
|
+
userInteractive: true,
|
|
126
|
+
});
|
|
127
|
+
const root = container.querySelector('[data-slot="media-player"]') as HTMLElement;
|
|
128
|
+
for (const attr of [
|
|
129
|
+
"autohide",
|
|
130
|
+
"breakpoints",
|
|
131
|
+
"defaultduration",
|
|
132
|
+
"gesturesdisabled",
|
|
133
|
+
"noautohide",
|
|
134
|
+
"novolumepref",
|
|
135
|
+
"resolvedlang",
|
|
136
|
+
"userinteractive",
|
|
137
|
+
]) {
|
|
138
|
+
expect(root.hasAttribute(attr), attr).toBe(false);
|
|
139
|
+
}
|
|
140
|
+
const forward = container.querySelector(FORWARD) as HTMLElement;
|
|
141
|
+
expect(forward.hasAttribute("seekoffset")).toBe(false);
|
|
142
|
+
});
|
|
60
143
|
|
|
61
|
-
|
|
144
|
+
it("plays and pauses", () => {
|
|
145
|
+
const { audio } = renderPlayer();
|
|
146
|
+
fireEvent.click(screen.getByRole("button", { name: "Play" }));
|
|
147
|
+
expect(HTMLMediaElement.prototype.play).toHaveBeenCalled();
|
|
148
|
+
expect(audio.paused).toBe(false);
|
|
149
|
+
fireEvent.click(screen.getByRole("button", { name: "Pause" }));
|
|
150
|
+
expect(HTMLMediaElement.prototype.pause).toHaveBeenCalled();
|
|
151
|
+
expect(audio.paused).toBe(true);
|
|
62
152
|
});
|
|
63
153
|
|
|
64
|
-
it("
|
|
65
|
-
const
|
|
154
|
+
it("seeks by 10 s back by default and by the legacy seekOffset forward", () => {
|
|
155
|
+
const { audio, container } = renderPlayer();
|
|
156
|
+
audio.currentTime = 20;
|
|
157
|
+
fireEvent.click(container.querySelector(BACKWARD) as HTMLElement);
|
|
158
|
+
expect(audio.currentTime).toBe(10);
|
|
159
|
+
fireEvent.click(container.querySelector(FORWARD) as HTMLElement);
|
|
160
|
+
expect(audio.currentTime).toBe(15);
|
|
161
|
+
});
|
|
66
162
|
|
|
67
|
-
|
|
68
|
-
|
|
163
|
+
it("mutes and unmutes", () => {
|
|
164
|
+
const { audio } = renderPlayer();
|
|
165
|
+
fireEvent.click(screen.getByRole("button", { name: "Mute" }));
|
|
166
|
+
expect(audio.muted).toBe(true);
|
|
167
|
+
fireEvent.click(screen.getByRole("button", { name: "Unmute" }));
|
|
168
|
+
expect(audio.muted).toBe(false);
|
|
169
|
+
});
|
|
69
170
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
expect(
|
|
171
|
+
it("handles the keyboard map, and noHotkeys turns it off", () => {
|
|
172
|
+
const { audio, container, unmount } = renderPlayer();
|
|
173
|
+
const root = container.querySelector('[data-slot="media-player"]') as HTMLElement;
|
|
174
|
+
fireEvent.keyDown(root, { key: "m" });
|
|
175
|
+
expect(audio.muted).toBe(true);
|
|
176
|
+
unmount();
|
|
75
177
|
|
|
76
|
-
|
|
178
|
+
const off = renderPlayer({ noHotkeys: true });
|
|
179
|
+
const offRoot = off.container.querySelector('[data-slot="media-player"]') as HTMLElement;
|
|
180
|
+
fireEvent.keyDown(offRoot, { key: "m" });
|
|
181
|
+
expect(off.audio.muted).toBe(false);
|
|
77
182
|
});
|
|
78
183
|
});
|