@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
package/dist/chunk-NSCLRHES.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import {
|
|
3
|
-
LazyEngineBoundary
|
|
4
|
-
} from "./chunk-S3ISPW3A.js";
|
|
5
|
-
|
|
6
|
-
// src/audio-player.tsx
|
|
7
|
-
import { Skeleton, isOptionalPeerMissing, useLocale } from "@elabs-ai/components-ui";
|
|
8
|
-
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
9
|
-
import { VolumeOffIcon } from "lucide-react";
|
|
10
|
-
import { lazy, Suspense } from "react";
|
|
11
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
-
var lazyPart = (pick) => lazy(() => import("./_audio-player-media-chrome.js").then((m) => ({ default: pick(m) })));
|
|
13
|
-
var AudioPlayerImpl = lazyPart((m) => m.AudioPlayer);
|
|
14
|
-
var AudioPlayerMissing = ({ className, error }) => {
|
|
15
|
-
const { t } = useLocale();
|
|
16
|
-
const isPeerMissing = isOptionalPeerMissing(error);
|
|
17
|
-
return /* @__PURE__ */ jsxs(
|
|
18
|
-
"div",
|
|
19
|
-
{
|
|
20
|
-
className: cn(
|
|
21
|
-
"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",
|
|
22
|
-
className
|
|
23
|
-
),
|
|
24
|
-
role: isPeerMissing ? "status" : "alert",
|
|
25
|
-
children: [
|
|
26
|
-
/* @__PURE__ */ jsx(VolumeOffIcon, { "aria-hidden": "true", className: "size-4 shrink-0" }),
|
|
27
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: isPeerMissing ? t("ai.error.engineMissingBody", {
|
|
28
|
-
feature: t("ai.audioPlayer.feature"),
|
|
29
|
-
packages: "media-chrome"
|
|
30
|
-
}) : t("ai.audioPlayer.renderError") })
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
);
|
|
34
|
-
};
|
|
35
|
-
var AudioPlayer = ({ className, ...props }) => /* @__PURE__ */ jsx(
|
|
36
|
-
LazyEngineBoundary,
|
|
37
|
-
{
|
|
38
|
-
renderMissing: (error) => /* @__PURE__ */ jsx(AudioPlayerMissing, { className, error }),
|
|
39
|
-
children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(Skeleton, { className: cn("h-10 w-full rounded-md", className) }), children: /* @__PURE__ */ jsx(AudioPlayerImpl, { className, "data-slot": "audio-player", ...props }) })
|
|
40
|
-
}
|
|
41
|
-
);
|
|
42
|
-
var AudioPlayerElement = ({ ...props }) => (
|
|
43
|
-
// oxlint-disable-next-line eslint-plugin-jsx-a11y(media-has-caption) -- audio player captions are provided by consumer
|
|
44
|
-
/* @__PURE__ */ jsx(
|
|
45
|
-
"audio",
|
|
46
|
-
{
|
|
47
|
-
"data-slot": "audio-player-element",
|
|
48
|
-
slot: "media",
|
|
49
|
-
src: "src" in props ? props.src : `data:${props.data.mediaType};base64,${props.data.base64}`,
|
|
50
|
-
...props
|
|
51
|
-
}
|
|
52
|
-
)
|
|
53
|
-
);
|
|
54
|
-
var AudioPlayerControlBarImpl = lazyPart(
|
|
55
|
-
(m) => m.AudioPlayerControlBar
|
|
56
|
-
);
|
|
57
|
-
var AudioPlayerControlBar = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerControlBarImpl, { "data-slot": "audio-player-control-bar", ...props }) }) });
|
|
58
|
-
var AudioPlayerPlayButtonImpl = lazyPart(
|
|
59
|
-
(m) => m.AudioPlayerPlayButton
|
|
60
|
-
);
|
|
61
|
-
var AudioPlayerPlayButton = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerPlayButtonImpl, { "data-slot": "audio-player-play-button", ...props }) }) });
|
|
62
|
-
var AudioPlayerSeekBackwardButtonImpl = lazyPart(
|
|
63
|
-
(m) => m.AudioPlayerSeekBackwardButton
|
|
64
|
-
);
|
|
65
|
-
var AudioPlayerSeekBackwardButton = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerSeekBackwardButtonImpl, { "data-slot": "audio-player-seek-backward-button", ...props }) }) });
|
|
66
|
-
var AudioPlayerSeekForwardButtonImpl = lazyPart(
|
|
67
|
-
(m) => m.AudioPlayerSeekForwardButton
|
|
68
|
-
);
|
|
69
|
-
var AudioPlayerSeekForwardButton = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerSeekForwardButtonImpl, { "data-slot": "audio-player-seek-forward-button", ...props }) }) });
|
|
70
|
-
var AudioPlayerTimeDisplayImpl = lazyPart(
|
|
71
|
-
(m) => m.AudioPlayerTimeDisplay
|
|
72
|
-
);
|
|
73
|
-
var AudioPlayerTimeDisplay = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerTimeDisplayImpl, { "data-slot": "audio-player-time-display", ...props }) }) });
|
|
74
|
-
var AudioPlayerTimeRangeImpl = lazyPart((m) => m.AudioPlayerTimeRange);
|
|
75
|
-
var AudioPlayerTimeRange = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerTimeRangeImpl, { "data-slot": "audio-player-time-range", ...props }) }) });
|
|
76
|
-
var AudioPlayerDurationDisplayImpl = lazyPart(
|
|
77
|
-
(m) => m.AudioPlayerDurationDisplay
|
|
78
|
-
);
|
|
79
|
-
var AudioPlayerDurationDisplay = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerDurationDisplayImpl, { "data-slot": "audio-player-duration-display", ...props }) }) });
|
|
80
|
-
var AudioPlayerMuteButtonImpl = lazyPart(
|
|
81
|
-
(m) => m.AudioPlayerMuteButton
|
|
82
|
-
);
|
|
83
|
-
var AudioPlayerMuteButton = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerMuteButtonImpl, { "data-slot": "audio-player-mute-button", ...props }) }) });
|
|
84
|
-
var AudioPlayerVolumeRangeImpl = lazyPart(
|
|
85
|
-
(m) => m.AudioPlayerVolumeRange
|
|
86
|
-
);
|
|
87
|
-
var AudioPlayerVolumeRange = (props) => /* @__PURE__ */ jsx(LazyEngineBoundary, { renderMissing: () => null, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(AudioPlayerVolumeRangeImpl, { "data-slot": "audio-player-volume-range", ...props }) }) });
|
|
88
|
-
|
|
89
|
-
export {
|
|
90
|
-
AudioPlayer,
|
|
91
|
-
AudioPlayerElement,
|
|
92
|
-
AudioPlayerControlBar,
|
|
93
|
-
AudioPlayerPlayButton,
|
|
94
|
-
AudioPlayerSeekBackwardButton,
|
|
95
|
-
AudioPlayerSeekForwardButton,
|
|
96
|
-
AudioPlayerTimeDisplay,
|
|
97
|
-
AudioPlayerTimeRange,
|
|
98
|
-
AudioPlayerDurationDisplay,
|
|
99
|
-
AudioPlayerMuteButton,
|
|
100
|
-
AudioPlayerVolumeRange
|
|
101
|
-
};
|
|
102
|
-
//# sourceMappingURL=chunk-NSCLRHES.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/audio-player.tsx"],"sourcesContent":["\"use client\";\n\nimport { Skeleton, isOptionalPeerMissing, useLocale } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport type { Experimental_SpeechResult as SpeechResult } from \"ai\";\nimport { VolumeOffIcon } from \"lucide-react\";\nimport type { ComponentProps, ComponentType, HTMLAttributes } from \"react\";\nimport { lazy, Suspense } from \"react\";\n\nimport { LazyEngineBoundary } from \"./_lazy-engine-boundary\";\n\n/**\n * media-chrome lives behind a dynamic import — it declares no `sideEffects`, so\n * a static import would put the whole custom-element library in every consumer's\n * entry chunk, `AudioPlayer` rendered or not. Every value import lives in\n * `_audio-player-media-chrome.tsx`. See ADR 0019 and `pnpm heavy-deps:check`.\n *\n * All ten parts resolve from the SAME module specifier, so they share one chunk:\n * once `AudioPlayer` has loaded it, the controls inside it resolve from cache.\n *\n * Issue #101: `media-chrome` is an OPTIONAL peer, so no PUBLIC export's type\n * may structurally reference `media-chrome/react`'s own types (previously\n * `ComponentProps<typeof MediaController>` and nine siblings, imported as\n * TYPE-only above) — doing so names `media-chrome/react`'s module specifier in\n * this package's generated root `.d.ts` and hands a `skipLibCheck: false`\n * consumer who has correctly omitted the peer a `TS2307` just for importing\n * the barrel — not just for using `AudioPlayer`. `AudioPlayerPartProps` below\n * is an OWNED base type: `media-chrome/react`'s wrapper\n * (`media-chrome/react`'s `createComponent`, from the `ce-la-react` helper it\n * is built on) types every part as `Omit<HTMLAttributes<I>, …> &\n * Partial<the element's own instance members>` — i.e. ordinary HTML attributes\n * plus a handful of optional custom attributes. `AudioPlayerPartProps` itself\n * stays ordinary HTML attributes only; the PRIMITIVE-typed (`string` /\n * `boolean` / `number`) extra instance members of `<media-controller>` and\n * `<media-seek-forward-button>` — the two elements measured by the round-2\n * validator — are mirrored one-by-one on `AudioPlayerProps` and\n * `AudioPlayerSeekForwardButtonProps` below (each a single declaration line\n * referencing nothing peer-owned, the same technique `seekOffset` already\n * used). What stays narrowed to `AudioPlayerPartProps` alone are the\n * PEER/COMPLEX-typed members (`MediaStore`, `AttributeTokenList`,\n * `MediaTooltip`, `TooltipPlacement`, DOM-element and `HTMLMediaElement`\n * callbacks, methods) and every member of the other eight parts — restoring\n * those would re-import the peer type graph this fix exists to avoid. See the\n * CHANGELOG's \"Breaking (types)\" entry for the exact narrowed set.\n * `AudioPlayerPartProps` is therefore NOT a full replica of media-chrome's\n * surface, only the base every part actually uses and re-exports (the same\n * scoping `TerminalColorTheme` uses for xterm's `ITheme` in\n * `packages/terminal/src/interactive-terminal.tsx`, also issue #101). The ten\n * conformance assertions at the bottom of `_audio-player-media-chrome.tsx`\n * (which still imports the real peer types — that module is reached only\n * through `lazy()` and never sits in the barrel's declaration graph) prove\n * every owned type here stays assignable to its real media-chrome counterpart;\n * a peer version bump that narrows a prop incompatibly fails\n * `pnpm --filter @elabs-ai/components-ai typecheck` locally instead of\n * reaching a consumer as silent drift.\n *\n * Deliberately no `ref` field: `ComponentProps<typeof MediaController>` (and\n * its nine siblings) included one via `RefAttributes<I>`, but `RefObject<T>`'s\n * mutable `current` makes ref types INVARIANT in `T` — a `Ref<HTMLElement>`\n * can never be assignable to a real element's own `Ref<MediaController>` (the\n * custom element subclass carries private/extra members `HTMLElement` lacks),\n * so keeping it would make every owned type fail its own conformance\n * assertion below. This IS a real-world break for a React 19 consumer: none\n * of the ten parts this package renders is wrapped in `forwardRef`, but\n * `MediaController` (and its siblings) are genuine `forwardRef` components,\n * and React 19's ref-as-prop carries a `ref` straight through an ordinary\n * function component's `{...props}` spread to the real underlying element —\n * see the CHANGELOG's \"Breaking (types)\" entry for the measured detail. It is\n * inert only under React 18.\n */\nexport type AudioPlayerPartProps = HTMLAttributes<HTMLElement>;\n\ntype MediaChromeModule = typeof import(\"./_audio-player-media-chrome\");\n\nconst lazyPart = <P,>(pick: (module: MediaChromeModule) => ComponentType<P>) =>\n lazy(() => import(\"./_audio-player-media-chrome\").then((m) => ({ default: pick(m) })));\n\n/**\n * The primitive-typed (`string`/`boolean`/`number`) extra instance members of\n * `media-chrome/react`'s `MediaController` — restored per issue #101's round-2\n * validation (R2): each is a single declaration line mirroring the real peer\n * type's name and shape, referencing nothing peer-owned, so it costs nothing\n * against the `.d.ts` leak this fix exists to prevent. Deliberately EXCLUDED:\n * `audio` (already narrowed out — `_audio-player-media-chrome.tsx`'s\n * conformance assertion Omits it from the real `ComponentProps`, since this\n * package renders its own `<AudioPlayerElement>` instead of passing one\n * through), and every PEER/COMPLEX-typed member (`mediaStore`, `hotkeys`\n * (`AttributeTokenList`), `media`/`fullscreenElement` (DOM elements),\n * `mediaStateReceivers`, `associatedElementSubscriptions`, every\n * `HTMLMediaElement` callback and every method) — those stay narrowed to\n * `AudioPlayerPartProps` alone; restoring them would re-import the peer type\n * graph. See `_audio-player-media-chrome.tsx`'s\n * `_AudioPlayerPropsConformance` assertion, which fails typecheck if this\n * interface ever drifts wider than the real element's own props.\n *\n * Doc comments below are inferred from each member's name and media-chrome's\n * public attribute-naming convention (`no*` = boolean opt-out of the\n * corresponding default-on behavior) — media-chrome ships no per-property doc\n * comments in its own `.d.ts` or README to copy from. Treat them as a reading\n * aid, not an authoritative spec; verify against media-chrome's source before\n * relying on exact semantics.\n */\nexport interface AudioPlayerProps extends AudioPlayerPartProps {\n /** Seconds of inactivity before controls hide; a string because it mirrors the HTML attribute value. */\n autohide?: string;\n /** Also autohide while the pointer is over the control elements themselves, not just the media. */\n autohideOverControls?: boolean;\n /** Breakpoint definitions (space-separated `name:width` pairs) driving `breakpointsComputed`. */\n breakpoints?: string;\n /** Whether the controller has computed its current breakpoint(s) at least once. */\n breakpointsComputed?: boolean;\n /** Fallback duration (seconds) to display before the real media duration is known. */\n defaultDuration?: number;\n /** Default stream type (\"on-demand\" or \"live\") before the media has resolved its own. */\n defaultStreamType?: string;\n /** Whether subtitles/captions are enabled by default. */\n defaultSubtitles?: boolean;\n /** Disables the controller's built-in tap/gesture handling (e.g. tap-to-play, double-tap-to-seek). */\n gesturesDisabled?: boolean;\n /** Whether hotkey keyboard control is currently active on the controller. */\n keyboardControl?: boolean;\n /** Space-separated list of hotkey names currently in use, for conflict detection with other listeners. */\n keysUsed?: string;\n /** Seconds behind the live edge still considered \"at\" live, for a live stream's seek range. */\n liveEdgeOffset?: number;\n /** Disables the controller's autohide-controls-on-inactivity behavior entirely. */\n noAutohide?: boolean;\n /** Disables automatically seeking back to the live edge after a manual seek on a live stream. */\n noAutoSeekToLive?: boolean;\n /** Opts out of the controller creating its own default internal media store. */\n noDefaultStore?: boolean;\n /** Disables the controller's built-in keyboard hotkeys entirely. */\n noHotkeys?: boolean;\n /** Opts out of remembering the user's mute preference across sessions. */\n noMutedPref?: boolean;\n /** Opts out of remembering the user's subtitles-language preference across sessions. */\n noSubtitlesLangPref?: boolean;\n /** Opts out of remembering the user's volume preference across sessions. */\n noVolumePref?: boolean;\n /** The language the controller resolved for its UI text, after applying any language preference. */\n resolvedLang?: string;\n /** Whether the user has interacted with the controller yet (affects autoplay-adjacent UI). */\n userInteractive?: boolean;\n}\n\nconst AudioPlayerImpl = lazyPart<AudioPlayerProps>((m) => m.AudioPlayer);\n\n/**\n * The settled-failure stand-in for the `LazyEngineBoundary` above — rendered\n * once the `media-chrome` load has genuinely failed, never while it is still\n * pending (that stays `Skeleton`, via the `Suspense fallback` below). A\n * loading skeleton left in place after a settled failure reads as \"still\n * loading, forever\" (loading-states.md) — this is a terminal state, so it\n * gets a real (if compact) notice instead, sized to the same `h-10` slot so\n * nothing shifts when the boundary trips.\n */\nconst AudioPlayerMissing = ({ className, error }: { className?: string; error: unknown }) => {\n const { t } = useLocale();\n const isPeerMissing = isOptionalPeerMissing(error);\n return (\n <div\n className={cn(\n \"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\",\n className,\n )}\n role={isPeerMissing ? \"status\" : \"alert\"}\n >\n <VolumeOffIcon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"truncate\">\n {isPeerMissing\n ? t(\"ai.error.engineMissingBody\", {\n feature: t(\"ai.audioPlayer.feature\"),\n packages: \"media-chrome\",\n })\n : t(\"ai.audioPlayer.renderError\")}\n </span>\n </div>\n );\n};\n\nexport const AudioPlayer = ({ className, ...props }: AudioPlayerProps) => (\n <LazyEngineBoundary\n renderMissing={(error) => <AudioPlayerMissing className={className} error={error} />}\n >\n <Suspense fallback={<Skeleton className={cn(\"h-10 w-full rounded-md\", className)} />}>\n <AudioPlayerImpl className={className} data-slot=\"audio-player\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\nexport type AudioPlayerElementProps = Omit<ComponentProps<\"audio\">, \"src\"> &\n (\n | {\n data: SpeechResult[\"audio\"];\n }\n | {\n src: string;\n }\n );\n\nexport const AudioPlayerElement = ({ ...props }: AudioPlayerElementProps) => (\n // oxlint-disable-next-line eslint-plugin-jsx-a11y(media-has-caption) -- audio player captions are provided by consumer\n <audio\n data-slot=\"audio-player-element\"\n slot=\"media\"\n src={\"src\" in props ? props.src : `data:${props.data.mediaType};base64,${props.data.base64}`}\n {...props}\n />\n);\n\nexport type AudioPlayerControlBarProps = AudioPlayerPartProps;\n\nconst AudioPlayerControlBarImpl = lazyPart<AudioPlayerControlBarProps>(\n (m) => m.AudioPlayerControlBar,\n);\n\nexport const AudioPlayerControlBar = (props: AudioPlayerControlBarProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerControlBarImpl data-slot=\"audio-player-control-bar\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\nexport type AudioPlayerPlayButtonProps = AudioPlayerPartProps;\n\nconst AudioPlayerPlayButtonImpl = lazyPart<AudioPlayerPlayButtonProps>(\n (m) => m.AudioPlayerPlayButton,\n);\n\nexport const AudioPlayerPlayButton = (props: AudioPlayerPlayButtonProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerPlayButtonImpl data-slot=\"audio-player-play-button\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\nexport interface AudioPlayerSeekBackwardButtonProps extends AudioPlayerPartProps {\n /** Skip distance in seconds. Mirrors the element's own default (10). */\n seekOffset?: number;\n}\n\nconst AudioPlayerSeekBackwardButtonImpl = lazyPart<AudioPlayerSeekBackwardButtonProps>(\n (m) => m.AudioPlayerSeekBackwardButton,\n);\n\nexport const AudioPlayerSeekBackwardButton = (props: AudioPlayerSeekBackwardButtonProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerSeekBackwardButtonImpl data-slot=\"audio-player-seek-backward-button\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\n/**\n * Primitive-typed extra members restored per issue #101's round-2 validation\n * (R2) — same technique and same inferred-from-naming caveat as\n * `AudioPlayerProps` above. Deliberately still narrowed out:\n * `keysUsed` (here typed `string[]`, unlike the controller's own `string`\n * member of the same name — an array is not primitive) and the DOM/method\n * members `disable`/`enable`/`handleClick`/`tooltipEl`/`tooltipPlacement`.\n */\nexport interface AudioPlayerSeekForwardButtonProps extends AudioPlayerPartProps {\n /** Whether the button is disabled. */\n disabled?: boolean;\n /** ID of the `<media-controller>` element this button controls, when it is not an ancestor. */\n mediaController?: string;\n /** The media's current playback time (seconds), as reflected by the controller's media store. */\n mediaCurrentTime?: number;\n /** Skip distance in seconds. Mirrors the element's own default (10). */\n seekOffset?: number;\n /** Suppresses the built-in hover tooltip on this button. */\n noTooltip?: boolean;\n /** Prevents the button's default click handling (for a consumer that wants to fully own the behavior). */\n preventClick?: boolean;\n}\n\nconst AudioPlayerSeekForwardButtonImpl = lazyPart<AudioPlayerSeekForwardButtonProps>(\n (m) => m.AudioPlayerSeekForwardButton,\n);\n\nexport const AudioPlayerSeekForwardButton = (props: AudioPlayerSeekForwardButtonProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerSeekForwardButtonImpl data-slot=\"audio-player-seek-forward-button\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\nexport type AudioPlayerTimeDisplayProps = AudioPlayerPartProps;\n\nconst AudioPlayerTimeDisplayImpl = lazyPart<AudioPlayerTimeDisplayProps>(\n (m) => m.AudioPlayerTimeDisplay,\n);\n\nexport const AudioPlayerTimeDisplay = (props: AudioPlayerTimeDisplayProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerTimeDisplayImpl data-slot=\"audio-player-time-display\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\nexport type AudioPlayerTimeRangeProps = AudioPlayerPartProps;\n\nconst AudioPlayerTimeRangeImpl = lazyPart<AudioPlayerTimeRangeProps>((m) => m.AudioPlayerTimeRange);\n\nexport const AudioPlayerTimeRange = (props: AudioPlayerTimeRangeProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerTimeRangeImpl data-slot=\"audio-player-time-range\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\nexport type AudioPlayerDurationDisplayProps = AudioPlayerPartProps;\n\nconst AudioPlayerDurationDisplayImpl = lazyPart<AudioPlayerDurationDisplayProps>(\n (m) => m.AudioPlayerDurationDisplay,\n);\n\nexport const AudioPlayerDurationDisplay = (props: AudioPlayerDurationDisplayProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerDurationDisplayImpl data-slot=\"audio-player-duration-display\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\nexport type AudioPlayerMuteButtonProps = AudioPlayerPartProps;\n\nconst AudioPlayerMuteButtonImpl = lazyPart<AudioPlayerMuteButtonProps>(\n (m) => m.AudioPlayerMuteButton,\n);\n\nexport const AudioPlayerMuteButton = (props: AudioPlayerMuteButtonProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerMuteButtonImpl data-slot=\"audio-player-mute-button\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n\nexport type AudioPlayerVolumeRangeProps = AudioPlayerPartProps;\n\nconst AudioPlayerVolumeRangeImpl = lazyPart<AudioPlayerVolumeRangeProps>(\n (m) => m.AudioPlayerVolumeRange,\n);\n\nexport const AudioPlayerVolumeRange = (props: AudioPlayerVolumeRangeProps) => (\n <LazyEngineBoundary renderMissing={() => null}>\n <Suspense fallback={null}>\n <AudioPlayerVolumeRangeImpl data-slot=\"audio-player-volume-range\" {...props} />\n </Suspense>\n </LazyEngineBoundary>\n);\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tool.tsx"],"sourcesContent":["\"use client\";\n\nimport { Skeleton, StatusBadge, useLocale, type Status } from \"@elabs-ai/components-ui\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport type { DynamicToolUIPart, ToolUIPart } from \"ai\";\nimport { ChevronDownIcon, WrenchIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { isValidElement } from \"react\";\nimport type { BundledLanguage } from \"shiki\";\n\nimport { CodeBlock } from \"./code-block\";\n\nexport type ToolProps = ComponentProps<typeof Collapsible>;\n\nexport const Tool = ({ className, ...props }: ToolProps) => (\n <Collapsible\n className={cn(\"group not-prose mb-4 w-full rounded-md border\", className)}\n {...props}\n />\n);\n\nexport type ToolPart = ToolUIPart | DynamicToolUIPart;\n\n/**\n * `JSON.stringify` throws on a circular reference or a `BigInt` — both\n * realistic shapes for tool input/output payloads a model produced. Falls\n * back to a readable placeholder instead of crashing the message render.\n */\nfunction safeJsonStringify(value: unknown): string {\n try {\n return JSON.stringify(value, (_key, v) => (typeof v === \"bigint\" ? `${v.toString()}n` : v), 2);\n } catch {\n return String(value);\n }\n}\n\nexport type ToolHeaderProps = {\n title?: string;\n /**\n * The business summary line (\"3 documents found\", \"8 rows reconciled\"),\n * shown beside the name + StatusBadge (#192, research 10 §B.5). Falls back\n * to nothing — the derived tool name still labels the row.\n */\n summary?: ReactNode;\n className?: string;\n} & (\n | { type: ToolUIPart[\"type\"]; state: ToolUIPart[\"state\"]; toolName?: never }\n | {\n type: DynamicToolUIPart[\"type\"];\n state: DynamicToolUIPart[\"state\"];\n toolName: string;\n }\n);\n\n/**\n * Map the AI-SDK `ToolUIPart` 7-state machine onto the canonical `Status`\n * enum (#189, research 10 §B.1 mapping a). Lives here — not in `@elabs-ai/components-ui` —\n * because it is typed against the SDK union; the `ai` import stays TYPES-ONLY\n * (D6, gate-enforced by `pnpm ai:types-only`).\n */\nexport const statusFromToolState = (state: ToolPart[\"state\"]): Status => {\n switch (state) {\n case \"input-streaming\":\n return \"pending\";\n case \"input-available\":\n return \"running\";\n case \"approval-requested\":\n return \"awaiting-approval\";\n case \"approval-responded\":\n return \"running\";\n case \"output-available\":\n return \"complete\";\n case \"output-denied\":\n return \"denied\";\n case \"output-error\":\n return \"failed\";\n }\n};\n\n/** Same signature as before #189; renders the canonical StatusBadge. */\nexport const getStatusBadge = (status: ToolPart[\"state\"]) => (\n <StatusBadge status={statusFromToolState(status)} />\n);\n\nexport const ToolHeader = ({\n className,\n title,\n summary,\n type,\n state,\n toolName,\n ...props\n}: ToolHeaderProps) => {\n const derivedName = type === \"dynamic-tool\" ? toolName : type.split(\"-\").slice(1).join(\"-\");\n\n return (\n <CollapsibleTrigger\n className={cn(\"flex w-full items-center justify-between gap-4 p-3\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 items-center gap-2\">\n <WrenchIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"shrink-0 text-body font-medium\">{title ?? derivedName}</span>\n {getStatusBadge(state)}\n {summary ? (\n <span className=\"truncate text-meta text-muted-foreground\">{summary}</span>\n ) : null}\n </div>\n <ChevronDownIcon className=\"size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180\" />\n </CollapsibleTrigger>\n );\n};\n\nexport type ToolContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const ToolContent = ({ className, ...props }: ToolContentProps) => (\n <CollapsibleContent\n className={cn(\n \"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=open]:[--tw-ease:var(--ease-entrance)] data-[state=closed]:[--tw-ease:var(--ease-exit)]\",\n className,\n )}\n {...props}\n />\n);\n\nexport type ToolDetailsProps = ComponentProps<typeof Collapsible> & {\n /** The disclosure label. */\n label?: ReactNode;\n};\n\n/**\n * The technical view, behind disclosure — the PACKAGE DEFAULT for tool JSON\n * (#192, research 10 §B.5): a nested collapsible, COLLAPSED by default,\n * holding `ToolInput`/`ToolOutput`. The header carries the business `summary`;\n * the raw payload is one expand away, never the headline.\n */\nexport const ToolDetails = ({\n className,\n label,\n defaultOpen = false,\n children,\n ...props\n}: ToolDetailsProps) => {\n const { t } = useLocale();\n return (\n <Collapsible\n className={cn(\"group/tool-details not-prose\", className)}\n defaultOpen={defaultOpen}\n {...props}\n >\n <CollapsibleTrigger className=\"flex items-center gap-1 rounded-sm text-meta text-muted-foreground transition-colors hover:text-foreground focus-ring\">\n <ChevronDownIcon className=\"size-3.5 transition-transform group-data-[state=open]/tool-details:rotate-180\" />\n {label ?? t(\"ai.tool.showTechnicalDetails\")}\n </CollapsibleTrigger>\n <CollapsibleContent className=\"mt-3 space-y-4\">{children}</CollapsibleContent>\n </Collapsible>\n );\n};\n\nexport type ToolInputProps = ComponentProps<\"div\"> & {\n input: ToolPart[\"input\"];\n};\n\nexport const ToolInput = ({ className, input, ...props }: ToolInputProps) => {\n const { t } = useLocale();\n return (\n <div className={cn(\"space-y-2 overflow-hidden\", className)} {...props}>\n <h4 className=\"text-eyebrow uppercase text-muted-foreground\">\n {t(\"ai.schemaDisplay.parameters\")}\n </h4>\n <div className=\"rounded-md bg-muted/50\">\n <CodeBlock code={safeJsonStringify(input)} language=\"json\" />\n </div>\n </div>\n );\n};\n\nexport type ToolOutputProps = ComponentProps<\"div\"> & {\n /**\n * The tool result. Objects/strings render as JSON inside the technical view.\n *\n * @deprecated Passing a pre-rendered **React element** here (the \"rich\n * output under a muted Result heading\" path) is deprecated since #192\n * (research 10 §B.5): a produced artifact is the HEADLINE, not a technical\n * detail — host it in a `<ToolResultCard>` and keep `ToolOutput` for the\n * JSON payload behind `<ToolDetails>`. The element path still renders for\n * existing consumers (e.g. the copy-owned `ai-chart` registry block) but\n * will be removed in a future release.\n */\n output: ToolPart[\"output\"];\n errorText: ToolPart[\"errorText\"];\n /**\n * The call has not produced output yet (loading-states.md `isStreaming`) —\n * derive it from the existing `statusFromToolState(state)` mapping\n * (`\"input-streaming\"`/`\"input-available\"` → not yet `\"complete\"`/`\"failed\"`)\n * rather than a second source of truth. While true and no `output`/\n * `errorText` has arrived, renders a layout-shaped skeleton in the Result\n * slot instead of `null`, so the technical view reserves its space. A\n * still-running call is never a terminal failure — the error branch is\n * suppressed while `isStreaming` per the loading-states.md error rule, even\n * if a stale `errorText` is still set from a previous render.\n * @default false\n */\n isStreaming?: boolean;\n};\n\nexport const ToolOutput = ({\n className,\n output,\n errorText,\n isStreaming = false,\n ...props\n}: ToolOutputProps) => {\n const { t } = useLocale();\n\n // `output` is a defined-but-falsy result (`0`, `false`, `\"\"`) for plenty of\n // real tools (a count, a boolean check, an empty-string field) — only\n // `undefined` means \"no output (yet)\".\n const hasOutput = output !== undefined;\n\n if (!(hasOutput || errorText || isStreaming)) {\n return null;\n }\n\n const showError = !isStreaming && Boolean(errorText);\n const pending = isStreaming && !hasOutput && !errorText;\n\n let Output: ReactNode = null;\n\n if (hasOutput) {\n if (isValidElement(output)) {\n Output = output;\n } else if (typeof output === \"string\") {\n // A tool result is arbitrary text, not guaranteed JSON — forcing the\n // JSON highlighter on it mis-colours ordinary strings. Shiki's own\n // `BundledLanguage` union (grammar-backed languages) omits its\n // hard-coded plain-text pseudo-languages (`isPlainLang`:\n // \"plaintext\" | \"txt\" | \"text\" | \"plain\") — `createHighlighter`/\n // `codeToTokens` accept and special-case them with no grammar load\n // (verified: `getLoadedLanguages()` stays empty, no throw), so this\n // is a type-only gap, not a runtime one.\n Output = <CodeBlock code={output} language={\"text\" as BundledLanguage} />;\n } else {\n // Objects, arrays, numbers, booleans, null, bigint — all safe to\n // stringify for display.\n Output = <CodeBlock code={safeJsonStringify(output)} language=\"json\" />;\n }\n }\n\n return (\n <div className={cn(\"space-y-2\", className)} {...props}>\n <h4 className=\"text-eyebrow uppercase text-muted-foreground\">\n {showError ? t(\"ai.tool.error\") : t(\"ai.tool.result\")}\n </h4>\n {pending ? (\n <div className=\"space-y-2 rounded-md bg-muted/50 p-3\" role=\"status\" aria-live=\"polite\">\n <span className=\"sr-only\">{t(\"loading\")}</span>\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n ) : (\n <div\n className={cn(\n \"overflow-x-auto rounded-md text-caption [&_table]:w-full\",\n // #124: this colours the ambient text of the error/result body —\n // running text, so the error branch takes the ink rung.\n showError ? \"bg-destructive/10 text-destructive-text\" : \"bg-muted/50 text-foreground\",\n )}\n >\n {showError && <div>{errorText}</div>}\n {Output}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/asset-preview.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * AssetPreview — type-keyed preview of one produced asset (#193, research 04\n * §5 ASSET-4). Reuses the `Artifact` chrome for the header/actions; the body\n * switches on `ContextAsset.type`:\n *\n * markdown → `MarkdownView` (the branded renderer — a document, never\n * Shiki source; the ASSET-2 fix)\n * code/sql → `CodeBlock` (Shiki)\n * csv → a small token-styled table + row-count summary\n * image → an image preview\n *\n * A Preview / Raw toggle serves users who want the source (Raw = `CodeBlock`,\n * soft-wrapped for narrow embeds). Selection state lives in the\n * `ContextPanelProvider` — the tree and this preview are the two levels of the\n * drill-in.\n *\n * Formats beyond that switch — a PDF, a spreadsheet, a video — arrive through\n * `renderPreview` (prop, or injected once on `ContextPanelProvider`), because\n * `@elabs-ai/components-viewer` is a layer PEER of this package and\n * neither may import the other (ADR 0024 §6). A renderer that returns `null`\n * declines, and everything below runs exactly as before.\n */\nimport {\n Button,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport type { BundledLanguage } from \"shiki\";\nimport {\n Artifact,\n ArtifactActions,\n ArtifactContent,\n ArtifactHeader,\n ArtifactTitle,\n} from \"./artifact\";\nimport { CodeBlock } from \"./code-block\";\nimport type { AssetPreviewRenderer, ContextAsset } from \"./context-panel\";\nimport { useAssetPreviewRenderer } from \"./context-panel\";\nimport { MarkdownView } from \"./markdown-view\";\n\nexport type AssetPreviewMode = \"preview\" | \"raw\";\n\nconst DEFAULT_CODE_LANGUAGE: BundledLanguage = \"typescript\";\n\n/** Raw-mode (and code-preview) Shiki language for an asset. */\nfunction assetLanguage(asset: ContextAsset): BundledLanguage {\n switch (asset.type) {\n case \"markdown\":\n return \"markdown\";\n case \"sql\":\n return \"sql\";\n case \"csv\":\n return \"csv\";\n case \"code\":\n return (asset.language as BundledLanguage) ?? DEFAULT_CODE_LANGUAGE;\n default:\n return DEFAULT_CODE_LANGUAGE;\n }\n}\n\ninterface ParsedCsv {\n header: string[];\n rows: string[][];\n}\n\n/**\n * A small RFC 4180-ish CSV tokenizer: handles quoted fields (commas and\n * newlines inside quotes don't end the field/row) and the `\"\"` escaped-quote\n * convention. Single pass over the raw string — no line pre-split, so a\n * quoted field spanning multiple lines survives intact. Still preview-grade\n * (unquoted cells are trimmed for the glance table), not a full CSV grammar.\n */\nfunction parseCsvRows(content: string): string[][] {\n const rows: string[][] = [];\n let row: string[] = [];\n let field = \"\";\n let wasQuoted = false;\n let inQuotes = false;\n\n const endField = () => {\n row.push(wasQuoted ? field : field.trim());\n field = \"\";\n wasQuoted = false;\n };\n const endRow = () => {\n endField();\n rows.push(row);\n row = [];\n };\n\n for (let i = 0; i < content.length; i++) {\n const ch = content[i];\n\n if (inQuotes) {\n if (ch === '\"') {\n if (content[i + 1] === '\"') {\n field += '\"';\n i += 1;\n } else {\n inQuotes = false;\n }\n } else {\n field += ch;\n }\n continue;\n }\n\n if (ch === '\"' && field === \"\") {\n inQuotes = true;\n wasQuoted = true;\n continue;\n }\n\n if (ch === \",\") {\n endField();\n continue;\n }\n\n if (ch === \"\\r\") {\n continue;\n }\n\n if (ch === \"\\n\") {\n endRow();\n continue;\n }\n\n field += ch;\n }\n\n // A trailing field/row with no terminating newline.\n if (field !== \"\" || row.length > 0) {\n endRow();\n }\n\n return rows;\n}\n\nfunction parseCsv(content: string): ParsedCsv {\n const rows = parseCsvRows(content.trim()).filter((r) => !(r.length === 1 && r[0] === \"\"));\n const [header = [], ...rest] = rows;\n return { header, rows: rest };\n}\n\nconst CsvPreview = ({ content }: { content: string }) => {\n const { t } = useLocale();\n const { header, rows } = useMemo(() => parseCsv(content), [content]);\n return (\n <div className=\"flex flex-col gap-2\">\n <Table className=\"text-body tabular-nums\">\n <TableHeader>\n <TableRow>\n {header.map((cell) => (\n <TableHead key={cell}>{cell}</TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((row, rowIndex) => (\n // Rows have no stable identity in raw CSV — index is the key.\n // oxlint-disable-next-line eslint-plugin-react(no-array-index-key)\n <TableRow key={`row-${rowIndex}`}>\n {row.map((cell, cellIndex) => (\n // oxlint-disable-next-line eslint-plugin-react(no-array-index-key)\n <TableCell key={`cell-${rowIndex}-${cellIndex}`}>{cell}</TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n <p className=\"text-meta text-muted-foreground\">\n {t(\"ai.assetPreview.rowCount\", { count: rows.length })}\n </p>\n </div>\n );\n};\n\nexport interface AssetPreviewProps extends HTMLAttributes<HTMLDivElement> {\n asset: ContextAsset;\n /** Initial mode. Default `\"preview\"`. */\n defaultMode?: AssetPreviewMode;\n /**\n * Draw this asset's preview body yourself; return `null` to decline and get\n * the built-in rendering. Wins over a renderer injected on\n * `ContextPanelProvider`, so one rail-wide default can still be overridden\n * for a single preview.\n */\n renderPreview?: AssetPreviewRenderer;\n}\n\nexport const AssetPreview = ({\n asset,\n defaultMode = \"preview\",\n renderPreview,\n className,\n ...props\n}: AssetPreviewProps) => {\n const { t } = useLocale();\n const [mode, setMode] = useState<AssetPreviewMode>(defaultMode);\n const injectedRenderer = useAssetPreviewRenderer();\n const content = asset.content ?? \"\";\n // Raw source only exists for text assets with content.\n const hasRaw = asset.type !== \"image\" && content.length > 0;\n const showRaw = mode === \"raw\" && hasRaw;\n\n // Raw is \"show me the source\", so an injection never intercepts it.\n const render = renderPreview ?? injectedRenderer;\n const injected = !showRaw && render ? render(asset) : null;\n\n let body: ReactNode;\n if (injected !== null) {\n body = injected;\n } else if (asset.type !== \"image\" && content.length === 0) {\n body = <p className=\"text-body text-muted-foreground\">{t(\"ai.assetPreview.noPreview\")}</p>;\n } else if (showRaw) {\n body = <CodeBlock code={content} language={assetLanguage(asset)} wrap />;\n } else {\n switch (asset.type) {\n case \"markdown\": {\n // Constrained heading rung inside the narrow rail (research 09 §G.2).\n body = <MarkdownView baseHeadingLevel={2}>{content}</MarkdownView>;\n break;\n }\n case \"code\":\n case \"sql\": {\n body = <CodeBlock code={content} language={assetLanguage(asset)} showLineNumbers wrap />;\n break;\n }\n case \"csv\": {\n body = <CsvPreview content={content} />;\n break;\n }\n case \"image\": {\n body = (\n <img\n src={content || asset.path}\n alt={asset.name}\n loading=\"lazy\"\n className=\"max-w-full rounded-md\"\n />\n );\n break;\n }\n }\n }\n\n return (\n <Artifact data-slot=\"asset-preview\" className={className} {...props}>\n <ArtifactHeader>\n <ArtifactTitle className=\"min-w-0 truncate\">{asset.name}</ArtifactTitle>\n {hasRaw ? (\n <ArtifactActions>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={!showRaw}\n className={cn(!showRaw && \"bg-accent text-accent-foreground\")}\n onClick={() => setMode(\"preview\")}\n >\n {t(\"ai.assetPreview.preview\")}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={showRaw}\n className={cn(showRaw && \"bg-accent text-accent-foreground\")}\n onClick={() => setMode(\"raw\")}\n >\n {t(\"ai.assetPreview.raw\")}\n </Button>\n </ArtifactActions>\n ) : null}\n </ArtifactHeader>\n <ArtifactContent>{body}</ArtifactContent>\n </Artifact>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,UAAU;AAEnB,SAAS,SAAS,gBAAgB;AA2H5B,SAIQ,KAJR;AA3GN,IAAM,wBAAyC;AAG/C,SAAS,cAAc,OAAsC;AAC3D,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAQ,MAAM,YAAgC;AAAA,IAChD;AACE,aAAO;AAAA,EACX;AACF;AAcA,SAAS,aAAa,SAA6B;AACjD,QAAM,OAAmB,CAAC;AAC1B,MAAI,MAAgB,CAAC;AACrB,MAAI,QAAQ;AACZ,MAAI,YAAY;AAChB,MAAI,WAAW;AAEf,QAAM,WAAW,MAAM;AACrB,QAAI,KAAK,YAAY,QAAQ,MAAM,KAAK,CAAC;AACzC,YAAQ;AACR,gBAAY;AAAA,EACd;AACA,QAAM,SAAS,MAAM;AACnB,aAAS;AACT,SAAK,KAAK,GAAG;AACb,UAAM,CAAC;AAAA,EACT;AAEA,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,KAAK,QAAQ,CAAC;AAEpB,QAAI,UAAU;AACZ,UAAI,OAAO,KAAK;AACd,YAAI,QAAQ,IAAI,CAAC,MAAM,KAAK;AAC1B,mBAAS;AACT,eAAK;AAAA,QACP,OAAO;AACL,qBAAW;AAAA,QACb;AAAA,MACF,OAAO;AACL,iBAAS;AAAA,MACX;AACA;AAAA,IACF;AAEA,QAAI,OAAO,OAAO,UAAU,IAAI;AAC9B,iBAAW;AACX,kBAAY;AACZ;AAAA,IACF;AAEA,QAAI,OAAO,KAAK;AACd,eAAS;AACT;AAAA,IACF;AAEA,QAAI,OAAO,MAAM;AACf;AAAA,IACF;AAEA,QAAI,OAAO,MAAM;AACf,aAAO;AACP;AAAA,IACF;AAEA,aAAS;AAAA,EACX;AAGA,MAAI,UAAU,MAAM,IAAI,SAAS,GAAG;AAClC,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,SAA4B;AAC5C,QAAM,OAAO,aAAa,QAAQ,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,WAAW,KAAK,EAAE,CAAC,MAAM,GAAG;AACxF,QAAM,CAAC,SAAS,CAAC,GAAG,GAAG,IAAI,IAAI;AAC/B,SAAO,EAAE,QAAQ,MAAM,KAAK;AAC9B;AAEA,IAAM,aAAa,CAAC,EAAE,QAAQ,MAA2B;AACvD,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,EAAE,QAAQ,KAAK,IAAI,QAAQ,MAAM,SAAS,OAAO,GAAG,CAAC,OAAO,CAAC;AACnE,SACE,qBAAC,SAAI,WAAU,uBACb;AAAA,yBAAC,SAAM,WAAU,0BACf;AAAA,0BAAC,eACC,8BAAC,YACE,iBAAO,IAAI,CAAC,SACX,oBAAC,aAAsB,kBAAP,IAAY,CAC7B,GACH,GACF;AAAA,MACA,oBAAC,aACE,eAAK,IAAI,CAAC,KAAK;AAAA;AAAA;AAAA,QAGd,oBAAC,YACE,cAAI,IAAI,CAAC,MAAM;AAAA;AAAA,UAEd,oBAAC,aAAiD,kBAAlC,QAAQ,QAAQ,IAAI,SAAS,EAAU;AAAA,SACxD,KAJY,OAAO,QAAQ,EAK9B;AAAA,OACD,GACH;AAAA,OACF;AAAA,IACA,oBAAC,OAAE,WAAU,mCACV,YAAE,4BAA4B,EAAE,OAAO,KAAK,OAAO,CAAC,GACvD;AAAA,KACF;AAEJ;AAeO,IAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAAyB;AACvB,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,CAAC,MAAM,OAAO,IAAI,SAA2B,WAAW;AAC9D,QAAM,mBAAmB,wBAAwB;AACjD,QAAM,UAAU,MAAM,WAAW;AAEjC,QAAM,SAAS,MAAM,SAAS,WAAW,QAAQ,SAAS;AAC1D,QAAM,UAAU,SAAS,SAAS;AAGlC,QAAM,SAAS,iBAAiB;AAChC,QAAM,WAAW,CAAC,WAAW,SAAS,OAAO,KAAK,IAAI;AAEtD,MAAI;AACJ,MAAI,aAAa,MAAM;AACrB,WAAO;AAAA,EACT,WAAW,MAAM,SAAS,WAAW,QAAQ,WAAW,GAAG;AACzD,WAAO,oBAAC,OAAE,WAAU,mCAAmC,YAAE,2BAA2B,GAAE;AAAA,EACxF,WAAW,SAAS;AAClB,WAAO,oBAAC,aAAU,MAAM,SAAS,UAAU,cAAc,KAAK,GAAG,MAAI,MAAC;AAAA,EACxE,OAAO;AACL,YAAQ,MAAM,MAAM;AAAA,MAClB,KAAK,YAAY;AAEf,eAAO,oBAAC,gBAAa,kBAAkB,GAAI,mBAAQ;AACnD;AAAA,MACF;AAAA,MACA,KAAK;AAAA,MACL,KAAK,OAAO;AACV,eAAO,oBAAC,aAAU,MAAM,SAAS,UAAU,cAAc,KAAK,GAAG,iBAAe,MAAC,MAAI,MAAC;AACtF;AAAA,MACF;AAAA,MACA,KAAK,OAAO;AACV,eAAO,oBAAC,cAAW,SAAkB;AACrC;AAAA,MACF;AAAA,MACA,KAAK,SAAS;AACZ,eACE;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,WAAW,MAAM;AAAA,YACtB,KAAK,MAAM;AAAA,YACX,SAAQ;AAAA,YACR,WAAU;AAAA;AAAA,QACZ;AAEF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SACE,qBAAC,YAAS,aAAU,iBAAgB,WAAuB,GAAG,OAC5D;AAAA,yBAAC,kBACC;AAAA,0BAAC,iBAAc,WAAU,oBAAoB,gBAAM,MAAK;AAAA,MACvD,SACC,qBAAC,mBACC;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,gBAAc,CAAC;AAAA,YACf,WAAW,GAAG,CAAC,WAAW,kCAAkC;AAAA,YAC5D,SAAS,MAAM,QAAQ,SAAS;AAAA,YAE/B,YAAE,yBAAyB;AAAA;AAAA,QAC9B;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,WAAW,GAAG,WAAW,kCAAkC;AAAA,YAC3D,SAAS,MAAM,QAAQ,KAAK;AAAA,YAE3B,YAAE,qBAAqB;AAAA;AAAA,QAC1B;AAAA,SACF,IACE;AAAA,OACN;AAAA,IACA,oBAAC,mBAAiB,gBAAK;AAAA,KACzB;AAEJ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_lazy-engine-boundary.tsx"],"sourcesContent":["\"use client\";\n\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\n\nexport interface LazyEngineBoundaryProps {\n children: ReactNode;\n /**\n * Called once a wrapped `lazy()` import rejects (an optional peer that is\n * not installed, or any other load-time failure). Return what to render in\n * its place — the boundary owns no default visual, since the right\n * stand-in differs by surface (an orb placeholder for `Persona`, `null`\n * for an `AudioPlayer` sub-control).\n */\n renderMissing: (error: unknown) => ReactNode;\n}\n\ninterface LazyEngineBoundaryState {\n // Wrapped in an object so `{ value: undefined }` (a boundary that caught\n // an error whose value happens to be `undefined`) is distinguishable from\n // \"nothing caught yet\" (`error: null`).\n error: { value: unknown } | null;\n}\n\n/**\n * Catches a `React.lazy()` load failure — an optional peer dependency that is\n * not installed (issue #33), or any other dynamic-import rejection — and\n * renders a caller-supplied fallback instead of unmounting the tree.\n *\n * `Suspense` alone does not do this: it only covers the PENDING state. A\n * REJECTED lazy import throws during render, and with no boundary above it\n * that throw propagates to the nearest ancestor boundary React DOES find —\n * by default, the whole app. This is the one place in `@elabs-ai/components-ai`\n * that needs a real render-phase error boundary; Mermaid and the interactive\n * terminal fail via an awaited promise instead (Streamdown's `errorComponent`\n * and a `.catch()` on the mount effect, respectively), which don't need one.\n */\nexport class LazyEngineBoundary extends Component<\n LazyEngineBoundaryProps,\n LazyEngineBoundaryState\n> {\n override state: LazyEngineBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: unknown): LazyEngineBoundaryState {\n return { error: { value: error } };\n }\n\n override componentDidCatch(error: unknown, info: ErrorInfo): void {\n console.error(\n \"[@elabs-ai/components-ai] a lazy engine failed to load:\",\n error,\n info.componentStack,\n );\n }\n\n override render(): ReactNode {\n if (this.state.error) {\n return this.props.renderMissing(this.state.error.value);\n }\n return this.props.children;\n }\n}\n"],"mappings":";;;AAEA,SAAS,iBAAiD;AAkCnD,IAAM,qBAAN,cAAiC,UAGtC;AAAA,EACS,QAAiC,EAAE,OAAO,KAAK;AAAA,EAExD,OAAO,yBAAyB,OAAyC;AACvE,WAAO,EAAE,OAAO,EAAE,OAAO,MAAM,EAAE;AAAA,EACnC;AAAA,EAES,kBAAkB,OAAgB,MAAuB;AAChE,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAES,SAAoB;AAC3B,QAAI,KAAK,MAAM,OAAO;AACpB,aAAO,KAAK,MAAM,cAAc,KAAK,MAAM,MAAM,KAAK;AAAA,IACxD;AACA,WAAO,KAAK,MAAM;AAAA,EACpB;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/code-block.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button, useCopyToClipboard, useLocale } from \"@elabs-ai/components-ui\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, HTMLAttributes } from \"react\";\nimport { Shimmer } from \"./shimmer\";\nimport {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n useState,\n} from \"react\";\nimport type { BundledLanguage, BundledTheme, HighlighterGeneric, ThemedToken } from \"shiki\";\nimport { createHighlighter } from \"shiki\";\n\nimport { buildCodeBlockTheme, codeBlockThemeId, getThemeScopeKey } from \"./_code-block-theme\";\nimport { getThemeScope, useThemeScopeRevision } from \"./_theme-scope-store\";\n\n// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline\n// oxlint-disable-next-line eslint(no-bitwise)\nconst isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;\n// oxlint-disable-next-line eslint(no-bitwise)\nconst isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;\nconst isUnderline = (fontStyle: number | undefined) =>\n // oxlint-disable-next-line eslint(no-bitwise)\n fontStyle && fontStyle & 4;\n\n// Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint\ninterface KeyedToken {\n token: ThemedToken;\n key: string;\n}\ninterface KeyedLine {\n tokens: KeyedToken[];\n key: string;\n}\n\nconst addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>\n lines.map((line, lineIdx) => ({\n key: `line-${lineIdx}`,\n tokens: line.map((token, tokenIdx) => ({\n key: `line-${lineIdx}-${tokenIdx}`,\n token,\n })),\n }));\n\n// Token rendering component. Colors come entirely from the derived brand\n// theme's inline styles (`_code-block-theme.ts`, #315) — no `dark:` escape\n// hatch needed, since the theme is already resolved for whichever brand\n// theme is active (including a monochrome ink palette).\nconst TokenSpan = ({ token }: { token: ThemedToken }) => (\n <span\n style={\n {\n backgroundColor: token.bgColor,\n color: token.color,\n fontStyle: isItalic(token.fontStyle) ? \"italic\" : undefined,\n fontWeight: isBold(token.fontStyle) ? \"bold\" : undefined,\n textDecoration: isUnderline(token.fontStyle) ? \"underline\" : undefined,\n ...token.htmlStyle,\n } as CSSProperties\n }\n >\n {token.content}\n </span>\n);\n\n// Line number styles using CSS counters\nconst LINE_NUMBER_CLASSES = cn(\n \"block\",\n \"before:content-[counter(line)]\",\n \"before:inline-block\",\n \"before:[counter-increment:line]\",\n \"before:w-8\",\n \"before:me-4\",\n \"before:text-end\",\n \"before:text-muted-foreground/50\",\n \"before:font-mono\",\n \"before:select-none\",\n);\n\n// Line rendering component\nconst LineSpan = ({\n keyedLine,\n showLineNumbers,\n}: {\n keyedLine: KeyedLine;\n showLineNumbers: boolean;\n}) => (\n <span className={showLineNumbers ? LINE_NUMBER_CLASSES : \"block\"}>\n {keyedLine.tokens.length === 0\n ? \"\\n\"\n : keyedLine.tokens.map(({ token, key }) => <TokenSpan key={key} token={token} />)}\n </span>\n);\n\n// Types\nexport type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {\n code: string;\n language: BundledLanguage;\n showLineNumbers?: boolean;\n /**\n * Soft-wrap long lines instead of scrolling horizontally. Use in narrow embeds\n * (e.g. a side rail) so content is not silently clipped. Default: false.\n */\n wrap?: boolean;\n /**\n * code is arriving incrementally (loading-states.md isStreaming) — code\n * keeps rendering progressively (build-up), NOT a skeleton, since a\n * streaming code block is not fetch-then-show. Shows a Shimmer generating\n * cue below the content as the in-progress affordance.\n * @default false\n */\n isStreaming?: boolean;\n};\n\ninterface TokenizedCode {\n tokens: ThemedToken[][];\n fg: string;\n bg: string;\n}\n\ninterface CodeBlockContextType {\n code: string;\n}\n\n// Context\nconst CodeBlockContext = createContext<CodeBlockContextType>({\n code: \"\",\n});\n\n// Highlighter cache (singleton per language)\nconst highlighterCache = new Map<\n string,\n Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>\n>();\n\n// Token cache — bounded to the `MAX_TOKENS_CACHE_ENTRIES` most recently used\n// entries (perf review 1.4a). Unbounded growth showed up while streaming: a\n// growing code string re-tokenizes on every token, and every intermediate\n// string used to get its own permanent cache entry — O(n²) memory that was\n// never released for the lifetime of the tab.\nconst MAX_TOKENS_CACHE_ENTRIES = 200;\nconst tokensCache = new Map<string, TokenizedCode>();\n\n/** Reads `key`, marking it most-recently-used (moves it to the end). */\nconst tokensCacheGet = (key: string): TokenizedCode | undefined => {\n const value = tokensCache.get(key);\n if (value === undefined) return undefined;\n tokensCache.delete(key);\n tokensCache.set(key, value);\n return value;\n};\n\n/** Writes `key`, evicting the least-recently-used entries over the cap. */\nconst tokensCacheSet = (key: string, value: TokenizedCode): void => {\n tokensCache.delete(key);\n tokensCache.set(key, value);\n while (tokensCache.size > MAX_TOKENS_CACHE_ENTRIES) {\n const oldestKey = tokensCache.keys().next().value;\n if (oldestKey === undefined) break;\n tokensCache.delete(oldestKey);\n }\n};\n\n// Subscribers for async token updates\nconst subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();\n\n// FNV-1a 32-bit — cheap, and (unlike the previous length + first/last 100\n// chars key) hashes the FULL string, so two different strings of the same\n// length sharing a prefix and suffix longer than 100 chars (an edit to the\n// MIDDLE of a >200-char block — the common case for a diff or a streamed\n// correction) no longer collide onto the same cache entry and show stale,\n// wrong-content highlighting (#perf 1.4b).\nconst hashCode = (value: string): string => {\n let hash = 0x811c9dc5;\n for (let i = 0; i < value.length; i++) {\n // oxlint-disable-next-line eslint(no-bitwise)\n hash ^= value.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n // oxlint-disable-next-line eslint(no-bitwise)\n return (hash >>> 0).toString(36);\n};\n\n// The theme id is part of the cache key (#315) — the SAME code+language must\n// re-tokenize (and re-cache) when the active brand theme changes, or a code\n// block would keep showing stale colors from the theme active when it was\n// first highlighted. The id is derived from `getThemeScopeKey` (the RAW\n// `data-theme` attribute, or a sentinel when unset) rather than the validated\n// `ThemeName` — see `_code-block-theme.ts`'s module doc comment for why: an\n// unset attribute and an explicit `data-theme=\"light\"` both narrow to\n// the SAME `ThemeName`, but `:root`'s `--code-*` fallback values are their own\n// distinct placeholder palette, not an alias of `light`'s — so keying on\n// the validated name would let the pre-mount render's `:root` colors poison\n// the cache under the key `ThemeProvider` later writes explicitly, and they'd\n// never be replaced.\nconst getTokensCacheKey = (code: string, language: BundledLanguage, themeId: string) =>\n `${themeId}:${language}:${code.length}:${hashCode(code)}`;\n\nconst getHighlighter = (\n language: BundledLanguage,\n): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {\n const cached = highlighterCache.get(language);\n if (cached) {\n return cached;\n }\n\n // No themes preloaded here (#315) — the theme is derived from brand tokens\n // per call (`buildCodeBlockTheme`) and passed directly to `codeToTokens`,\n // never a bundled `github-*` literal.\n const highlighterPromise = createHighlighter({\n langs: [language],\n themes: [],\n });\n\n highlighterCache.set(language, highlighterPromise);\n return highlighterPromise;\n};\n\n// Create raw tokens for immediate display while highlighting loads\nconst createRawTokens = (code: string): TokenizedCode => ({\n bg: \"transparent\",\n fg: \"inherit\",\n tokens: code.split(\"\\n\").map((line) =>\n line === \"\"\n ? []\n : [\n {\n color: \"inherit\",\n content: line,\n } as ThemedToken,\n ],\n ),\n});\n\n// Synchronous highlight with callback for async results. `callback` stays in\n// its ORIGINAL third position (pre-#315 public API — a positional break here\n// would silently mis-wire any existing 3-arg caller, see #315 follow-up); `el`\n// is a new, purely-additive fourth parameter: the element (default `<html>`)\n// whose active brand theme's `--code-*` tokens the highlighter derives its\n// colors from. Passing a descendant of a region-scoped `<div data-theme=\"…\">`\n// resolves THAT region's theme instead of the document root's.\n//\n// `skipCache` (fifth, purely-additive parameter, default `false`) is set by a\n// caller mid-stream (perf review 1.4a): a streaming code block re-tokenizes a\n// GROWING string on every token, so every intermediate value is, by\n// definition, never seen again — permanently caching it only pays rent\n// (bounded now by the LRU cap, but still pure waste) without ever paying off\n// with a hit. A cache HIT (the final, settled string matches an\n// already-cached entry) is still honored either way.\nexport const highlightCode = (\n code: string,\n language: BundledLanguage,\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)\n callback?: (result: TokenizedCode) => void,\n el?: Element | null,\n skipCache = false,\n): TokenizedCode | null => {\n const themeId = codeBlockThemeId(getThemeScopeKey(el));\n const tokensCacheKey = getTokensCacheKey(code, language, themeId);\n\n // Return cached result if available\n const cached = tokensCacheGet(tokensCacheKey);\n if (cached) {\n return cached;\n }\n\n // Subscribe callback if provided\n if (callback) {\n if (!subscribers.has(tokensCacheKey)) {\n subscribers.set(tokensCacheKey, new Set());\n }\n subscribers.get(tokensCacheKey)?.add(callback);\n }\n\n // Start highlighting in background - fire-and-forget async pattern\n getHighlighter(language)\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)\n .then((highlighter) => {\n const availableLangs = highlighter.getLoadedLanguages();\n const langToUse = availableLangs.includes(language) ? language : \"text\";\n\n // Derived from brand `--code-*` tokens (#315), never a `github-*` literal.\n // Same `el` the theme NAME above came from, so id/type/colors agree.\n const result = highlighter.codeToTokens(code, {\n lang: langToUse,\n theme: buildCodeBlockTheme(el),\n });\n\n const tokenized: TokenizedCode = {\n bg: result.bg ?? \"transparent\",\n fg: result.fg ?? \"inherit\",\n tokens: result.tokens,\n };\n\n // Cache the result — unless the caller told us it is mid-stream, in\n // which case this exact string is very unlikely to recur.\n if (!skipCache) {\n tokensCacheSet(tokensCacheKey, tokenized);\n }\n\n // Notify all subscribers\n const subs = subscribers.get(tokensCacheKey);\n if (subs) {\n for (const sub of subs) {\n sub(tokenized);\n }\n subscribers.delete(tokensCacheKey);\n }\n })\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)\n .catch((error) => {\n console.error(\"Failed to highlight code:\", error);\n subscribers.delete(tokensCacheKey);\n });\n\n return null;\n};\n\nconst CodeBlockBody = memo(\n ({\n tokenized,\n showLineNumbers,\n wrap,\n className,\n }: {\n tokenized: TokenizedCode;\n showLineNumbers: boolean;\n wrap?: boolean;\n className?: string;\n }) => {\n const preStyle = useMemo(\n () => ({\n backgroundColor: tokenized.bg,\n color: tokenized.fg,\n }),\n [tokenized.bg, tokenized.fg],\n );\n\n const keyedLines = useMemo(() => addKeysToTokens(tokenized.tokens), [tokenized.tokens]);\n\n return (\n <pre\n className={cn(\"m-0 p-4 text-body\", wrap && \"whitespace-pre-wrap break-words\", className)}\n style={preStyle}\n >\n <code\n className={cn(\n \"font-mono text-body\",\n showLineNumbers && \"[counter-increment:line_0] [counter-reset:line]\",\n )}\n >\n {keyedLines.map((keyedLine) => (\n <LineSpan key={keyedLine.key} keyedLine={keyedLine} showLineNumbers={showLineNumbers} />\n ))}\n </code>\n </pre>\n );\n },\n (prevProps, nextProps) =>\n prevProps.tokenized === nextProps.tokenized &&\n prevProps.showLineNumbers === nextProps.showLineNumbers &&\n prevProps.wrap === nextProps.wrap &&\n prevProps.className === nextProps.className,\n);\n\nCodeBlockBody.displayName = \"CodeBlockBody\";\n\nexport const CodeBlockContainer = ({\n className,\n language,\n style,\n ...props\n}: HTMLAttributes<HTMLDivElement> & { language: string }) => (\n <div\n className={cn(\n \"group relative w-full overflow-hidden rounded-md border bg-background text-foreground\",\n className,\n )}\n data-language={language}\n style={{\n containIntrinsicSize: \"auto 200px\",\n contentVisibility: \"auto\",\n ...style,\n }}\n {...props}\n />\n);\n\nexport const CodeBlockHeader = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-meta\",\n className,\n )}\n {...props}\n >\n {children}\n </div>\n);\n\nexport const CodeBlockTitle = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div className={cn(\"flex items-center gap-2\", className)} {...props}>\n {children}\n </div>\n);\n\nexport const CodeBlockFilename = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLSpanElement>) => (\n <span className={cn(\"font-mono\", className)} {...props}>\n {children}\n </span>\n);\n\nexport const CodeBlockActions = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div className={cn(\"-my-1 -me-1 flex items-center gap-2\", className)} {...props}>\n {children}\n </div>\n);\n\n// Streaming re-highlight throttle (perf review 1.4e / §3.2): re-tokenizing\n// on every single streamed token is O(n²) work over the life of a response.\n// At most one highlight pass per window, ALWAYS with a trailing call so the\n// final, settled string still gets highlighted the moment streaming stops.\nconst STREAM_HIGHLIGHT_THROTTLE_MS = 200;\n\nexport const CodeBlockContent = ({\n code,\n language,\n showLineNumbers = false,\n wrap = false,\n isStreaming = false,\n}: {\n code: string;\n language: BundledLanguage;\n showLineNumbers?: boolean;\n wrap?: boolean;\n isStreaming?: boolean;\n}) => {\n // Track the active brand theme (#315), SCOPED to this code block's own\n // subtree (`getThemeScope`) rather than always `<html>` — so a CodeBlock\n // nested inside a region-scoped `<div data-theme=\"dark\">` (a supported\n // ThemeProvider/decorator pattern) picks up THAT region's `--code-*` tokens,\n // not the document root's. `useThemeScopeRevision` (`_theme-scope-store.ts`)\n // shares ONE MutationObserver per scope element across every subscriber\n // watching it, instead of one per `CodeBlock` instance (perf review §3.3).\n const scopeRef = useRef<HTMLDivElement>(null);\n // The ref only attaches after the first commit, so the scope resolved\n // during the initial render (before mount) may have fallen back to\n // `<html>`. Force one more render right after mount — synchronously, before\n // paint, via `useLayoutEffect` — so a scoped code block never flashes the\n // document root's colors first; `useThemeScopeRevision` re-subscribes to\n // the now-correct scope on that render.\n const [, forceMountRerender] = useReducer((tick: number) => tick + 1, 0);\n useLayoutEffect(() => {\n forceMountRerender();\n }, []);\n\n const scopeEl = getThemeScope(scopeRef.current);\n const themeRevision = useThemeScopeRevision(scopeRef.current);\n // Keyed on the RAW `data-theme` scope, not the validated theme name (#315\n // follow-up) — see `_code-block-theme.ts`'s module doc comment. An unset\n // attribute (the pre-mount render) and an explicit `data-theme=\"light\"`\n // both narrow to the SAME `ThemeName`, so using the validated name here would\n // mean this memo's dependency doesn't CHANGE across that mutation and the\n // stale `:root`-tokenized colors would never be recomputed once\n // `ThemeProvider` mounts and writes the attribute explicitly.\n const themeScopeKey = useMemo(\n () => getThemeScopeKey(scopeEl),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- themeRevision is the trigger; scopeEl's own identity already changes across the post-mount re-render above.\n [themeRevision, scopeEl],\n );\n\n // Memoized raw tokens for immediate display\n const rawTokens = useMemo(() => createRawTokens(code), [code]);\n\n // Synchronous cache PEEK — a plain, side-effect-free `Map.get`, never a\n // call into `highlightCode` (the OLD code called it here with no callback,\n // AND again in the effect below with one; on a cache miss both calls\n // independently kicked off `getHighlighter(language).then(...)` and\n // computed `codeToTokens` for the SAME code twice — perf review 1.4d).\n // Highlighting is now triggered from exactly ONE place: the effect.\n const themeId = codeBlockThemeId(themeScopeKey);\n const tokensCacheKey = getTokensCacheKey(code, language, themeId);\n const syncTokens = tokensCache.get(tokensCacheKey) ?? rawTokens;\n\n // Async highlighting result (populated after shiki loads, or synchronously\n // inside the effect on a cache hit).\n const [asyncTokens, setAsyncTokens] = useState<TokenizedCode | null>(null);\n const asyncKeyRef = useRef({ code, language, themeScopeKey });\n\n // Invalidate stale async tokens synchronously during render\n if (\n asyncKeyRef.current.code !== code ||\n asyncKeyRef.current.language !== language ||\n asyncKeyRef.current.themeScopeKey !== themeScopeKey\n ) {\n asyncKeyRef.current = { code, language, themeScopeKey };\n setAsyncTokens(null);\n }\n\n // Last time THIS instance actually ran a highlight pass — the throttle\n // clock while `isStreaming` (perf review 1.4e).\n const lastHighlightAtRef = useRef(0);\n\n useEffect(() => {\n let cancelled = false;\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const runHighlight = () => {\n lastHighlightAtRef.current = Date.now();\n // `highlightCode` returns the tokenized result SYNCHRONOUSLY on a cache\n // hit (and never invokes the callback in that case — see its early\n // `if (cached) return cached` branch). That cache hit is the COMMON case\n // right after a theme switch (#315): the highlighter for this language is\n // already loaded, so re-tokenizing for the new theme resolves within a\n // microtask — often before this effect even runs — and without this\n // direct check `asyncTokens` would stay null forever, stranding the\n // code block on its raw/unhighlighted fallback after every theme change.\n const cached = highlightCode(\n code,\n language,\n (result) => {\n if (!cancelled) {\n setAsyncTokens(result);\n }\n },\n scopeEl,\n isStreaming,\n );\n if (cached && !cancelled) {\n setAsyncTokens(cached);\n }\n };\n\n if (!isStreaming) {\n // Not streaming: always highlight immediately (also the path a\n // just-finished stream's LAST token takes, guaranteeing the final,\n // settled code always gets a full, cacheable highlight even if the\n // throttle skipped some of the tokens before it).\n runHighlight();\n } else {\n const elapsed = Date.now() - lastHighlightAtRef.current;\n if (elapsed >= STREAM_HIGHLIGHT_THROTTLE_MS) {\n runHighlight();\n } else {\n timeoutId = setTimeout(runHighlight, STREAM_HIGHLIGHT_THROTTLE_MS - elapsed);\n }\n }\n\n return () => {\n cancelled = true;\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [code, language, themeScopeKey, scopeEl, isStreaming]);\n\n const tokenized = asyncTokens ?? syncTokens;\n\n return (\n <div\n ref={scopeRef}\n className={cn(\n \"relative overflow-auto\",\n // Discoverable horizontal-scroll affordance in non-wrap mode, so a narrow\n // embed doesn't silently clip long lines. Hidden when wrapping (no overflow).\n !wrap &&\n \"[scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border\",\n )}\n >\n <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} wrap={wrap} />\n </div>\n );\n};\n\nexport const CodeBlock = ({\n code,\n language,\n showLineNumbers = false,\n wrap = false,\n isStreaming = false,\n className,\n children,\n ...props\n}: CodeBlockProps) => {\n const contextValue = useMemo(() => ({ code }), [code]);\n const { t } = useLocale();\n\n return (\n <CodeBlockContext.Provider value={contextValue}>\n <CodeBlockContainer className={className} language={language} {...props}>\n {children}\n <CodeBlockContent\n code={code}\n language={language}\n showLineNumbers={showLineNumbers}\n wrap={wrap}\n isStreaming={isStreaming}\n />\n {isStreaming ? (\n <div\n className=\"flex items-center gap-2 border-t bg-muted/80 px-3 py-1.5 text-caption text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Shimmer>{t(\"ai.codeBlock.generating\")}</Shimmer>\n </div>\n ) : null}\n </CodeBlockContainer>\n </CodeBlockContext.Provider>\n );\n};\n\nexport type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {\n onCopy?: () => void;\n onError?: (error: Error) => void;\n timeout?: number;\n};\n\nexport const CodeBlockCopyButton = ({\n onCopy,\n onError,\n timeout,\n children,\n className,\n ...props\n}: CodeBlockCopyButtonProps) => {\n const { t } = useLocale();\n const { code } = useContext(CodeBlockContext);\n // Shared implementation (`@elabs-ai/components-ui`) instead of a private\n // copy of the same copy-to-clipboard state machine — this one used to\n // duplicate `SnippetCopyButton`'s (issue-workflow.md dedupe finding).\n const { copied: isCopied, copy } = useCopyToClipboard(\n timeout === undefined ? undefined : { resetAfterMs: timeout },\n );\n\n const copyToClipboard = useCallback(async () => {\n const ok = await copy(code);\n if (ok) {\n onCopy?.();\n } else {\n onError?.(new Error(\"Clipboard API not available\"));\n }\n }, [copy, code, onCopy, onError]);\n\n const Icon = isCopied ? CheckIcon : CopyIcon;\n\n return (\n <Button\n aria-label={t(\"copy\")}\n className={cn(\"shrink-0\", className)}\n onClick={copyToClipboard}\n size=\"icon\"\n variant=\"ghost\"\n {...props}\n >\n {children ?? (\n <span\n key={isCopied ? \"check\" : \"copy\"}\n className=\"flex animate-in fade-in-0 zoom-in-95 duration-fast ease-entrance\"\n >\n <Icon size={14} />\n </span>\n )}\n </Button>\n );\n};\n\nexport type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;\n\nexport const CodeBlockLanguageSelector = (props: CodeBlockLanguageSelectorProps) => (\n <Select {...props} />\n);\n\nexport type CodeBlockLanguageSelectorTriggerProps = ComponentProps<typeof SelectTrigger>;\n\nexport const CodeBlockLanguageSelectorTrigger = ({\n className,\n ...props\n}: CodeBlockLanguageSelectorTriggerProps) => (\n <SelectTrigger\n className={cn(\"h-7 border-none bg-transparent px-2 text-meta shadow-none\", className)}\n size=\"sm\"\n {...props}\n />\n);\n\nexport type CodeBlockLanguageSelectorValueProps = ComponentProps<typeof SelectValue>;\n\nexport const CodeBlockLanguageSelectorValue = (props: CodeBlockLanguageSelectorValueProps) => (\n <SelectValue {...props} />\n);\n\nexport type CodeBlockLanguageSelectorContentProps = ComponentProps<typeof SelectContent>;\n\nexport const CodeBlockLanguageSelectorContent = ({\n align = \"end\",\n ...props\n}: CodeBlockLanguageSelectorContentProps) => <SelectContent align={align} {...props} />;\n\nexport type CodeBlockLanguageSelectorItemProps = ComponentProps<typeof SelectItem>;\n\nexport const CodeBlockLanguageSelectorItem = (props: CodeBlockLanguageSelectorItemProps) => (\n <SelectItem {...props} />\n);\n"],"mappings":"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","names":[]}
|
package/dist/chunk-V6E5DYZD.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
// src/image.tsx
|
|
4
|
-
import { Skeleton, useLocale } from "@elabs-ai/components-ui";
|
|
5
|
-
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
6
|
-
import { ImageOff } from "lucide-react";
|
|
7
|
-
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
8
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
var Image = forwardRef(function Image2({
|
|
10
|
-
base64,
|
|
11
|
-
uint8Array: _uint8Array,
|
|
12
|
-
mediaType,
|
|
13
|
-
className,
|
|
14
|
-
showSkeleton,
|
|
15
|
-
onLoad,
|
|
16
|
-
onError,
|
|
17
|
-
width,
|
|
18
|
-
height,
|
|
19
|
-
alt,
|
|
20
|
-
...props
|
|
21
|
-
}, forwardedRef) {
|
|
22
|
-
const { t } = useLocale();
|
|
23
|
-
const shouldSkeleton = showSkeleton ?? Boolean(width && height);
|
|
24
|
-
const [loaded, setLoaded] = useState(false);
|
|
25
|
-
const [failed, setFailed] = useState(false);
|
|
26
|
-
const imgRef = useRef(null);
|
|
27
|
-
useImperativeHandle(forwardedRef, () => imgRef.current);
|
|
28
|
-
const pending = shouldSkeleton && !loaded && !failed;
|
|
29
|
-
const [announce, setAnnounce] = useState(false);
|
|
30
|
-
useEffect(() => setAnnounce(pending), [pending]);
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (imgRef.current?.complete) setLoaded(true);
|
|
33
|
-
}, []);
|
|
34
|
-
if (failed) {
|
|
35
|
-
return /* @__PURE__ */ jsx(
|
|
36
|
-
"span",
|
|
37
|
-
{
|
|
38
|
-
className: cn(
|
|
39
|
-
"flex flex-col items-center justify-center gap-1 rounded-md bg-muted text-muted-foreground",
|
|
40
|
-
className
|
|
41
|
-
),
|
|
42
|
-
style: { height, width },
|
|
43
|
-
...alt ? { role: "img", "aria-label": alt } : { "aria-hidden": true },
|
|
44
|
-
children: /* @__PURE__ */ jsx(ImageOff, { className: "size-6", "aria-hidden": "true" })
|
|
45
|
-
}
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
const img = /* @__PURE__ */ jsx(
|
|
49
|
-
"img",
|
|
50
|
-
{
|
|
51
|
-
...props,
|
|
52
|
-
ref: imgRef,
|
|
53
|
-
alt: alt ?? "",
|
|
54
|
-
height,
|
|
55
|
-
width,
|
|
56
|
-
className: cn("h-auto max-w-full overflow-hidden rounded-md", className),
|
|
57
|
-
src: `data:${mediaType};base64,${base64}`,
|
|
58
|
-
onLoad: (event) => {
|
|
59
|
-
setLoaded(true);
|
|
60
|
-
onLoad?.(event);
|
|
61
|
-
},
|
|
62
|
-
onError: (event) => {
|
|
63
|
-
setFailed(true);
|
|
64
|
-
onError?.(event);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
);
|
|
68
|
-
if (!shouldSkeleton) return img;
|
|
69
|
-
return /* @__PURE__ */ jsxs("span", { className: "relative inline-block max-w-full", style: { height, width }, children: [
|
|
70
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", children: announce ? t("loading") : "" }),
|
|
71
|
-
pending ? /* @__PURE__ */ jsx(Skeleton, { className: "absolute inset-0 size-full rounded-md" }) : null,
|
|
72
|
-
img
|
|
73
|
-
] });
|
|
74
|
-
});
|
|
75
|
-
Image.displayName = "Image";
|
|
76
|
-
|
|
77
|
-
export {
|
|
78
|
-
Image
|
|
79
|
-
};
|
|
80
|
-
//# sourceMappingURL=chunk-V6E5DYZD.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/image.tsx"],"sourcesContent":["\"use client\";\n\nimport { Skeleton, useLocale } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport type { Experimental_GeneratedImage } from \"ai\";\nimport { ImageOff } from \"lucide-react\";\nimport type { ImgHTMLAttributes } from \"react\";\nimport { forwardRef, useEffect, useImperativeHandle, useRef, useState } from \"react\";\n\nexport type ImageProps = Experimental_GeneratedImage &\n Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\"> & {\n /**\n * Show a Skeleton placeholder (loading-states.md) until the img element\n * finishes decoding, so the base64 payload does not pop in. Requires\n * explicit `width`+`height` so the reserved box matches the final size\n * (interaction-guidelines.md Images section — prevents CLS); without\n * both there is no box to reserve, so the default is `false` in that\n * case rather than mounting a skeleton that collapses to nothing.\n * @default Boolean(width && height)\n */\n showSkeleton?: boolean;\n };\n\n/** `<img>` wrapper for an AI-SDK generated image, with a decode-aware skeleton. */\nexport const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(\n {\n base64,\n uint8Array: _uint8Array,\n mediaType,\n className,\n showSkeleton,\n onLoad,\n onError,\n width,\n height,\n alt,\n ...props\n },\n forwardedRef,\n) {\n const { t } = useLocale();\n const shouldSkeleton = showSkeleton ?? Boolean(width && height);\n const [loaded, setLoaded] = useState(false);\n const [failed, setFailed] = useState(false);\n const imgRef = useRef<HTMLImageElement>(null);\n useImperativeHandle(forwardedRef, () => imgRef.current as HTMLImageElement);\n\n const pending = shouldSkeleton && !loaded && !failed;\n\n // The live region must exist BEFORE its text appears (ARIA22), so it is\n // always mounted (while a skeleton can render at all) and its label is set\n // one tick later via this effect.\n const [announce, setAnnounce] = useState(false);\n useEffect(() => setAnnounce(pending), [pending]);\n\n // A cached / instant data-URI decode can finish before React attaches\n // `onLoad` — read `complete` once mounted (gallery.tsx's GalleryImg has the\n // same guard).\n useEffect(() => {\n if (imgRef.current?.complete) setLoaded(true);\n }, []);\n\n if (failed) {\n return (\n <span\n className={cn(\n \"flex flex-col items-center justify-center gap-1 rounded-md bg-muted text-muted-foreground\",\n className,\n )}\n style={{ height, width }}\n {...(alt ? { role: \"img\", \"aria-label\": alt } : { \"aria-hidden\": true })}\n >\n <ImageOff className=\"size-6\" aria-hidden=\"true\" />\n </span>\n );\n }\n\n const img = (\n <img\n {...props}\n ref={imgRef}\n alt={alt ?? \"\"}\n height={height}\n width={width}\n className={cn(\"h-auto max-w-full overflow-hidden rounded-md\", className)}\n src={`data:${mediaType};base64,${base64}`}\n onLoad={(event) => {\n setLoaded(true);\n onLoad?.(event);\n }}\n onError={(event) => {\n setFailed(true);\n onError?.(event);\n }}\n />\n );\n\n // No skeleton to reserve a box for — render the bare <img> exactly as\n // before this feature landed (a `className` like \"w-full\" keeps resolving\n // against the real parent, not a shrink-to-fit wrapper), and mount no\n // (permanently empty) live region.\n if (!shouldSkeleton) return img;\n\n return (\n <span className=\"relative inline-block max-w-full\" style={{ height, width }}>\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {announce ? t(\"loading\") : \"\"}\n </span>\n {pending ? <Skeleton className=\"absolute inset-0 size-full rounded-md\" /> : null}\n {img}\n </span>\n );\n});\n\nImage.displayName = \"Image\";\n"],"mappings":";;;AAEA,SAAS,UAAU,iBAAiB;AACpC,SAAS,UAAU;AAEnB,SAAS,gBAAgB;AAEzB,SAAS,YAAY,WAAW,qBAAqB,QAAQ,gBAAgB;AAiErE,cAgCJ,YAhCI;AAhDD,IAAM,QAAQ,WAAyC,SAASA,OACrE;AAAA,EACE;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,cACA;AACA,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,iBAAiB,gBAAgB,QAAQ,SAAS,MAAM;AAC9D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,SAAS,OAAyB,IAAI;AAC5C,sBAAoB,cAAc,MAAM,OAAO,OAA2B;AAE1E,QAAM,UAAU,kBAAkB,CAAC,UAAU,CAAC;AAK9C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,YAAU,MAAM,YAAY,OAAO,GAAG,CAAC,OAAO,CAAC;AAK/C,YAAU,MAAM;AACd,QAAI,OAAO,SAAS,SAAU,WAAU,IAAI;AAAA,EAC9C,GAAG,CAAC,CAAC;AAEL,MAAI,QAAQ;AACV,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACA,OAAO,EAAE,QAAQ,MAAM;AAAA,QACtB,GAAI,MAAM,EAAE,MAAM,OAAO,cAAc,IAAI,IAAI,EAAE,eAAe,KAAK;AAAA,QAEtE,8BAAC,YAAS,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,IAClD;AAAA,EAEJ;AAEA,QAAM,MACJ;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,KAAK;AAAA,MACL,KAAK,OAAO;AAAA,MACZ;AAAA,MACA;AAAA,MACA,WAAW,GAAG,gDAAgD,SAAS;AAAA,MACvE,KAAK,QAAQ,SAAS,WAAW,MAAM;AAAA,MACvC,QAAQ,CAAC,UAAU;AACjB,kBAAU,IAAI;AACd,iBAAS,KAAK;AAAA,MAChB;AAAA,MACA,SAAS,CAAC,UAAU;AAClB,kBAAU,IAAI;AACd,kBAAU,KAAK;AAAA,MACjB;AAAA;AAAA,EACF;AAOF,MAAI,CAAC,eAAgB,QAAO;AAE5B,SACE,qBAAC,UAAK,WAAU,oCAAmC,OAAO,EAAE,QAAQ,MAAM,GACxE;AAAA,wBAAC,UAAK,WAAU,WAAU,MAAK,UAAS,aAAU,UAC/C,qBAAW,EAAE,SAAS,IAAI,IAC7B;AAAA,IACC,UAAU,oBAAC,YAAS,WAAU,yCAAwC,IAAK;AAAA,IAC3E;AAAA,KACH;AAEJ,CAAC;AAED,MAAM,cAAc;","names":["Image"]}
|