@elabs-ai/components-ai 5.4.0 → 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 (112) 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-VJZJWNEC.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-AOOIAFDT.js → chunk-OCZNA5SO.js} +2 -2
  29. package/dist/{chunk-S3ISPW3A.js → chunk-PWQPVOZJ.js} +1 -1
  30. package/dist/chunk-PWQPVOZJ.js.map +1 -0
  31. package/dist/chunk-RLV64EWK.js +41 -0
  32. package/dist/chunk-RLV64EWK.js.map +1 -0
  33. package/dist/chunk-SSES3F4Q.js +126 -0
  34. package/dist/chunk-SSES3F4Q.js.map +1 -0
  35. package/dist/{chunk-3XNPMDZY.js → chunk-TBCMCF5O.js} +9 -56
  36. package/dist/chunk-TBCMCF5O.js.map +1 -0
  37. package/dist/{chunk-UHLVPAHG.js → chunk-WXUXFDJQ.js} +2 -2
  38. package/dist/{chunk-X47QMM6K.js → chunk-YLN7E4OV.js} +2 -2
  39. package/dist/code-block.js +1 -1
  40. package/dist/diff-view.js +2 -2
  41. package/dist/gallery.js +1 -1
  42. package/dist/generated-image.js +9 -0
  43. package/dist/generated-image.js.map +1 -0
  44. package/dist/grouped-parts.js +6 -6
  45. package/dist/image.js +4 -4
  46. package/dist/index.d.ts +92 -150
  47. package/dist/index.js +67 -65
  48. package/dist/markdown-view.js +3 -3
  49. package/dist/message.js +3 -3
  50. package/dist/model-provider-logo.js +1 -1
  51. package/dist/persona.js +2 -2
  52. package/dist/queue.js +1 -1
  53. package/dist/reasoning.js +3 -3
  54. package/dist/sandbox.js +3 -3
  55. package/dist/tool.js +2 -2
  56. package/package.json +8 -13
  57. package/schemas/a2ui-surface.v1.schema.json +1 -1
  58. package/src/__contract__/{image.contract.test.tsx → attachments.contract.test.tsx} +7 -7
  59. package/src/__contract__/generated-image.contract.test.tsx +49 -0
  60. package/src/_lazy-boundary-conformance.ts +12 -18
  61. package/src/_lazy-engine-boundary.tsx +1 -2
  62. package/src/_lazy-mermaid-absent.test.ts +2 -2
  63. package/src/_media-fixtures.ts +47 -0
  64. package/src/_persona-rive.tsx +1 -2
  65. package/src/a2ui/catalog.source.json +1 -1
  66. package/src/asset-preview.stories.tsx +12 -0
  67. package/src/asset-preview.test.tsx +14 -0
  68. package/src/asset-preview.tsx +4 -2
  69. package/src/attachments.stories.tsx +147 -0
  70. package/src/attachments.test.tsx +104 -0
  71. package/src/attachments.tsx +28 -20
  72. package/src/audio-player.stories.tsx +10 -45
  73. package/src/audio-player.test.tsx +166 -61
  74. package/src/audio-player.tsx +175 -266
  75. package/src/chat.stories.tsx +75 -3
  76. package/src/code-block.test.tsx +32 -0
  77. package/src/code-block.tsx +22 -5
  78. package/src/gallery.tsx +13 -77
  79. package/src/generated-image.stories.tsx +67 -0
  80. package/src/generated-image.test.tsx +107 -0
  81. package/src/generated-image.tsx +57 -0
  82. package/src/image.tsx +2 -115
  83. package/src/index.ts +1 -0
  84. package/src/model-provider-logo.tsx +20 -18
  85. package/src/queue.test.tsx +24 -0
  86. package/src/queue.tsx +8 -5
  87. package/dist/_audio-player-media-chrome.js +0 -85
  88. package/dist/_audio-player-media-chrome.js.map +0 -1
  89. package/dist/chunk-2XOIG5TG.js.map +0 -1
  90. package/dist/chunk-34CQ2VHM.js.map +0 -1
  91. package/dist/chunk-3XNPMDZY.js.map +0 -1
  92. package/dist/chunk-7RL6KZDF.js.map +0 -1
  93. package/dist/chunk-NSCLRHES.js +0 -102
  94. package/dist/chunk-NSCLRHES.js.map +0 -1
  95. package/dist/chunk-RB7G65JC.js.map +0 -1
  96. package/dist/chunk-S3ISPW3A.js.map +0 -1
  97. package/dist/chunk-TWPHAJNM.js.map +0 -1
  98. package/dist/chunk-V6E5DYZD.js +0 -80
  99. package/dist/chunk-V6E5DYZD.js.map +0 -1
  100. package/src/_audio-player-media-chrome.tsx +0 -233
  101. package/src/image.stories.tsx +0 -67
  102. package/src/image.test.tsx +0 -105
  103. /package/dist/{chunk-VJZJWNEC.js.map → chunk-3KXIWGHW.js.map} +0 -0
  104. /package/dist/{chunk-V4DORGWJ.js.map → chunk-5AMSNG2T.js.map} +0 -0
  105. /package/dist/{chunk-PQBF6ZIQ.js.map → chunk-DI645WR4.js.map} +0 -0
  106. /package/dist/{chunk-NCL2UZKI.js.map → chunk-FFA7OPEH.js.map} +0 -0
  107. /package/dist/{chunk-6PB2MXCI.js.map → chunk-HLODND65.js.map} +0 -0
  108. /package/dist/{chunk-ADOYAGGK.js.map → chunk-IFVO7LQJ.js.map} +0 -0
  109. /package/dist/{chunk-CZP3RHC4.js.map → chunk-LY7O4BMP.js.map} +0 -0
  110. /package/dist/{chunk-AOOIAFDT.js.map → chunk-OCZNA5SO.js.map} +0 -0
  111. /package/dist/{chunk-UHLVPAHG.js.map → chunk-WXUXFDJQ.js.map} +0 -0
  112. /package/dist/{chunk-X47QMM6K.js.map → chunk-YLN7E4OV.js.map} +0 -0
@@ -1,191 +1,146 @@
1
1
  "use client";
2
2
 
3
- import { Skeleton, isOptionalPeerMissing, useLocale } from "@elabs-ai/components-ui";
4
- import { cn } from "@elabs-ai/components-ui/lib/cn";
3
+ import {
4
+ MediaPlayer,
5
+ MediaPlayerControls,
6
+ MediaPlayerElement,
7
+ MediaPlayerError,
8
+ MediaPlayerLoading,
9
+ MediaPlayerMuteButton,
10
+ MediaPlayerPlayButton,
11
+ MediaPlayerSeekButton,
12
+ MediaPlayerTime,
13
+ MediaPlayerTimeSlider,
14
+ MediaPlayerVolumeSlider,
15
+ useLocale,
16
+ useMediaPlayer,
17
+ type MediaPlayerButtonProps,
18
+ type MediaPlayerControlsProps,
19
+ type MediaPlayerElementProps,
20
+ type MediaPlayerProps,
21
+ type MediaPlayerTimeProps,
22
+ type MediaPlayerTimeSliderProps,
23
+ type MediaPlayerVolumeSliderProps,
24
+ } from "@elabs-ai/components-ui";
5
25
  import type { Experimental_SpeechResult as SpeechResult } from "ai";
6
- import { VolumeOffIcon } from "lucide-react";
7
- import type { ComponentProps, ComponentType, HTMLAttributes } from "react";
8
- import { lazy, Suspense } from "react";
26
+ import type { ComponentProps, HTMLAttributes, Ref } from "react";
9
27
 
10
- import { LazyEngineBoundary } from "./_lazy-engine-boundary";
28
+ /**
29
+ * `AudioPlayer*` — the chat-facing names for spoken agent replies, as thin
30
+ * presets over ui's `MediaPlayer*` compound parts (ADR 0041). Each preset
31
+ * emits the SHARED slot of the ui part it wraps (`media-player*`, spelled out
32
+ * so this module declares what it renders); the former `audio-player*`
33
+ * selectors are gone (maintainer decision 2026-09-23, ADR 0041 §7). The ui
34
+ * part underneath owns the behaviour, labels and styling.
35
+ */
11
36
 
12
37
  /**
13
- * media-chrome lives behind a dynamic import — it declares no `sideEffects`, so
14
- * a static import would put the whole custom-element library in every consumer's
15
- * entry chunk, `AudioPlayer` rendered or not. Every value import lives in
16
- * `_audio-player-media-chrome.tsx`. See ADR 0019 and `pnpm heavy-deps:check`.
17
- *
18
- * All ten parts resolve from the SAME module specifier, so they share one chunk:
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.
38
+ * @deprecated The shared base of the former custom-element parts. Each part now
39
+ * has its own prop type (`AudioPlayerPlayButtonProps`, …); removed in the next
40
+ * major.
70
41
  */
71
42
  export type AudioPlayerPartProps = HTMLAttributes<HTMLElement>;
72
43
 
73
- type MediaChromeModule = typeof import("./_audio-player-media-chrome");
74
-
75
- const lazyPart = <P,>(pick: (module: MediaChromeModule) => ComponentType<P>) =>
76
- lazy(() => import("./_audio-player-media-chrome").then((m) => ({ default: pick(m) })));
77
-
78
44
  /**
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.
45
+ * The former custom-element controller attributes. Kept so existing
46
+ * call sites typecheck; `noHotkeys`/`keyboardControl` still map onto
47
+ * `keyboardShortcuts`, every other member is accepted and ignored. All are
48
+ * removed in the next major.
102
49
  */
103
- export interface AudioPlayerProps extends AudioPlayerPartProps {
104
- /** Seconds of inactivity before controls hide; a string because it mirrors the HTML attribute value. */
50
+ interface AudioPlayerLegacyProps {
51
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
105
52
  autohide?: string;
106
- /** Also autohide while the pointer is over the control elements themselves, not just the media. */
53
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
107
54
  autohideOverControls?: boolean;
108
- /** Breakpoint definitions (space-separated `name:width` pairs) driving `breakpointsComputed`. */
55
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
109
56
  breakpoints?: string;
110
- /** Whether the controller has computed its current breakpoint(s) at least once. */
57
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
111
58
  breakpointsComputed?: boolean;
112
- /** Fallback duration (seconds) to display before the real media duration is known. */
59
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
113
60
  defaultDuration?: number;
114
- /** Default stream type ("on-demand" or "live") before the media has resolved its own. */
61
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
115
62
  defaultStreamType?: string;
116
- /** Whether subtitles/captions are enabled by default. */
63
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
117
64
  defaultSubtitles?: boolean;
118
- /** Disables the controller's built-in tap/gesture handling (e.g. tap-to-play, double-tap-to-seek). */
65
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
119
66
  gesturesDisabled?: boolean;
120
- /** Whether hotkey keyboard control is currently active on the controller. */
67
+ /** @deprecated Use `keyboardShortcuts`. `false` turns the player's keyboard map off. */
121
68
  keyboardControl?: boolean;
122
- /** Space-separated list of hotkey names currently in use, for conflict detection with other listeners. */
69
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
123
70
  keysUsed?: string;
124
- /** Seconds behind the live edge still considered "at" live, for a live stream's seek range. */
71
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
125
72
  liveEdgeOffset?: number;
126
- /** Disables the controller's autohide-controls-on-inactivity behavior entirely. */
73
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
127
74
  noAutohide?: boolean;
128
- /** Disables automatically seeking back to the live edge after a manual seek on a live stream. */
75
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
129
76
  noAutoSeekToLive?: boolean;
130
- /** Opts out of the controller creating its own default internal media store. */
77
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
131
78
  noDefaultStore?: boolean;
132
- /** Disables the controller's built-in keyboard hotkeys entirely. */
79
+ /** @deprecated Use `keyboardShortcuts={false}`. */
133
80
  noHotkeys?: boolean;
134
- /** Opts out of remembering the user's mute preference across sessions. */
81
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
135
82
  noMutedPref?: boolean;
136
- /** Opts out of remembering the user's subtitles-language preference across sessions. */
83
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
137
84
  noSubtitlesLangPref?: boolean;
138
- /** Opts out of remembering the user's volume preference across sessions. */
85
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
139
86
  noVolumePref?: boolean;
140
- /** The language the controller resolved for its UI text, after applying any language preference. */
87
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
141
88
  resolvedLang?: string;
142
- /** Whether the user has interacted with the controller yet (affects autoplay-adjacent UI). */
89
+ /** @deprecated Ignored (legacy controller attribute); removed in the next major. */
143
90
  userInteractive?: boolean;
144
91
  }
145
92
 
146
- const AudioPlayerImpl = lazyPart<AudioPlayerProps>((m) => m.AudioPlayer);
93
+ export interface AudioPlayerProps extends Omit<MediaPlayerProps, "kind">, AudioPlayerLegacyProps {}
147
94
 
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 }) => {
95
+ /** Replaces the controls with the compact error panel once playback has terminally failed. */
96
+ function AudioPlayerErrorRung() {
158
97
  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
- };
98
+ const { state } = useMediaPlayer();
99
+ if (!state.error) return null;
100
+ return <MediaPlayerError title={t("ai.audioPlayer.renderError")} />;
101
+ }
180
102
 
181
- export const AudioPlayer = ({ className, ...props }: AudioPlayerProps) => (
182
- <LazyEngineBoundary
183
- renderMissing={(error) => <AudioPlayerMissing className={className} error={error} />}
103
+ /**
104
+ * The player root (`MediaPlayer kind="audio"`): transparent, so put it in the
105
+ * bubble or card that is its surface. Compose `AudioPlayerElement` plus an
106
+ * `AudioPlayerControlBar` inside.
107
+ */
108
+ export const AudioPlayer = ({
109
+ keyboardShortcuts,
110
+ noHotkeys,
111
+ keyboardControl,
112
+ children,
113
+ // Accepted and ignored (former legacy controller attributes).
114
+ autohide: _autohide,
115
+ autohideOverControls: _autohideOverControls,
116
+ breakpoints: _breakpoints,
117
+ breakpointsComputed: _breakpointsComputed,
118
+ defaultDuration: _defaultDuration,
119
+ defaultStreamType: _defaultStreamType,
120
+ defaultSubtitles: _defaultSubtitles,
121
+ gesturesDisabled: _gesturesDisabled,
122
+ keysUsed: _keysUsed,
123
+ liveEdgeOffset: _liveEdgeOffset,
124
+ noAutohide: _noAutohide,
125
+ noAutoSeekToLive: _noAutoSeekToLive,
126
+ noDefaultStore: _noDefaultStore,
127
+ noMutedPref: _noMutedPref,
128
+ noSubtitlesLangPref: _noSubtitlesLangPref,
129
+ noVolumePref: _noVolumePref,
130
+ resolvedLang: _resolvedLang,
131
+ userInteractive: _userInteractive,
132
+ ...props
133
+ }: AudioPlayerProps) => (
134
+ <MediaPlayer
135
+ kind="audio"
136
+ data-slot="media-player"
137
+ keyboardShortcuts={(keyboardShortcuts ?? true) && !noHotkeys && keyboardControl !== false}
138
+ {...props}
184
139
  >
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>
140
+ {children}
141
+ <MediaPlayerLoading />
142
+ <AudioPlayerErrorRung />
143
+ </MediaPlayer>
189
144
  );
190
145
 
191
146
  export type AudioPlayerElementProps = Omit<ComponentProps<"audio">, "src"> &
@@ -198,160 +153,114 @@ export type AudioPlayerElementProps = Omit<ComponentProps<"audio">, "src"> &
198
153
  }
199
154
  );
200
155
 
201
- export const AudioPlayerElement = ({ ...props }: AudioPlayerElementProps) => (
202
- // oxlint-disable-next-line eslint-plugin-jsx-a11y(media-has-caption) -- audio player captions are provided by consumer
203
- <audio
204
- data-slot="audio-player-element"
205
- slot="media"
206
- src={"src" in props ? props.src : `data:${props.data.mediaType};base64,${props.data.base64}`}
207
- {...props}
208
- />
209
- );
210
-
211
- export type AudioPlayerControlBarProps = AudioPlayerPartProps;
156
+ /**
157
+ * The `<audio>` element. Pass a URL as `src`, or an AI SDK speech result's
158
+ * `audio` as `data` (converted to a base64 data URL).
159
+ */
160
+ export const AudioPlayerElement = ({ ref, ...props }: AudioPlayerElementProps) => {
161
+ const { src, data, ...rest } = props as typeof props & {
162
+ src?: string;
163
+ data?: SpeechResult["audio"];
164
+ };
165
+ return (
166
+ <MediaPlayerElement
167
+ data-slot="media-player-element"
168
+ {...(rest as MediaPlayerElementProps)}
169
+ ref={ref as Ref<HTMLMediaElement> | undefined}
170
+ src={data ? `data:${data.mediaType};base64,${data.base64}` : src}
171
+ />
172
+ );
173
+ };
212
174
 
213
- const AudioPlayerControlBarImpl = lazyPart<AudioPlayerControlBarProps>(
214
- (m) => m.AudioPlayerControlBar,
215
- );
175
+ export type AudioPlayerControlBarProps = MediaPlayerControlsProps;
216
176
 
217
177
  export const AudioPlayerControlBar = (props: AudioPlayerControlBarProps) => (
218
- <LazyEngineBoundary renderMissing={() => null}>
219
- <Suspense fallback={null}>
220
- <AudioPlayerControlBarImpl data-slot="audio-player-control-bar" {...props} />
221
- </Suspense>
222
- </LazyEngineBoundary>
178
+ <MediaPlayerControls data-slot="media-player-controls" {...props} />
223
179
  );
224
180
 
225
- export type AudioPlayerPlayButtonProps = AudioPlayerPartProps;
226
-
227
- const AudioPlayerPlayButtonImpl = lazyPart<AudioPlayerPlayButtonProps>(
228
- (m) => m.AudioPlayerPlayButton,
229
- );
181
+ export type AudioPlayerPlayButtonProps = MediaPlayerButtonProps;
230
182
 
231
183
  export const AudioPlayerPlayButton = (props: AudioPlayerPlayButtonProps) => (
232
- <LazyEngineBoundary renderMissing={() => null}>
233
- <Suspense fallback={null}>
234
- <AudioPlayerPlayButtonImpl data-slot="audio-player-play-button" {...props} />
235
- </Suspense>
236
- </LazyEngineBoundary>
184
+ <MediaPlayerPlayButton data-slot="media-player-play-button" {...props} />
237
185
  );
238
186
 
239
- export interface AudioPlayerSeekBackwardButtonProps extends AudioPlayerPartProps {
240
- /** Skip distance in seconds. Mirrors the element's own default (10). */
187
+ /** Props shared by the two seek presets. */
188
+ interface AudioPlayerSeekPresetProps extends MediaPlayerButtonProps {
189
+ /** Seconds to skip, as a positive distance; the button sets the direction. Default 10. */
190
+ offset?: number;
191
+ /** @deprecated Use `offset`. */
241
192
  seekOffset?: number;
242
193
  }
243
194
 
244
- const AudioPlayerSeekBackwardButtonImpl = lazyPart<AudioPlayerSeekBackwardButtonProps>(
245
- (m) => m.AudioPlayerSeekBackwardButton,
246
- );
195
+ export type AudioPlayerSeekBackwardButtonProps = AudioPlayerSeekPresetProps;
247
196
 
248
- export const AudioPlayerSeekBackwardButton = (props: AudioPlayerSeekBackwardButtonProps) => (
249
- <LazyEngineBoundary renderMissing={() => null}>
250
- <Suspense fallback={null}>
251
- <AudioPlayerSeekBackwardButtonImpl data-slot="audio-player-seek-backward-button" {...props} />
252
- </Suspense>
253
- </LazyEngineBoundary>
197
+ export const AudioPlayerSeekBackwardButton = ({
198
+ offset,
199
+ seekOffset,
200
+ ...props
201
+ }: AudioPlayerSeekBackwardButtonProps) => (
202
+ <MediaPlayerSeekButton
203
+ data-slot="media-player-seek-button"
204
+ {...props}
205
+ offset={-Math.abs(offset ?? seekOffset ?? 10)}
206
+ />
254
207
  );
255
208
 
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. */
209
+ export interface AudioPlayerSeekForwardButtonProps extends AudioPlayerSeekPresetProps {
210
+ /** @deprecated Ignored (legacy attribute); removed in the next major. */
268
211
  mediaController?: string;
269
- /** The media's current playback time (seconds), as reflected by the controller's media store. */
212
+ /** @deprecated Ignored (legacy attribute); removed in the next major. */
270
213
  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. */
214
+ /** @deprecated Ignored (legacy attribute); removed in the next major. */
274
215
  noTooltip?: boolean;
275
- /** Prevents the button's default click handling (for a consumer that wants to fully own the behavior). */
216
+ /** @deprecated Ignored (legacy attribute); removed in the next major. */
276
217
  preventClick?: boolean;
277
218
  }
278
219
 
279
- const AudioPlayerSeekForwardButtonImpl = lazyPart<AudioPlayerSeekForwardButtonProps>(
280
- (m) => m.AudioPlayerSeekForwardButton,
281
- );
282
-
283
- export const AudioPlayerSeekForwardButton = (props: AudioPlayerSeekForwardButtonProps) => (
284
- <LazyEngineBoundary renderMissing={() => null}>
285
- <Suspense fallback={null}>
286
- <AudioPlayerSeekForwardButtonImpl data-slot="audio-player-seek-forward-button" {...props} />
287
- </Suspense>
288
- </LazyEngineBoundary>
220
+ export const AudioPlayerSeekForwardButton = ({
221
+ offset,
222
+ seekOffset,
223
+ mediaController: _mediaController,
224
+ mediaCurrentTime: _mediaCurrentTime,
225
+ noTooltip: _noTooltip,
226
+ preventClick: _preventClick,
227
+ ...props
228
+ }: AudioPlayerSeekForwardButtonProps) => (
229
+ <MediaPlayerSeekButton
230
+ data-slot="media-player-seek-button"
231
+ {...props}
232
+ offset={Math.abs(offset ?? seekOffset ?? 10)}
233
+ />
289
234
  );
290
235
 
291
- export type AudioPlayerTimeDisplayProps = AudioPlayerPartProps;
292
-
293
- const AudioPlayerTimeDisplayImpl = lazyPart<AudioPlayerTimeDisplayProps>(
294
- (m) => m.AudioPlayerTimeDisplay,
295
- );
236
+ export type AudioPlayerTimeDisplayProps = Omit<MediaPlayerTimeProps, "mode">;
296
237
 
238
+ /** The current playback position. */
297
239
  export const AudioPlayerTimeDisplay = (props: AudioPlayerTimeDisplayProps) => (
298
- <LazyEngineBoundary renderMissing={() => null}>
299
- <Suspense fallback={null}>
300
- <AudioPlayerTimeDisplayImpl data-slot="audio-player-time-display" {...props} />
301
- </Suspense>
302
- </LazyEngineBoundary>
240
+ <MediaPlayerTime data-slot="media-player-time" {...props} mode="current" />
303
241
  );
304
242
 
305
- export type AudioPlayerTimeRangeProps = AudioPlayerPartProps;
306
-
307
- const AudioPlayerTimeRangeImpl = lazyPart<AudioPlayerTimeRangeProps>((m) => m.AudioPlayerTimeRange);
243
+ export type AudioPlayerTimeRangeProps = MediaPlayerTimeSliderProps;
308
244
 
309
245
  export const AudioPlayerTimeRange = (props: AudioPlayerTimeRangeProps) => (
310
- <LazyEngineBoundary renderMissing={() => null}>
311
- <Suspense fallback={null}>
312
- <AudioPlayerTimeRangeImpl data-slot="audio-player-time-range" {...props} />
313
- </Suspense>
314
- </LazyEngineBoundary>
246
+ <MediaPlayerTimeSlider data-slot="media-player-time-slider" {...props} />
315
247
  );
316
248
 
317
- export type AudioPlayerDurationDisplayProps = AudioPlayerPartProps;
318
-
319
- const AudioPlayerDurationDisplayImpl = lazyPart<AudioPlayerDurationDisplayProps>(
320
- (m) => m.AudioPlayerDurationDisplay,
321
- );
249
+ export type AudioPlayerDurationDisplayProps = Omit<MediaPlayerTimeProps, "mode">;
322
250
 
251
+ /** The total duration. */
323
252
  export const AudioPlayerDurationDisplay = (props: AudioPlayerDurationDisplayProps) => (
324
- <LazyEngineBoundary renderMissing={() => null}>
325
- <Suspense fallback={null}>
326
- <AudioPlayerDurationDisplayImpl data-slot="audio-player-duration-display" {...props} />
327
- </Suspense>
328
- </LazyEngineBoundary>
253
+ <MediaPlayerTime data-slot="media-player-time" {...props} mode="duration" />
329
254
  );
330
255
 
331
- export type AudioPlayerMuteButtonProps = AudioPlayerPartProps;
332
-
333
- const AudioPlayerMuteButtonImpl = lazyPart<AudioPlayerMuteButtonProps>(
334
- (m) => m.AudioPlayerMuteButton,
335
- );
256
+ export type AudioPlayerMuteButtonProps = MediaPlayerButtonProps;
336
257
 
337
258
  export const AudioPlayerMuteButton = (props: AudioPlayerMuteButtonProps) => (
338
- <LazyEngineBoundary renderMissing={() => null}>
339
- <Suspense fallback={null}>
340
- <AudioPlayerMuteButtonImpl data-slot="audio-player-mute-button" {...props} />
341
- </Suspense>
342
- </LazyEngineBoundary>
259
+ <MediaPlayerMuteButton data-slot="media-player-mute-button" {...props} />
343
260
  );
344
261
 
345
- export type AudioPlayerVolumeRangeProps = AudioPlayerPartProps;
346
-
347
- const AudioPlayerVolumeRangeImpl = lazyPart<AudioPlayerVolumeRangeProps>(
348
- (m) => m.AudioPlayerVolumeRange,
349
- );
262
+ export type AudioPlayerVolumeRangeProps = MediaPlayerVolumeSliderProps;
350
263
 
351
264
  export const AudioPlayerVolumeRange = (props: AudioPlayerVolumeRangeProps) => (
352
- <LazyEngineBoundary renderMissing={() => null}>
353
- <Suspense fallback={null}>
354
- <AudioPlayerVolumeRangeImpl data-slot="audio-player-volume-range" {...props} />
355
- </Suspense>
356
- </LazyEngineBoundary>
265
+ <MediaPlayerVolumeSlider data-slot="media-player-volume-slider" {...props} />
357
266
  );