@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.
Files changed (115) hide show
  1. package/README.md +4 -6
  2. package/dist/_lazy-engine-boundary.js +1 -1
  3. package/dist/_persona-rive.js.map +1 -1
  4. package/dist/_streamdown-i18n.js +2 -2
  5. package/dist/agent.js +2 -2
  6. package/dist/asset-preview.js +5 -5
  7. package/dist/attachments.js +1 -1
  8. package/dist/audio-player.js +1 -2
  9. package/dist/{chunk-7RL6KZDF.js → chunk-24M6XWVJ.js} +25 -17
  10. package/dist/chunk-24M6XWVJ.js.map +1 -0
  11. package/dist/{chunk-WU5BLGTF.js → chunk-3KXIWGHW.js} +3 -3
  12. package/dist/{chunk-TWPHAJNM.js → chunk-3UHR3RLZ.js} +12 -6
  13. package/dist/chunk-3UHR3RLZ.js.map +1 -0
  14. package/dist/{chunk-V4DORGWJ.js → chunk-5AMSNG2T.js} +2 -2
  15. package/dist/chunk-D7BZMRI3.js +2 -0
  16. package/dist/chunk-D7BZMRI3.js.map +1 -0
  17. package/dist/{chunk-PQBF6ZIQ.js → chunk-DI645WR4.js} +4 -4
  18. package/dist/{chunk-NCL2UZKI.js → chunk-FFA7OPEH.js} +2 -2
  19. package/dist/{chunk-34CQ2VHM.js → chunk-FO44KY7Y.js} +10 -7
  20. package/dist/chunk-FO44KY7Y.js.map +1 -0
  21. package/dist/{chunk-RB7G65JC.js → chunk-GP7ZZCID.js} +7 -5
  22. package/dist/chunk-GP7ZZCID.js.map +1 -0
  23. package/dist/{chunk-6PB2MXCI.js → chunk-HLODND65.js} +2 -2
  24. package/dist/{chunk-ADOYAGGK.js → chunk-IFVO7LQJ.js} +2 -2
  25. package/dist/{chunk-2XOIG5TG.js → chunk-JSEV46SH.js} +18 -12
  26. package/dist/chunk-JSEV46SH.js.map +1 -0
  27. package/dist/{chunk-CZP3RHC4.js → chunk-LY7O4BMP.js} +2 -2
  28. package/dist/{chunk-PTWCTSZL.js → chunk-OCZNA5SO.js} +4 -4
  29. package/dist/chunk-OCZNA5SO.js.map +1 -0
  30. package/dist/{chunk-S3ISPW3A.js → chunk-PWQPVOZJ.js} +1 -1
  31. package/dist/chunk-PWQPVOZJ.js.map +1 -0
  32. package/dist/chunk-RLV64EWK.js +41 -0
  33. package/dist/chunk-RLV64EWK.js.map +1 -0
  34. package/dist/chunk-SSES3F4Q.js +126 -0
  35. package/dist/chunk-SSES3F4Q.js.map +1 -0
  36. package/dist/{chunk-3XNPMDZY.js → chunk-TBCMCF5O.js} +9 -56
  37. package/dist/chunk-TBCMCF5O.js.map +1 -0
  38. package/dist/{chunk-FERYDFZA.js → chunk-WXUXFDJQ.js} +2 -2
  39. package/dist/{chunk-X47QMM6K.js → chunk-YLN7E4OV.js} +2 -2
  40. package/dist/code-block.js +1 -1
  41. package/dist/diff-view.js +2 -2
  42. package/dist/gallery.js +1 -1
  43. package/dist/generated-image.js +9 -0
  44. package/dist/generated-image.js.map +1 -0
  45. package/dist/grouped-parts.js +6 -6
  46. package/dist/image.js +4 -4
  47. package/dist/index.d.ts +92 -150
  48. package/dist/index.js +67 -65
  49. package/dist/markdown-view.js +3 -3
  50. package/dist/message.js +3 -3
  51. package/dist/model-provider-logo.js +1 -1
  52. package/dist/persona.js +2 -2
  53. package/dist/queue.js +1 -1
  54. package/dist/reasoning.js +3 -3
  55. package/dist/sandbox.js +3 -3
  56. package/dist/tool.js +2 -2
  57. package/package.json +8 -13
  58. package/schemas/a2ui-surface.v1.schema.json +5 -2
  59. package/src/__contract__/{image.contract.test.tsx → attachments.contract.test.tsx} +7 -7
  60. package/src/__contract__/generated-image.contract.test.tsx +49 -0
  61. package/src/_lazy-boundary-conformance.ts +12 -18
  62. package/src/_lazy-engine-boundary.tsx +1 -2
  63. package/src/_lazy-mermaid-absent.test.ts +2 -2
  64. package/src/_media-fixtures.ts +47 -0
  65. package/src/_persona-rive.tsx +1 -2
  66. package/src/a2ui/catalog.source.json +3 -2
  67. package/src/asset-preview.stories.tsx +12 -0
  68. package/src/asset-preview.test.tsx +14 -0
  69. package/src/asset-preview.tsx +4 -2
  70. package/src/attachments.stories.tsx +147 -0
  71. package/src/attachments.test.tsx +104 -0
  72. package/src/attachments.tsx +28 -20
  73. package/src/audio-player.stories.tsx +10 -45
  74. package/src/audio-player.test.tsx +166 -61
  75. package/src/audio-player.tsx +175 -266
  76. package/src/chat.stories.tsx +75 -3
  77. package/src/code-block.test.tsx +32 -0
  78. package/src/code-block.tsx +22 -5
  79. package/src/gallery.tsx +13 -77
  80. package/src/generated-image.stories.tsx +67 -0
  81. package/src/generated-image.test.tsx +107 -0
  82. package/src/generated-image.tsx +57 -0
  83. package/src/image.tsx +2 -115
  84. package/src/index.ts +1 -0
  85. package/src/model-provider-logo.tsx +20 -18
  86. package/src/queue.test.tsx +24 -0
  87. package/src/queue.tsx +8 -5
  88. package/src/tool.test.tsx +26 -0
  89. package/src/tool.tsx +2 -2
  90. package/dist/_audio-player-media-chrome.js +0 -85
  91. package/dist/_audio-player-media-chrome.js.map +0 -1
  92. package/dist/chunk-2XOIG5TG.js.map +0 -1
  93. package/dist/chunk-34CQ2VHM.js.map +0 -1
  94. package/dist/chunk-3XNPMDZY.js.map +0 -1
  95. package/dist/chunk-7RL6KZDF.js.map +0 -1
  96. package/dist/chunk-NSCLRHES.js +0 -102
  97. package/dist/chunk-NSCLRHES.js.map +0 -1
  98. package/dist/chunk-PTWCTSZL.js.map +0 -1
  99. package/dist/chunk-RB7G65JC.js.map +0 -1
  100. package/dist/chunk-S3ISPW3A.js.map +0 -1
  101. package/dist/chunk-TWPHAJNM.js.map +0 -1
  102. package/dist/chunk-V6E5DYZD.js +0 -80
  103. package/dist/chunk-V6E5DYZD.js.map +0 -1
  104. package/src/_audio-player-media-chrome.tsx +0 -233
  105. package/src/image.stories.tsx +0 -67
  106. package/src/image.test.tsx +0 -105
  107. /package/dist/{chunk-WU5BLGTF.js.map → chunk-3KXIWGHW.js.map} +0 -0
  108. /package/dist/{chunk-V4DORGWJ.js.map → chunk-5AMSNG2T.js.map} +0 -0
  109. /package/dist/{chunk-PQBF6ZIQ.js.map → chunk-DI645WR4.js.map} +0 -0
  110. /package/dist/{chunk-NCL2UZKI.js.map → chunk-FFA7OPEH.js.map} +0 -0
  111. /package/dist/{chunk-6PB2MXCI.js.map → chunk-HLODND65.js.map} +0 -0
  112. /package/dist/{chunk-ADOYAGGK.js.map → chunk-IFVO7LQJ.js.map} +0 -0
  113. /package/dist/{chunk-CZP3RHC4.js.map → chunk-LY7O4BMP.js.map} +0 -0
  114. /package/dist/{chunk-FERYDFZA.js.map → chunk-WXUXFDJQ.js.map} +0 -0
  115. /package/dist/{chunk-X47QMM6K.js.map → chunk-YLN7E4OV.js.map} +0 -0
@@ -1,78 +1,183 @@
1
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`.
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
- 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
- }));
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
- import { AudioPlayer, AudioPlayerPlayButton } from "./audio-player";
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
- afterEach(cleanup);
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
- const flush = () => act(async () => void (await new Promise((resolve) => setTimeout(resolve, 0))));
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 — 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(() => {});
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
- render(<AudioPlayer />);
49
- await flush();
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
- // 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();
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
- errorSpy.mockRestore();
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("degrades a sub-control to nothing rather than crashing", async () => {
65
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
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
- const { container } = render(<AudioPlayerPlayButton />);
68
- await flush();
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
- // `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();
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
- errorSpy.mockRestore();
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
  });