@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
package/src/image.tsx CHANGED
@@ -1,115 +1,2 @@
1
- "use client";
2
-
3
- import { Skeleton, useLocale } from "@elabs-ai/components-ui";
4
- import { cn } from "@elabs-ai/components-ui/lib/cn";
5
- import type { Experimental_GeneratedImage } from "ai";
6
- import { ImageOff } from "lucide-react";
7
- import type { ImgHTMLAttributes } from "react";
8
- import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
9
-
10
- export type ImageProps = Experimental_GeneratedImage &
11
- Omit<ImgHTMLAttributes<HTMLImageElement>, "src"> & {
12
- /**
13
- * Show a Skeleton placeholder (loading-states.md) until the img element
14
- * finishes decoding, so the base64 payload does not pop in. Requires
15
- * explicit `width`+`height` so the reserved box matches the final size
16
- * (interaction-guidelines.md Images section — prevents CLS); without
17
- * both there is no box to reserve, so the default is `false` in that
18
- * case rather than mounting a skeleton that collapses to nothing.
19
- * @default Boolean(width && height)
20
- */
21
- showSkeleton?: boolean;
22
- };
23
-
24
- /** `<img>` wrapper for an AI-SDK generated image, with a decode-aware skeleton. */
25
- export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(
26
- {
27
- base64,
28
- uint8Array: _uint8Array,
29
- mediaType,
30
- className,
31
- showSkeleton,
32
- onLoad,
33
- onError,
34
- width,
35
- height,
36
- alt,
37
- ...props
38
- },
39
- forwardedRef,
40
- ) {
41
- const { t } = useLocale();
42
- const shouldSkeleton = showSkeleton ?? Boolean(width && height);
43
- const [loaded, setLoaded] = useState(false);
44
- const [failed, setFailed] = useState(false);
45
- const imgRef = useRef<HTMLImageElement>(null);
46
- useImperativeHandle(forwardedRef, () => imgRef.current as HTMLImageElement);
47
-
48
- const pending = shouldSkeleton && !loaded && !failed;
49
-
50
- // The live region must exist BEFORE its text appears (ARIA22), so it is
51
- // always mounted (while a skeleton can render at all) and its label is set
52
- // one tick later via this effect.
53
- const [announce, setAnnounce] = useState(false);
54
- useEffect(() => setAnnounce(pending), [pending]);
55
-
56
- // A cached / instant data-URI decode can finish before React attaches
57
- // `onLoad` — read `complete` once mounted (gallery.tsx's GalleryImg has the
58
- // same guard).
59
- useEffect(() => {
60
- if (imgRef.current?.complete) setLoaded(true);
61
- }, []);
62
-
63
- if (failed) {
64
- return (
65
- <span
66
- className={cn(
67
- "flex flex-col items-center justify-center gap-1 rounded-md bg-muted text-muted-foreground",
68
- className,
69
- )}
70
- style={{ height, width }}
71
- {...(alt ? { role: "img", "aria-label": alt } : { "aria-hidden": true })}
72
- >
73
- <ImageOff className="size-6" aria-hidden="true" />
74
- </span>
75
- );
76
- }
77
-
78
- const img = (
79
- <img
80
- {...props}
81
- ref={imgRef}
82
- alt={alt ?? ""}
83
- height={height}
84
- width={width}
85
- className={cn("h-auto max-w-full overflow-hidden rounded-md", className)}
86
- src={`data:${mediaType};base64,${base64}`}
87
- onLoad={(event) => {
88
- setLoaded(true);
89
- onLoad?.(event);
90
- }}
91
- onError={(event) => {
92
- setFailed(true);
93
- onError?.(event);
94
- }}
95
- />
96
- );
97
-
98
- // No skeleton to reserve a box for — render the bare <img> exactly as
99
- // before this feature landed (a `className` like "w-full" keeps resolving
100
- // against the real parent, not a shrink-to-fit wrapper), and mount no
101
- // (permanently empty) live region.
102
- if (!shouldSkeleton) return img;
103
-
104
- return (
105
- <span className="relative inline-block max-w-full" style={{ height, width }}>
106
- <span className="sr-only" role="status" aria-live="polite">
107
- {announce ? t("loading") : ""}
108
- </span>
109
- {pending ? <Skeleton className="absolute inset-0 size-full rounded-md" /> : null}
110
- {img}
111
- </span>
112
- );
113
- });
114
-
115
- Image.displayName = "Image";
1
+ /** @deprecated Use `GeneratedImage` — `Image` is now the ui primitive (ADR 0041). Removed in the next major. */
2
+ export { GeneratedImage as Image, type GeneratedImageProps as ImageProps } from "./generated-image";
package/src/index.ts CHANGED
@@ -45,6 +45,7 @@ export * from "./environment-variables";
45
45
  export * from "./file-tree";
46
46
  export * from "./gallery";
47
47
  export * from "./grouped-parts";
48
+ export * from "./generated-image";
48
49
  export * from "./image";
49
50
  export * from "./inline-citation";
50
51
  export * from "./a2ui";
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { Image } from "@elabs-ai/components-ui";
3
4
  import { cn } from "@elabs-ai/components-ui/lib/cn";
4
5
  import { resolveThemeIsDark } from "@elabs-ai/components-tokens";
5
6
  import { BotIcon } from "lucide-react";
@@ -130,30 +131,31 @@ export const ModelProviderLogo = ({
130
131
  onError,
131
132
  ...props
132
133
  }: ModelProviderLogoProps) => {
133
- const [failed, setFailed] = useState(false);
134
134
  const isDark = useIsDarkTheme();
135
+ const label = `${provider} logo`;
135
136
 
136
- // A blocked/missing logo must not leave a broken-image glyph in the row.
137
- if (failed) {
138
- return (
139
- <>
140
- {fallback ?? (
141
- <BotIcon aria-label={`${provider} logo`} className={cn("size-3", className)} role="img" />
142
- )}
143
- </>
144
- );
145
- }
146
-
137
+ // A blocked/missing logo must not leave a broken-image glyph in the row:
138
+ // ui `Image` renders `fallback` AS the root on a terminal error (or at once
139
+ // when there is no `src`).
147
140
  return (
148
- <img
141
+ <Image
149
142
  {...props}
150
- alt={`${provider} logo`}
143
+ data-slot="model-provider-logo"
144
+ alt={label}
151
145
  className={cn("size-3", isDark && "invert", className)}
146
+ fallback={
147
+ fallback ?? (
148
+ <BotIcon
149
+ data-slot="model-provider-logo"
150
+ aria-label={label}
151
+ className={cn("size-3", className)}
152
+ role="img"
153
+ />
154
+ )
155
+ }
152
156
  height={12}
153
- onError={(event) => {
154
- setFailed(true);
155
- onError?.(event);
156
- }}
157
+ onError={onError}
158
+ showSkeleton={false}
157
159
  // AFTER the spread, so the caller's `src` wins via the destructured
158
160
  // default rather than being silently overwritten.
159
161
  src={src ?? `${MODEL_PROVIDER_LOGO_BASE_URL}/${provider}.svg`}
@@ -0,0 +1,24 @@
1
+ import { cleanup, fireEvent, render } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { QueueItemImage } from "./queue";
4
+
5
+ afterEach(cleanup);
6
+
7
+ describe("QueueItemImage", () => {
8
+ it("renders the ui Image as a 32 px decorative thumbnail with its own slot", () => {
9
+ const { container } = render(<QueueItemImage src="/thumb.png" />);
10
+ const img = container.firstElementChild;
11
+ expect(img?.tagName).toBe("IMG");
12
+ expect(img).toHaveAttribute("data-slot", "queue-item-image");
13
+ expect(img).toHaveAttribute("alt", "");
14
+ expect(img).toHaveAttribute("width", "32");
15
+ expect(img).toHaveClass("object-cover");
16
+ });
17
+
18
+ it("keeps a caller's alt and settles into a fallback on a load error", () => {
19
+ const { container, getByRole } = render(<QueueItemImage src="/thumb.png" alt="Screenshot" />);
20
+ fireEvent.error(getByRole("img", { name: "Screenshot" }));
21
+ expect(container.querySelector("img")).toBeNull();
22
+ expect(getByRole("img", { name: "Screenshot" })).toBeInTheDocument();
23
+ });
24
+ });
package/src/queue.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@elabs-ai/components-ui";
3
+ import { Button, Image } from "@elabs-ai/components-ui";
4
4
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@elabs-ai/components-ui";
5
5
  import { ScrollArea } from "@elabs-ai/components-ui";
6
6
  import { cn } from "@elabs-ai/components-ui/lib/cn";
@@ -127,13 +127,16 @@ export const QueueItemAttachment = ({ className, ...props }: QueueItemAttachment
127
127
 
128
128
  export type QueueItemImageProps = ComponentProps<"img">;
129
129
 
130
- export const QueueItemImage = ({ className, ...props }: QueueItemImageProps) => (
131
- <img
132
- alt=""
133
- className={cn("h-8 w-8 rounded border object-cover", className)}
130
+ export const QueueItemImage = ({ className, alt, ...props }: QueueItemImageProps) => (
131
+ <Image
134
132
  height={32}
135
133
  width={32}
134
+ showSkeleton={false}
136
135
  {...props}
136
+ data-slot="queue-item-image"
137
+ alt={alt ?? ""}
138
+ fit="cover"
139
+ className={cn("h-8 w-8 rounded border", className)}
137
140
  />
138
141
  );
139
142
 
@@ -1,85 +0,0 @@
1
- "use client";
2
- "use client";
3
-
4
- // src/_audio-player-media-chrome.tsx
5
- import { Button, ButtonGroup, ButtonGroupText } from "@elabs-ai/components-ui";
6
- import { cn } from "@elabs-ai/components-ui/lib/cn";
7
- import * as MediaChromeReactModule from "media-chrome/react";
8
- import { jsx } from "react/jsx-runtime";
9
- var {
10
- MediaControlBar,
11
- MediaController,
12
- MediaDurationDisplay,
13
- MediaMuteButton,
14
- MediaPlayButton,
15
- MediaSeekBackwardButton,
16
- MediaSeekForwardButton,
17
- MediaTimeDisplay,
18
- MediaTimeRange,
19
- MediaVolumeRange
20
- } = MediaChromeReactModule;
21
- if (!MediaController) {
22
- throw new Error("Cannot find module 'media-chrome'");
23
- }
24
- var AudioPlayer = ({ children, style, ...props }) => /* @__PURE__ */ jsx(
25
- MediaController,
26
- {
27
- audio: true,
28
- style: {
29
- "--media-background-color": "transparent",
30
- "--media-button-icon-height": "1rem",
31
- "--media-button-icon-width": "1rem",
32
- "--media-control-background": "transparent",
33
- "--media-control-hover-background": "var(--color-accent)",
34
- "--media-control-padding": "0",
35
- "--media-font": "var(--font-sans)",
36
- "--media-font-size": "10px",
37
- "--media-icon-color": "currentColor",
38
- "--media-preview-time-background": "var(--color-background)",
39
- "--media-preview-time-border-radius": "var(--radius-md)",
40
- "--media-preview-time-text-shadow": "none",
41
- "--media-primary-color": "var(--color-primary)",
42
- "--media-range-bar-color": "var(--color-primary)",
43
- "--media-range-track-background": "var(--color-secondary)",
44
- "--media-secondary-color": "var(--color-secondary)",
45
- "--media-text-color": "var(--color-foreground)",
46
- "--media-tooltip-arrow-display": "none",
47
- "--media-tooltip-background": "var(--color-background)",
48
- "--media-tooltip-border-radius": "var(--radius-md)",
49
- ...style
50
- },
51
- ...props,
52
- children
53
- }
54
- );
55
- var AudioPlayerControlBar = ({ children, ...props }) => /* @__PURE__ */ jsx(MediaControlBar, { ...props, children: /* @__PURE__ */ jsx(ButtonGroup, { orientation: "horizontal", children }) });
56
- var AudioPlayerPlayButton = ({ className, ...props }) => /* @__PURE__ */ jsx(Button, { asChild: true, size: "icon-sm", variant: "outline", children: /* @__PURE__ */ jsx(MediaPlayButton, { className: cn("bg-transparent", className), ...props }) });
57
- var AudioPlayerSeekBackwardButton = ({
58
- seekOffset = 10,
59
- ...props
60
- }) => /* @__PURE__ */ jsx(Button, { asChild: true, size: "icon-sm", variant: "outline", children: /* @__PURE__ */ jsx(MediaSeekBackwardButton, { seekOffset, ...props }) });
61
- var AudioPlayerSeekForwardButton = ({
62
- seekOffset = 10,
63
- ...props
64
- }) => /* @__PURE__ */ jsx(Button, { asChild: true, size: "icon-sm", variant: "outline", children: /* @__PURE__ */ jsx(MediaSeekForwardButton, { seekOffset, ...props }) });
65
- var AudioPlayerTimeDisplay = ({ className, ...props }) => /* @__PURE__ */ jsx(ButtonGroupText, { asChild: true, className: "bg-transparent", children: /* @__PURE__ */ jsx(MediaTimeDisplay, { className: cn("tabular-nums", className), ...props }) });
66
- var AudioPlayerTimeRange = ({ className, ...props }) => /* @__PURE__ */ jsx(ButtonGroupText, { asChild: true, className: "bg-transparent", children: /* @__PURE__ */ jsx(MediaTimeRange, { className: cn("", className), ...props }) });
67
- var AudioPlayerDurationDisplay = ({
68
- className,
69
- ...props
70
- }) => /* @__PURE__ */ jsx(ButtonGroupText, { asChild: true, className: "bg-transparent", children: /* @__PURE__ */ jsx(MediaDurationDisplay, { className: cn("tabular-nums", className), ...props }) });
71
- var AudioPlayerMuteButton = ({ className, ...props }) => /* @__PURE__ */ jsx(ButtonGroupText, { asChild: true, className: "bg-transparent", children: /* @__PURE__ */ jsx(MediaMuteButton, { className: cn("", className), ...props }) });
72
- var AudioPlayerVolumeRange = ({ className, ...props }) => /* @__PURE__ */ jsx(ButtonGroupText, { asChild: true, className: "bg-transparent", children: /* @__PURE__ */ jsx(MediaVolumeRange, { className: cn("", className), ...props }) });
73
- export {
74
- AudioPlayer,
75
- AudioPlayerControlBar,
76
- AudioPlayerDurationDisplay,
77
- AudioPlayerMuteButton,
78
- AudioPlayerPlayButton,
79
- AudioPlayerSeekBackwardButton,
80
- AudioPlayerSeekForwardButton,
81
- AudioPlayerTimeDisplay,
82
- AudioPlayerTimeRange,
83
- AudioPlayerVolumeRange
84
- };
85
- //# sourceMappingURL=_audio-player-media-chrome.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/_audio-player-media-chrome.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * The media-chrome half of `AudioPlayer`, split out so it can be `lazy()`-loaded.\n *\n * `media-chrome` declares no `sideEffects`, so a static import from\n * `audio-player.tsx` put the whole custom-element library (and the\n * `media-chrome/react` wrappers that register it) into the entry chunk of every\n * `@elabs-ai/components-ai` consumer — including the vast majority that\n * never render an `AudioPlayer`. Keeping every media-chrome *value* import in\n * this module, reached only through\n * `lazy(() => import(\"./_audio-player-media-chrome\"))`, confines it to its own\n * chunk. `audio-player.tsx` owns the public prop types, and — since issue #101\n * — owns them as OWNED types (`AudioPlayerPartProps` and its ten aliases) that\n * never reference `media-chrome/react`'s own types; this module still imports\n * the REAL media-chrome values (below) and, at the bottom of the file, proves\n * every owned type stays assignable to its real counterpart.\n *\n * See ADR 0019 and `pnpm heavy-deps:check`.\n *\n * @lazy-boundary This module must only ever be reached via `import()`. The gate\n * fails if anything imports it statically, which would put media-chrome back in\n * the entry chunk and make the `lazy()` pointless.\n */\nimport { Button, ButtonGroup, ButtonGroupText } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n// A NAMED import (`import { MediaController } from \"media-chrome/react\"`) is a\n// static ESM binding a bundler must resolve at build time. Now that\n// `media-chrome` is a genuinely optional peer (issue #33), a consumer who has\n// not installed it hits that resolution at the worst possible time: Vite's own\n// optional-peer-dependency handling swaps in a build-time stub with no\n// exports, and Rollup's static named-export check then fails the WHOLE APP\n// BUILD — not a runtime error any `.catch()`/error boundary could ever see\n// (confirmed against `fixtures/consumer-smoke`'s real Vite build). A namespace\n// import defers every one of these to a plain property lookup, which Rollup\n// does not statically validate, so the build always succeeds; a genuinely\n// missing peer instead resolves the destructured names below to `undefined`,\n// and the guard turns that into a render-phase throw `LazyEngineBoundary`\n// already catches (see `persona.tsx`'s identical pattern).\nimport * as MediaChromeReactModule from \"media-chrome/react\";\nimport type { ComponentProps, CSSProperties } from \"react\";\n\nimport type { AssertAssignable } from \"./_lazy-boundary-conformance\";\nimport type {\n AudioPlayerControlBarProps,\n AudioPlayerDurationDisplayProps,\n AudioPlayerMuteButtonProps,\n AudioPlayerPlayButtonProps,\n AudioPlayerProps,\n AudioPlayerSeekBackwardButtonProps,\n AudioPlayerSeekForwardButtonProps,\n AudioPlayerTimeDisplayProps,\n AudioPlayerTimeRangeProps,\n AudioPlayerVolumeRangeProps,\n} from \"./audio-player\";\n\nconst {\n MediaControlBar,\n MediaController,\n MediaDurationDisplay,\n MediaMuteButton,\n MediaPlayButton,\n MediaSeekBackwardButton,\n MediaSeekForwardButton,\n MediaTimeDisplay,\n MediaTimeRange,\n MediaVolumeRange,\n} = MediaChromeReactModule;\n\n// The peer is genuinely absent (Vite's build-time stub, or any other bundler\n// that resolves an optional peer to an empty module) — surface a message\n// `isModuleNotFoundMessage` recognizes, so the caller's `renderMissing` still\n// gets the actionable \"install media-chrome\" copy rather than a raw\n// \"MediaController is not a valid JSX element\" crash.\nif (!MediaController) {\n throw new Error(\"Cannot find module 'media-chrome'\");\n}\n\nexport const AudioPlayer = ({ children, style, ...props }: AudioPlayerProps) => (\n <MediaController\n audio\n style={\n {\n \"--media-background-color\": \"transparent\",\n \"--media-button-icon-height\": \"1rem\",\n \"--media-button-icon-width\": \"1rem\",\n \"--media-control-background\": \"transparent\",\n \"--media-control-hover-background\": \"var(--color-accent)\",\n \"--media-control-padding\": \"0\",\n \"--media-font\": \"var(--font-sans)\",\n \"--media-font-size\": \"10px\",\n \"--media-icon-color\": \"currentColor\",\n \"--media-preview-time-background\": \"var(--color-background)\",\n \"--media-preview-time-border-radius\": \"var(--radius-md)\",\n \"--media-preview-time-text-shadow\": \"none\",\n \"--media-primary-color\": \"var(--color-primary)\",\n \"--media-range-bar-color\": \"var(--color-primary)\",\n \"--media-range-track-background\": \"var(--color-secondary)\",\n \"--media-secondary-color\": \"var(--color-secondary)\",\n \"--media-text-color\": \"var(--color-foreground)\",\n \"--media-tooltip-arrow-display\": \"none\",\n \"--media-tooltip-background\": \"var(--color-background)\",\n \"--media-tooltip-border-radius\": \"var(--radius-md)\",\n ...style,\n } as CSSProperties\n }\n {...props}\n >\n {children}\n </MediaController>\n);\n\nexport const AudioPlayerControlBar = ({ children, ...props }: AudioPlayerControlBarProps) => (\n <MediaControlBar {...props}>\n <ButtonGroup orientation=\"horizontal\">{children}</ButtonGroup>\n </MediaControlBar>\n);\n\nexport const AudioPlayerPlayButton = ({ className, ...props }: AudioPlayerPlayButtonProps) => (\n <Button asChild size=\"icon-sm\" variant=\"outline\">\n <MediaPlayButton className={cn(\"bg-transparent\", className)} {...props} />\n </Button>\n);\n\nexport const AudioPlayerSeekBackwardButton = ({\n seekOffset = 10,\n ...props\n}: AudioPlayerSeekBackwardButtonProps) => (\n <Button asChild size=\"icon-sm\" variant=\"outline\">\n <MediaSeekBackwardButton seekOffset={seekOffset} {...props} />\n </Button>\n);\n\nexport const AudioPlayerSeekForwardButton = ({\n seekOffset = 10,\n ...props\n}: AudioPlayerSeekForwardButtonProps) => (\n <Button asChild size=\"icon-sm\" variant=\"outline\">\n <MediaSeekForwardButton seekOffset={seekOffset} {...props} />\n </Button>\n);\n\nexport const AudioPlayerTimeDisplay = ({ className, ...props }: AudioPlayerTimeDisplayProps) => (\n <ButtonGroupText asChild className=\"bg-transparent\">\n <MediaTimeDisplay className={cn(\"tabular-nums\", className)} {...props} />\n </ButtonGroupText>\n);\n\nexport const AudioPlayerTimeRange = ({ className, ...props }: AudioPlayerTimeRangeProps) => (\n <ButtonGroupText asChild className=\"bg-transparent\">\n <MediaTimeRange className={cn(\"\", className)} {...props} />\n </ButtonGroupText>\n);\n\nexport const AudioPlayerDurationDisplay = ({\n className,\n ...props\n}: AudioPlayerDurationDisplayProps) => (\n <ButtonGroupText asChild className=\"bg-transparent\">\n <MediaDurationDisplay className={cn(\"tabular-nums\", className)} {...props} />\n </ButtonGroupText>\n);\n\nexport const AudioPlayerMuteButton = ({ className, ...props }: AudioPlayerMuteButtonProps) => (\n <ButtonGroupText asChild className=\"bg-transparent\">\n <MediaMuteButton className={cn(\"\", className)} {...props} />\n </ButtonGroupText>\n);\n\nexport const AudioPlayerVolumeRange = ({ className, ...props }: AudioPlayerVolumeRangeProps) => (\n <ButtonGroupText asChild className=\"bg-transparent\">\n <MediaVolumeRange className={cn(\"\", className)} {...props} />\n </ButtonGroupText>\n);\n\n/**\n * Conformance assertions (issue #101): compile-time proof that every OWNED\n * type declared in `audio-player.tsx` stays assignable to its REAL\n * `media-chrome/react` counterpart — this module still has the real values\n * (and therefore the real types) in scope, since it is the one place they are\n * allowed to be (see the module doc comment above). `AssertAssignable`'s type\n * parameter is constrained (`TOwned extends TReal`), so if a future\n * media-chrome release narrows an element's props in a way the owned type no\n * longer satisfies, ONE of these ten lines fails to typecheck — caught by\n * `pnpm --filter @elabs-ai/components-ai typecheck` locally, never shipped as\n * a silent mismatch to a consumer using the real component underneath.\n *\n * `AssertAssignable` itself is shared with `_persona-rive.tsx` via\n * `_lazy-boundary-conformance.ts` — see that module's doc comment for what\n * this check can and cannot prove (it is one-directional: it cannot catch the\n * owned type being NARROWER than the real one, which is exactly what these\n * owned types are — see the CHANGELOG's \"Breaking (types)\" entry).\n */\nexport type _AudioPlayerPropsConformance = AssertAssignable<\n AudioPlayerProps,\n Omit<ComponentProps<typeof MediaController>, \"audio\">\n>;\nexport type _AudioPlayerControlBarPropsConformance = AssertAssignable<\n AudioPlayerControlBarProps,\n ComponentProps<typeof MediaControlBar>\n>;\nexport type _AudioPlayerPlayButtonPropsConformance = AssertAssignable<\n AudioPlayerPlayButtonProps,\n ComponentProps<typeof MediaPlayButton>\n>;\nexport type _AudioPlayerSeekBackwardButtonPropsConformance = AssertAssignable<\n AudioPlayerSeekBackwardButtonProps,\n ComponentProps<typeof MediaSeekBackwardButton>\n>;\nexport type _AudioPlayerSeekForwardButtonPropsConformance = AssertAssignable<\n AudioPlayerSeekForwardButtonProps,\n ComponentProps<typeof MediaSeekForwardButton>\n>;\nexport type _AudioPlayerTimeDisplayPropsConformance = AssertAssignable<\n AudioPlayerTimeDisplayProps,\n ComponentProps<typeof MediaTimeDisplay>\n>;\nexport type _AudioPlayerTimeRangePropsConformance = AssertAssignable<\n AudioPlayerTimeRangeProps,\n ComponentProps<typeof MediaTimeRange>\n>;\nexport type _AudioPlayerDurationDisplayPropsConformance = AssertAssignable<\n AudioPlayerDurationDisplayProps,\n ComponentProps<typeof MediaDurationDisplay>\n>;\nexport type _AudioPlayerMuteButtonPropsConformance = AssertAssignable<\n AudioPlayerMuteButtonProps,\n ComponentProps<typeof MediaMuteButton>\n>;\nexport type _AudioPlayerVolumeRangePropsConformance = AssertAssignable<\n AudioPlayerVolumeRangeProps,\n ComponentProps<typeof MediaVolumeRange>\n>;\n"],"mappings":";;;;AAwBA,SAAS,QAAQ,aAAa,uBAAuB;AACrD,SAAS,UAAU;AAcnB,YAAY,4BAA4B;AAwCtC;AAvBF,IAAM;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,IAAI;AAOJ,IAAI,CAAC,iBAAiB;AACpB,QAAM,IAAI,MAAM,mCAAmC;AACrD;AAEO,IAAM,cAAc,CAAC,EAAE,UAAU,OAAO,GAAG,MAAM,MACtD;AAAA,EAAC;AAAA;AAAA,IACC,OAAK;AAAA,IACL,OACE;AAAA,MACE,4BAA4B;AAAA,MAC5B,8BAA8B;AAAA,MAC9B,6BAA6B;AAAA,MAC7B,8BAA8B;AAAA,MAC9B,oCAAoC;AAAA,MACpC,2BAA2B;AAAA,MAC3B,gBAAgB;AAAA,MAChB,qBAAqB;AAAA,MACrB,sBAAsB;AAAA,MACtB,mCAAmC;AAAA,MACnC,sCAAsC;AAAA,MACtC,oCAAoC;AAAA,MACpC,yBAAyB;AAAA,MACzB,2BAA2B;AAAA,MAC3B,kCAAkC;AAAA,MAClC,2BAA2B;AAAA,MAC3B,sBAAsB;AAAA,MACtB,iCAAiC;AAAA,MACjC,8BAA8B;AAAA,MAC9B,iCAAiC;AAAA,MACjC,GAAG;AAAA,IACL;AAAA,IAED,GAAG;AAAA,IAEH;AAAA;AACH;AAGK,IAAM,wBAAwB,CAAC,EAAE,UAAU,GAAG,MAAM,MACzD,oBAAC,mBAAiB,GAAG,OACnB,8BAAC,eAAY,aAAY,cAAc,UAAS,GAClD;AAGK,IAAM,wBAAwB,CAAC,EAAE,WAAW,GAAG,MAAM,MAC1D,oBAAC,UAAO,SAAO,MAAC,MAAK,WAAU,SAAQ,WACrC,8BAAC,mBAAgB,WAAW,GAAG,kBAAkB,SAAS,GAAI,GAAG,OAAO,GAC1E;AAGK,IAAM,gCAAgC,CAAC;AAAA,EAC5C,aAAa;AAAA,EACb,GAAG;AACL,MACE,oBAAC,UAAO,SAAO,MAAC,MAAK,WAAU,SAAQ,WACrC,8BAAC,2BAAwB,YAAyB,GAAG,OAAO,GAC9D;AAGK,IAAM,+BAA+B,CAAC;AAAA,EAC3C,aAAa;AAAA,EACb,GAAG;AACL,MACE,oBAAC,UAAO,SAAO,MAAC,MAAK,WAAU,SAAQ,WACrC,8BAAC,0BAAuB,YAAyB,GAAG,OAAO,GAC7D;AAGK,IAAM,yBAAyB,CAAC,EAAE,WAAW,GAAG,MAAM,MAC3D,oBAAC,mBAAgB,SAAO,MAAC,WAAU,kBACjC,8BAAC,oBAAiB,WAAW,GAAG,gBAAgB,SAAS,GAAI,GAAG,OAAO,GACzE;AAGK,IAAM,uBAAuB,CAAC,EAAE,WAAW,GAAG,MAAM,MACzD,oBAAC,mBAAgB,SAAO,MAAC,WAAU,kBACjC,8BAAC,kBAAe,WAAW,GAAG,IAAI,SAAS,GAAI,GAAG,OAAO,GAC3D;AAGK,IAAM,6BAA6B,CAAC;AAAA,EACzC;AAAA,EACA,GAAG;AACL,MACE,oBAAC,mBAAgB,SAAO,MAAC,WAAU,kBACjC,8BAAC,wBAAqB,WAAW,GAAG,gBAAgB,SAAS,GAAI,GAAG,OAAO,GAC7E;AAGK,IAAM,wBAAwB,CAAC,EAAE,WAAW,GAAG,MAAM,MAC1D,oBAAC,mBAAgB,SAAO,MAAC,WAAU,kBACjC,8BAAC,mBAAgB,WAAW,GAAG,IAAI,SAAS,GAAI,GAAG,OAAO,GAC5D;AAGK,IAAM,yBAAyB,CAAC,EAAE,WAAW,GAAG,MAAM,MAC3D,oBAAC,mBAAgB,SAAO,MAAC,WAAU,kBACjC,8BAAC,oBAAiB,WAAW,GAAG,IAAI,SAAS,GAAI,GAAG,OAAO,GAC7D;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/model-provider-logo.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport { resolveThemeIsDark } from \"@elabs-ai/components-tokens\";\nimport { BotIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { useEffect, useState } from \"react\";\n\n/**\n * Tracks the ACTIVE theme's own `color-scheme` (`resolveThemeIsDark`), not the\n * two shipped `light`/`dark` themes — a consumer-authored dark theme still\n * gets an inverted (visible) logo with no registration step (theming.md:\n * \"Use semantic tokens, not `dark:` — `@custom-variant dark` covers only the\n * themes shipped HERE\"). Mirrors `Toaster`'s `useDocumentThemeMode`\n * (`packages/ui/src/components/sonner/sonner.tsx`).\n */\nfunction useIsDarkTheme(): boolean {\n const [isDark, setIsDark] = useState(false);\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n const el = document.documentElement;\n const read = () => setIsDark(resolveThemeIsDark(el));\n read();\n const obs = new MutationObserver(read);\n obs.observe(el, { attributes: true, attributeFilter: [\"data-theme\"] });\n return () => obs.disconnect();\n }, []);\n return isDark;\n}\n\n/**\n * Where provider logos are fetched from by default.\n *\n * This is a REMOTE origin: a deployment with a restrictive `img-src` blocks it.\n * Self-host the SVGs and pass `src` (or set a `fallback`) — see\n * `docs/CSP-AND-NETWORK.md`.\n */\nexport const MODEL_PROVIDER_LOGO_BASE_URL = \"https://models.dev/logos\";\n\nexport type ModelProviderLogoProps = Omit<ComponentProps<\"img\">, \"alt\"> & {\n /**\n * Override the logo URL — point at a self-hosted or bundled asset when a CSP\n * blocks `models.dev`. Defaults to `${MODEL_PROVIDER_LOGO_BASE_URL}/<provider>.svg`.\n */\n src?: string;\n /**\n * Rendered when the logo fails to load (blocked, offline, or unknown\n * provider). Defaults to a neutral Lucide glyph, so the row never collapses\n * to a broken image.\n */\n fallback?: ReactNode;\n provider:\n | \"moonshotai-cn\"\n | \"lucidquery\"\n | \"moonshotai\"\n | \"zai-coding-plan\"\n | \"alibaba\"\n | \"xai\"\n | \"vultr\"\n | \"nvidia\"\n | \"upstage\"\n | \"groq\"\n | \"github-copilot\"\n | \"mistral\"\n | \"vercel\"\n | \"nebius\"\n | \"deepseek\"\n | \"alibaba-cn\"\n | \"google-vertex-anthropic\"\n | \"venice\"\n | \"chutes\"\n | \"cortecs\"\n | \"github-models\"\n | \"togetherai\"\n | \"azure\"\n | \"baseten\"\n | \"huggingface\"\n | \"opencode\"\n | \"fastrouter\"\n | \"google\"\n | \"google-vertex\"\n | \"cloudflare-workers-ai\"\n | \"inception\"\n | \"wandb\"\n | \"openai\"\n | \"zhipuai-coding-plan\"\n | \"perplexity\"\n | \"openrouter\"\n | \"zenmux\"\n | \"v0\"\n | \"iflowcn\"\n | \"synthetic\"\n | \"deepinfra\"\n | \"zhipuai\"\n | \"submodel\"\n | \"zai\"\n | \"inference\"\n | \"requesty\"\n | \"morph\"\n | \"lmstudio\"\n | \"anthropic\"\n | \"aihubmix\"\n | \"fireworks-ai\"\n | \"modelscope\"\n | \"llama\"\n | \"scaleway\"\n | \"amazon-bedrock\"\n | \"cerebras\"\n // oxlint-disable-next-line typescript-eslint(ban-types) -- intentional pattern for autocomplete-friendly string union\n | (string & {});\n};\n\n/**\n * The mark of an AI model provider (`anthropic`, `openai`, `google`, …), sized\n * for a row in a model list.\n *\n * **`ModelProviderLogo` vs `ServiceLogo` (`@elabs-ai/components-icons`).** An AI\n * provider mark with a zero-config remote default — name the provider and a logo\n * appears, no registration step — is this component. Any other third-party\n * service mark, resolved from a registry the app supplies and never fetched, is\n * `ServiceLogo`. The two are not interchangeable: `ServiceLogo` has no\n * per-instance `src` and no `onError`, and this one reaches a remote origin by\n * default (see `docs/CSP-AND-NETWORK.md`).\n */\nexport const ModelProviderLogo = ({\n provider,\n className,\n src,\n fallback,\n onError,\n ...props\n}: ModelProviderLogoProps) => {\n const [failed, setFailed] = useState(false);\n const isDark = useIsDarkTheme();\n\n // A blocked/missing logo must not leave a broken-image glyph in the row.\n if (failed) {\n return (\n <>\n {fallback ?? (\n <BotIcon aria-label={`${provider} logo`} className={cn(\"size-3\", className)} role=\"img\" />\n )}\n </>\n );\n }\n\n return (\n <img\n {...props}\n alt={`${provider} logo`}\n className={cn(\"size-3\", isDark && \"invert\", className)}\n height={12}\n onError={(event) => {\n setFailed(true);\n onError?.(event);\n }}\n // AFTER the spread, so the caller's `src` wins via the destructured\n // default rather than being silently overwritten.\n src={src ?? `${MODEL_PROVIDER_LOGO_BASE_URL}/${provider}.svg`}\n width={12}\n />\n );\n};\n\nexport type ModelProviderLogoGroupProps = ComponentProps<\"div\">;\n\nexport const ModelProviderLogoGroup = ({ className, ...props }: ModelProviderLogoGroupProps) => {\n // Same active-theme signal as the single logo above (not the two shipped\n // `light`/`dark` themes) — the backdrop swaps to `--foreground` so a\n // transparent-background SVG still reads against a dark chip in ANY dark\n // theme a consumer registers.\n const isDark = useIsDarkTheme();\n return (\n <div\n className={cn(\n \"flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:p-px [&>img]:ring-1\",\n isDark ? \"[&>img]:bg-foreground\" : \"[&>img]:bg-background\",\n className,\n )}\n {...props}\n />\n );\n};\n"],"mappings":";;;AAEA,SAAS,UAAU;AACnB,SAAS,0BAA0B;AACnC,SAAS,eAAe;AAExB,SAAS,WAAW,gBAAgB;AAoI9B,mBAEI,WAFJ;AA1HN,SAAS,iBAA0B;AACjC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,YAAU,MAAM;AACd,QAAI,OAAO,aAAa,YAAa;AACrC,UAAM,KAAK,SAAS;AACpB,UAAM,OAAO,MAAM,UAAU,mBAAmB,EAAE,CAAC;AACnD,SAAK;AACL,UAAM,MAAM,IAAI,iBAAiB,IAAI;AACrC,QAAI,QAAQ,IAAI,EAAE,YAAY,MAAM,iBAAiB,CAAC,YAAY,EAAE,CAAC;AACrE,WAAO,MAAM,IAAI,WAAW;AAAA,EAC9B,GAAG,CAAC,CAAC;AACL,SAAO;AACT;AASO,IAAM,+BAA+B;AAuFrC,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA8B;AAC5B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,SAAS,eAAe;AAG9B,MAAI,QAAQ;AACV,WACE,gCACG,sBACC,oBAAC,WAAQ,cAAY,GAAG,QAAQ,SAAS,WAAW,GAAG,UAAU,SAAS,GAAG,MAAK,OAAM,GAE5F;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,KAAK,GAAG,QAAQ;AAAA,MAChB,WAAW,GAAG,UAAU,UAAU,UAAU,SAAS;AAAA,MACrD,QAAQ;AAAA,MACR,SAAS,CAAC,UAAU;AAClB,kBAAU,IAAI;AACd,kBAAU,KAAK;AAAA,MACjB;AAAA,MAGA,KAAK,OAAO,GAAG,4BAA4B,IAAI,QAAQ;AAAA,MACvD,OAAO;AAAA;AAAA,EACT;AAEJ;AAIO,IAAM,yBAAyB,CAAC,EAAE,WAAW,GAAG,MAAM,MAAmC;AAK9F,QAAM,SAAS,eAAe;AAC9B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,SAAS,0BAA0B;AAAA,QACnC;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queue.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button } from \"@elabs-ai/components-ui\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@elabs-ai/components-ui\";\nimport { ScrollArea } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport { ChevronDownIcon, PaperclipIcon } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\n\nexport interface QueueMessagePart {\n type: string;\n text?: string;\n url?: string;\n filename?: string;\n mediaType?: string;\n}\n\nexport interface QueueMessage {\n id: string;\n parts: QueueMessagePart[];\n}\n\nexport interface QueueTodo {\n id: string;\n title: string;\n description?: string;\n status?: \"pending\" | \"completed\";\n}\n\nexport type QueueItemProps = ComponentProps<\"li\">;\n\nexport const QueueItem = ({ className, ...props }: QueueItemProps) => (\n <li\n className={cn(\n \"group flex flex-col gap-1 rounded-md px-3 py-1 text-body transition-colors hover:bg-muted\",\n className,\n )}\n {...props}\n />\n);\n\nexport type QueueItemIndicatorProps = ComponentProps<\"span\"> & {\n completed?: boolean;\n};\n\nexport const QueueItemIndicator = ({\n completed = false,\n className,\n ...props\n}: QueueItemIndicatorProps) => (\n <span\n className={cn(\n \"mt-0.5 inline-block size-2.5 rounded-full border\",\n completed\n ? \"border-muted-foreground/20 bg-muted-foreground/10\"\n : \"border-muted-foreground/50\",\n className,\n )}\n {...props}\n />\n);\n\nexport type QueueItemContentProps = ComponentProps<\"span\"> & {\n completed?: boolean;\n};\n\nexport const QueueItemContent = ({\n completed = false,\n className,\n ...props\n}: QueueItemContentProps) => (\n <span\n className={cn(\n \"line-clamp-1 grow break-words\",\n completed ? \"text-muted-foreground/50 line-through\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n />\n);\n\nexport type QueueItemDescriptionProps = ComponentProps<\"div\"> & {\n completed?: boolean;\n};\n\nexport const QueueItemDescription = ({\n completed = false,\n className,\n ...props\n}: QueueItemDescriptionProps) => (\n <div\n className={cn(\n \"ms-6 text-meta\",\n completed ? \"text-muted-foreground/40 line-through\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n />\n);\n\nexport type QueueItemActionsProps = ComponentProps<\"div\">;\n\nexport const QueueItemActions = ({ className, ...props }: QueueItemActionsProps) => (\n <div className={cn(\"flex gap-1\", className)} {...props} />\n);\n\nexport type QueueItemActionProps = Omit<ComponentProps<typeof Button>, \"variant\" | \"size\">;\n\nexport const QueueItemAction = ({ className, ...props }: QueueItemActionProps) => (\n <Button\n className={cn(\n \"size-auto rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-muted-foreground/10 hover:text-foreground group-hover:opacity-100\",\n className,\n )}\n size=\"icon\"\n type=\"button\"\n variant=\"ghost\"\n {...props}\n />\n);\n\nexport type QueueItemAttachmentProps = ComponentProps<\"div\">;\n\nexport const QueueItemAttachment = ({ className, ...props }: QueueItemAttachmentProps) => (\n <div className={cn(\"mt-1 flex flex-wrap gap-2\", className)} {...props} />\n);\n\nexport type QueueItemImageProps = ComponentProps<\"img\">;\n\nexport const QueueItemImage = ({ className, ...props }: QueueItemImageProps) => (\n <img\n alt=\"\"\n className={cn(\"h-8 w-8 rounded border object-cover\", className)}\n height={32}\n width={32}\n {...props}\n />\n);\n\nexport type QueueItemFileProps = ComponentProps<\"span\">;\n\nexport const QueueItemFile = ({ children, className, ...props }: QueueItemFileProps) => (\n <span\n className={cn(\"flex items-center gap-1 rounded border bg-muted px-2 py-1 text-meta\", className)}\n {...props}\n >\n <PaperclipIcon size={12} />\n <span className=\"max-w-[100px] truncate\">{children}</span>\n </span>\n);\n\nexport type QueueListProps = ComponentProps<typeof ScrollArea>;\n\nexport const QueueList = ({ children, className, ...props }: QueueListProps) => (\n <ScrollArea className={cn(\"mt-2 -mb-1\", className)} {...props}>\n <div className=\"max-h-40 pe-4\">\n <ul>{children}</ul>\n </div>\n </ScrollArea>\n);\n\n// QueueSection - collapsible section container\nexport type QueueSectionProps = ComponentProps<typeof Collapsible>;\n\nexport const QueueSection = ({ className, defaultOpen = true, ...props }: QueueSectionProps) => (\n <Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />\n);\n\n// QueueSectionTrigger - section header/trigger\nexport type QueueSectionTriggerProps = ComponentProps<\"button\">;\n\nexport const QueueSectionTrigger = ({\n children,\n className,\n ...props\n}: QueueSectionTriggerProps) => (\n <CollapsibleTrigger asChild>\n <button\n className={cn(\n \"group flex w-full items-center justify-between rounded-md bg-muted/40 px-3 py-2 text-start font-medium text-muted-foreground text-body transition-colors hover:bg-muted\",\n className,\n )}\n type=\"button\"\n {...props}\n >\n {children}\n </button>\n </CollapsibleTrigger>\n);\n\n// QueueSectionLabel - label content with icon and count\nexport type QueueSectionLabelProps = ComponentProps<\"span\"> & {\n count?: number;\n label: string;\n icon?: React.ReactNode;\n};\n\nexport const QueueSectionLabel = ({\n count,\n label,\n icon,\n className,\n ...props\n}: QueueSectionLabelProps) => (\n <span className={cn(\"flex items-center gap-2\", className)} {...props}>\n <ChevronDownIcon className=\"size-4 transition-transform group-data-[state=closed]:-rotate-90\" />\n {icon}\n <span>\n {count} {label}\n </span>\n </span>\n);\n\n// QueueSectionContent - collapsible content area\nexport type QueueSectionContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const QueueSectionContent = ({ className, ...props }: QueueSectionContentProps) => (\n <CollapsibleContent\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:slide-in-from-top-2 data-[state=closed]:slide-out-to-top-2 data-[state=closed]:fade-out-0 data-[state=open]:[--tw-ease:var(--ease-entrance)] data-[state=closed]:[--tw-ease:var(--ease-exit)] outline-none\",\n className,\n )}\n {...props}\n />\n);\n\nexport type QueueProps = ComponentProps<\"div\">;\n\nexport const Queue = ({ className, ...props }: QueueProps) => (\n <div\n className={cn(\n \"flex flex-col gap-2 rounded-xl border border-border bg-background px-3 pt-2 pb-2 shadow-xs\",\n className,\n )}\n {...props}\n />\n);\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/gallery.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Gallery — an OUTPUT-oriented image gallery for AI results / chat surfaces.\n *\n * A `Card` that renders a single image, or a thumbnail grid that collapses the\n * overflow into a \"+N more\" tile, and opens a unified lightbox `Dialog` with a\n * `Carousel` and a right-side metadata panel that tracks the active slide.\n *\n * Distinct from the other image surfaces in this package — don't merge them:\n * - `attachments.tsx` — composer INPUT (removable, file-typed thumbnails).\n * - `asset-preview.tsx` — a SINGLE asset in the context rail (type-switched).\n * - `image.tsx` — a base64 `<img>` wrapper for AI-SDK generated images.\n * Gallery is the multi-image lightbox: expand / download / carousel / metadata.\n *\n * The data model is framework-agnostic (plain `{ src, alt, … }`) — it does NOT\n * couple to the AI-SDK message model. Map a `UIMessage` part onto `GalleryImage`\n * in the app or a thin adapter.\n */\n\nimport {\n AspectRatio,\n Button,\n Card,\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogTitle,\n Skeleton,\n downloadUrl,\n type CarouselApi,\n} from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport { useLocale } from \"@elabs-ai/components-ui\";\nimport { useReducedMotion } from \"@elabs-ai/components-tokens\";\nimport { Download, ImageOff, Maximize2 } from \"lucide-react\";\nimport {\n createContext,\n forwardRef,\n use,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\n\n/** One labelled metadata row rendered in the detail panel `<dl>`. */\nexport interface GalleryMetaItem {\n /** Field label (rendered muted, in the `<dt>`). */\n label: string;\n /** Field value — a string in the common case; `ReactNode` allows a link/badge. */\n value: ReactNode;\n}\n\n/** A single gallery image plus its optional metadata. */\nexport interface GalleryImage {\n /** Full-resolution source shown in the lightbox and standalone tile. */\n src: string;\n /** Required alt text (a11y). Pass `\"\"` to mark the image decorative. */\n alt: string;\n /** Cheaper thumbnail for the grid; falls back to `src`. */\n thumbnailSrc?: string;\n /** Headline shown in the panel; also feeds the dialog's accessible name. */\n title?: string;\n /** Long-form copy shown under the title in the detail panel. */\n description?: string;\n /** Structured key/value rows rendered as a `<dl>` in the detail panel. */\n meta?: GalleryMetaItem[];\n /** Overrides `src` for the download (e.g. a signed full-res URL). */\n downloadUrl?: string;\n /** Suggested download filename. */\n downloadName?: string;\n}\n\nexport interface GalleryProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Images to display. `0` → empty state, `1` → standalone, `≥2` → grid. */\n images: GalleryImage[];\n /** Thumbnails to show before collapsing the rest into a \"+N more\" tile. @default 5 */\n maxVisible?: number;\n /** Aspect ratio for grid tiles and the standalone image. @default 1 (square) */\n aspectRatio?: number;\n /** Hide every download affordance (e.g. rights-restricted assets). @default false */\n hideDownload?: boolean;\n /**\n * The set is still arriving (loading-states.md `loading` signal). Renders\n * skeleton tiles in the grid footprint instead of the empty state, and fills\n * the gap up to `expectedCount` (or `maxVisible`) while images stream in.\n * @default false\n */\n loading?: boolean;\n /**\n * Total images expected once the set finishes arriving. When\n * `images.length < expectedCount`, the arrived tiles render alongside\n * skeleton tiles for the missing ones, so the grid fills in place (no reflow).\n */\n expectedCount?: number;\n /**\n * Replace the default metadata body in the detail panel. Receives the\n * currently-active image, so it stays in sync with the carousel. The panel\n * chrome + title heading remain.\n */\n renderMeta?: (image: GalleryImage, index: number) => ReactNode;\n}\n\ninterface GalleryContextValue {\n images: GalleryImage[];\n maxVisible: number;\n aspectRatio: number;\n hideDownload: boolean;\n /** The set is still arriving — render skeleton tiles to fill the gap. */\n incoming: boolean;\n /** How many skeleton tiles to append after the arrived images. */\n skeletonCount: number;\n renderMeta?: GalleryProps[\"renderMeta\"];\n state: { open: boolean; activeIndex: number };\n actions: {\n open: (index: number) => void;\n close: () => void;\n setActiveIndex: (index: number) => void;\n setOpen: (open: boolean) => void;\n };\n}\n\nconst GalleryContext = createContext<GalleryContextValue | null>(null);\n\nfunction useGallery(): GalleryContextValue {\n const ctx = use(GalleryContext);\n if (!ctx) throw new Error(\"useGallery must be used within <Gallery />\");\n return ctx;\n}\n\nfunction triggerDownload(image: GalleryImage): void {\n void downloadUrl(image.downloadUrl ?? image.src, image.downloadName);\n}\n\n/**\n * `<img>` with a `Skeleton` placeholder until it loads (loading-states.md — the\n * box is already reserved by `AspectRatio`, so no CLS) and a token-styled\n * fallback box when the source fails to load.\n */\nfunction GalleryImg({\n src,\n alt,\n className,\n loading,\n showSkeleton = true,\n}: {\n src: string;\n alt: string;\n className?: string;\n loading?: \"lazy\" | \"eager\";\n /**\n * Show the `Skeleton` placeholder until load. Only meaningful inside a\n * space-reserved box (`AspectRatio`); the lightbox sets `false` because the\n * slide isn't sized to the image, so an `absolute inset-0` skeleton would fill\n * the whole padded slide rather than the image footprint.\n */\n showSkeleton?: boolean;\n}) {\n const [failed, setFailed] = useState(false);\n const [loaded, setLoaded] = useState(false);\n const imgRef = useRef<HTMLImageElement>(null);\n\n // Cached images can finish before React attaches `onLoad` — read `complete`.\n useEffect(() => {\n if (imgRef.current?.complete) setLoaded(true);\n }, []);\n\n if (failed) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center gap-1 bg-muted text-muted-foreground\",\n className,\n )}\n {...(alt ? { role: \"img\", \"aria-label\": alt } : { \"aria-hidden\": true })}\n >\n <ImageOff className=\"size-6\" aria-hidden=\"true\" />\n </div>\n );\n }\n\n return (\n <>\n {showSkeleton && !loaded ? (\n <Skeleton className=\"absolute inset-0 size-full rounded-none\" />\n ) : null}\n <img\n ref={imgRef}\n src={src}\n alt={alt}\n loading={loading}\n onError={() => setFailed(true)}\n onLoad={() => setLoaded(true)}\n className={className}\n />\n </>\n );\n}\n\n/** Round, frosted icon button that downloads `image`. */\nfunction GalleryDownloadButton({\n image,\n className,\n tabIndex,\n}: {\n image: GalleryImage;\n className?: string;\n /** Off-screen carousel slides pass -1 so they're not phantom tab stops. */\n tabIndex?: number;\n}) {\n const { t } = useLocale();\n\n return (\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"outline\"\n tabIndex={tabIndex}\n aria-label={t(\"ai.gallery.downloadImage\")}\n className={cn(\"rounded-full bg-background/80 backdrop-blur-sm\", className)}\n onClick={(event) => {\n event.stopPropagation();\n triggerDownload(image);\n }}\n >\n <Download aria-hidden=\"true\" />\n </Button>\n );\n}\n\nconst HOVER_TOOLBAR =\n \"opacity-0 transition-opacity duration-fast ease-standard group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none\";\n\n// \"Comes closer\" hover for a tile: a gentle scale-up + drop shadow, plus a 3D\n// tilt that tracks the cursor. Movement (tilt + scale) is gated by\n// useReducedMotion — the shadow still responds (\"reduced != none\", like Card).\n// The wrapper carries `group relative` (toolbar reveal + absolute children) and\n// NO overflow, so the shadow and the inner control's focus ring aren't clipped.\nconst TILT_PERSPECTIVE = 800; // px — larger = subtler tilt\nconst TILT_MAX_DEG = 6;\nconst TILT_SCALE = 1.04;\nconst tiltTransform = (rotateX: number, rotateY: number, scale: number) =>\n `perspective(${TILT_PERSPECTIVE}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(${scale})`;\nconst TILT_REST = tiltTransform(0, 0, 1);\n\nfunction GalleryTilt({ className, children }: { className?: string; children: ReactNode }) {\n const reduced = useReducedMotion();\n const ref = useRef<HTMLDivElement>(null);\n const rect = useRef<DOMRect | null>(null);\n const raf = useRef<number | null>(null);\n\n useEffect(\n () => () => {\n if (raf.current != null) cancelAnimationFrame(raf.current);\n },\n [],\n );\n\n // Reduced motion: no tilt/scale; the shadow alone signals the lift.\n if (reduced) {\n return (\n <div\n className={cn(\n \"group relative rounded-lg transition-shadow duration-base ease-standard hover:z-10 hover:shadow-xl\",\n className,\n )}\n >\n {children}\n </div>\n );\n }\n\n const write = (transform: string) => {\n if (ref.current) ref.current.style.transform = transform;\n };\n\n return (\n <div\n ref={ref}\n style={{ transform: TILT_REST }}\n onPointerEnter={() => {\n // Cache geometry once per hover (no per-move layout read); grow on enter.\n rect.current = ref.current?.getBoundingClientRect() ?? null;\n write(tiltTransform(0, 0, TILT_SCALE));\n }}\n onPointerMove={(event) => {\n const r = rect.current;\n if (!r) return;\n const px = (event.clientX - r.left) / r.width - 0.5;\n const py = (event.clientY - r.top) / r.height - 0.5;\n if (raf.current != null) cancelAnimationFrame(raf.current);\n raf.current = requestAnimationFrame(() =>\n write(tiltTransform(-py * TILT_MAX_DEG, px * TILT_MAX_DEG, TILT_SCALE)),\n );\n }}\n onPointerLeave={() => {\n if (raf.current != null) cancelAnimationFrame(raf.current);\n write(TILT_REST);\n }}\n className={cn(\n \"group relative rounded-lg transition-[transform,box-shadow] duration-base ease-standard hover:z-10 hover:shadow-2xl\",\n className,\n )}\n >\n {children}\n </div>\n );\n}\n\nfunction GalleryEmpty() {\n const { t } = useLocale();\n return (\n <div className=\"flex min-h-32 flex-col items-center justify-center gap-2 p-6 text-center text-muted-foreground\">\n <ImageOff className=\"size-6\" aria-hidden=\"true\" />\n <p className=\"text-body\">{t(\"ai.gallery.noImages\")}</p>\n </div>\n );\n}\n\n/** The single-image branch — a framed image with an always-on expand button. */\nfunction GallerySingle() {\n const { t } = useLocale();\n const { images, aspectRatio, hideDownload, actions } = useGallery();\n const image = images[0];\n if (!image) return null;\n\n return (\n <GalleryTilt>\n <AspectRatio ratio={aspectRatio} className=\"overflow-hidden rounded-lg\">\n <GalleryImg\n src={image.src}\n alt={image.alt}\n loading=\"lazy\"\n className=\"size-full object-cover\"\n />\n </AspectRatio>\n <div className=\"absolute end-2 top-2 z-10 flex items-center gap-1\">\n {hideDownload ? null : <GalleryDownloadButton image={image} className={HOVER_TOOLBAR} />}\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"outline\"\n aria-label={t(\"ai.gallery.expandImage\")}\n className=\"rounded-full bg-background/80 backdrop-blur-sm\"\n onClick={() => actions.open(0)}\n >\n <Maximize2 aria-hidden=\"true\" />\n </Button>\n </div>\n </GalleryTilt>\n );\n}\n\n/** One thumbnail in the grid — a button that opens the lightbox at `index`. */\nfunction GalleryTile({ image, index }: { image: GalleryImage; index: number }) {\n const { images, aspectRatio, hideDownload, actions } = useGallery();\n const total = images.length;\n const label = image.title ?? (image.alt || `Image ${index + 1} of ${total}`);\n\n return (\n <GalleryTilt>\n <button\n type=\"button\"\n onClick={() => actions.open(index)}\n aria-label={label}\n className=\"block w-full rounded-lg focus-ring\"\n >\n <AspectRatio ratio={aspectRatio} className=\"overflow-hidden rounded-lg\">\n {/* Decorative: the button carries the accessible name. */}\n <GalleryImg\n src={image.thumbnailSrc ?? image.src}\n alt=\"\"\n loading=\"lazy\"\n className=\"size-full object-cover\"\n />\n </AspectRatio>\n </button>\n {hideDownload ? null : (\n <GalleryDownloadButton\n image={image}\n className={cn(\"absolute end-2 top-2 z-10\", HOVER_TOOLBAR)}\n />\n )}\n </GalleryTilt>\n );\n}\n\n/** The \"+N more\" tile — same footprint as a thumbnail; opens the carousel. */\nfunction GalleryOverflowTile() {\n const { images, maxVisible, aspectRatio, actions } = useGallery();\n const total = images.length;\n const count = total - maxVisible;\n\n return (\n <GalleryTilt>\n <button\n type=\"button\"\n onClick={() => actions.open(maxVisible)}\n aria-label={`Show all ${total} images`}\n className=\"block w-full rounded-lg border border-border bg-muted focus-ring\"\n >\n <AspectRatio ratio={aspectRatio} className=\"overflow-hidden rounded-lg\">\n <span className=\"flex size-full items-center justify-center text-subtitle text-muted-foreground transition-colors duration-fast ease-standard group-hover:text-foreground motion-reduce:transition-none\">\n +{count} more\n </span>\n </AspectRatio>\n </button>\n </GalleryTilt>\n );\n}\n\n/** A grid tile placeholder — same footprint as a thumbnail, shown while loading. */\nfunction GallerySkeletonTile() {\n const { aspectRatio } = useGallery();\n return (\n <AspectRatio ratio={aspectRatio} className=\"overflow-hidden rounded-lg\">\n <Skeleton className=\"size-full rounded-none\" />\n </AspectRatio>\n );\n}\n\n/**\n * The all-skeleton grid for the loading-from-empty case (no images yet).\n * Decorative — the loading announcement comes from the persistent live region on\n * the always-mounted `Card` (so it fires reliably; ARIA22).\n */\nfunction GallerySkeletonGrid() {\n const { skeletonCount } = useGallery();\n return (\n <div className=\"grid grid-cols-2 gap-2 sm:grid-cols-3\" aria-hidden=\"true\">\n {Array.from({ length: Math.max(1, skeletonCount) }).map((_, i) => (\n <GallerySkeletonTile key={`skeleton-${i}`} />\n ))}\n </div>\n );\n}\n\nfunction GalleryGrid() {\n const { images, maxVisible, incoming, skeletonCount } = useGallery();\n const total = images.length;\n\n // While the set is still arriving, render the arrived tiles plus skeleton\n // tiles for the missing ones — no \"+N more\" collapse until the set settles.\n if (incoming) {\n return (\n <div className=\"grid grid-cols-2 gap-2 sm:grid-cols-3\">\n {images.map((image, index) => (\n <GalleryTile key={`${image.src}-${index}`} image={image} index={index} />\n ))}\n {Array.from({ length: skeletonCount }).map((_, i) => (\n <GallerySkeletonTile key={`skeleton-${i}`} />\n ))}\n </div>\n );\n }\n\n const hasOverflow = total > maxVisible;\n const visible = hasOverflow ? images.slice(0, maxVisible) : images;\n\n return (\n <div className=\"grid grid-cols-2 gap-2 sm:grid-cols-3\">\n {visible.map((image, index) => (\n <GalleryTile key={`${image.src}-${index}`} image={image} index={index} />\n ))}\n {hasOverflow ? <GalleryOverflowTile /> : null}\n </div>\n );\n}\n\n/** Default detail-panel body: description + a `<dl>` of `meta` rows. */\nfunction GalleryDetailPanel({\n image,\n index,\n renderMeta,\n}: {\n image: GalleryImage;\n index: number;\n renderMeta?: GalleryProps[\"renderMeta\"];\n}) {\n const { t } = useLocale();\n const custom = renderMeta?.(image, index);\n if (custom != null) {\n return <div className=\"flex flex-col gap-4\">{custom}</div>;\n }\n\n const hasMeta = image.meta != null && image.meta.length > 0;\n const hasBody = Boolean(image.description) || hasMeta;\n\n if (!hasBody) {\n return <p className=\"text-body text-muted-foreground\">{t(\"ai.gallery.noDetails\")}</p>;\n }\n\n return (\n <div className=\"flex flex-col gap-4\">\n {image.description ? (\n <p className=\"text-body text-muted-foreground\">{image.description}</p>\n ) : null}\n {hasMeta ? (\n <dl className=\"flex flex-col gap-3\">\n {image.meta?.map((item, i) => (\n <div key={`${item.label}-${i}`} className=\"flex flex-col gap-0.5\">\n <dt className=\"text-meta text-muted-foreground\">{item.label}</dt>\n <dd className=\"text-body text-foreground\">{item.value}</dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n );\n}\n\n/** The lightbox carousel — one slide per image; syncs `activeIndex` on select. */\nfunction GalleryCarousel() {\n const { t } = useLocale();\n const { images, state, actions, hideDownload } = useGallery();\n const [api, setApi] = useState<CarouselApi>();\n const showArrows = images.length > 1;\n const { setActiveIndex } = actions;\n // Capture the open-at index ONCE (the modal remounts the carousel each open).\n // Tying opts.startIndex to the live activeIndex hands Embla a changed options\n // object on every slide (it deep-compares) → constant reInit → the native\n // slide animation never plays. A stable opts keeps Embla's scroll intact;\n // activeIndex flows one-way (carousel select → panel), never back into opts.\n const [startIndex] = useState(() => state.activeIndex);\n const opts = useMemo(() => ({ startIndex, loop: false }), [startIndex]);\n\n useEffect(() => {\n if (!api) return;\n const onSelect = () => setActiveIndex(api.selectedScrollSnap());\n onSelect();\n api.on(\"select\", onSelect);\n api.on(\"reInit\", onSelect);\n return () => {\n api.off(\"select\", onSelect);\n api.off(\"reInit\", onSelect);\n };\n }, [api, setActiveIndex]);\n\n // The dialog animates in (zoom-in-95); re-measure once laid out so Embla\n // aligns and lands on the opened slide.\n useEffect(() => {\n if (!api) return;\n const raf = requestAnimationFrame(() => api.reInit());\n return () => cancelAnimationFrame(raf);\n }, [api]);\n\n return (\n <Carousel\n setApi={setApi}\n opts={opts}\n className=\"relative w-full\"\n aria-label={t(\"ai.gallery.label\")}\n >\n <CarouselContent>\n {images.map((image, index) => (\n <CarouselItem\n key={`${image.src}-${index}`}\n aria-label={`${index + 1} of ${images.length}`}\n >\n <div className=\"group/slide relative flex items-center justify-center px-4\">\n <GalleryImg\n src={image.src}\n alt={image.alt}\n showSkeleton={false}\n className=\"max-h-[80vh] w-auto max-w-full object-contain\"\n />\n {hideDownload ? null : (\n <GalleryDownloadButton\n image={image}\n // Off-screen slides stay in the DOM (Embla); keep only the\n // active slide's download button in the tab order.\n tabIndex={index === state.activeIndex ? 0 : -1}\n className=\"absolute end-14 top-3 opacity-0 transition-opacity duration-fast ease-standard group-hover/slide:opacity-100 group-focus-within/slide:opacity-100 motion-reduce:transition-none\"\n />\n )}\n </div>\n </CarouselItem>\n ))}\n </CarouselContent>\n {showArrows ? (\n <>\n <CarouselPrevious className=\"start-4\" />\n <CarouselNext className=\"end-4\" />\n </>\n ) : null}\n </Carousel>\n );\n}\n\nfunction GalleryModal() {\n const { images, state, actions, renderMeta } = useGallery();\n const total = images.length;\n const active = images[state.activeIndex];\n\n const fallbackTitle = active\n ? active.alt || `Image ${state.activeIndex + 1} of ${total}`\n : \"Image preview\";\n\n return (\n <Dialog open={state.open} onOpenChange={actions.setOpen}>\n <DialogContent\n size=\"full\"\n className=\"grid grid-rows-[minmax(0,3fr)_minmax(0,2fr)] gap-0 overflow-hidden p-0 md:grid-cols-[minmax(0,1fr)_22rem] md:grid-rows-1\"\n >\n <div className=\"relative flex min-h-0 items-center justify-center overflow-hidden bg-muted\">\n {active ? <GalleryCarousel /> : null}\n </div>\n <aside className=\"flex min-h-0 flex-col overflow-y-auto border-border bg-card p-6 max-md:border-t md:border-s\">\n {active?.title ? (\n <DialogTitle className=\"pe-8 text-title\">{active.title}</DialogTitle>\n ) : (\n <DialogTitle className=\"sr-only\">{fallbackTitle}</DialogTitle>\n )}\n <DialogDescription className=\"sr-only\">\n {total > 1\n ? `Image ${state.activeIndex + 1} of ${total}. Use the arrow keys to browse.`\n : \"Image preview\"}\n </DialogDescription>\n {active ? (\n <div className=\"mt-4 flex-1\">\n <GalleryDetailPanel\n image={active}\n index={state.activeIndex}\n renderMeta={renderMeta}\n />\n </div>\n ) : null}\n </aside>\n {/* Carousel navigation only changes the title's text, which doesn't\n re-announce. A polite live region orients screen-reader users on each\n slide change. (sr-only is position:absolute, so it adds no grid cell.) */}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {active && total > 1\n ? `${active.title ?? active.alt} — image ${state.activeIndex + 1} of ${total}`\n : \"\"}\n </span>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport const Gallery = forwardRef<HTMLDivElement, GalleryProps>(function Gallery(\n {\n images,\n maxVisible = 5,\n aspectRatio = 1,\n hideDownload = false,\n loading = false,\n expectedCount,\n renderMeta,\n className,\n ...props\n },\n ref,\n) {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState(0);\n\n const openAt = useCallback((index: number) => {\n setActiveIndex(index);\n setOpen(true);\n }, []);\n const close = useCallback(() => setOpen(false), []);\n\n // The set is \"incoming\" while it's still arriving — either an explicit\n // `loading` flag, or fewer images than the `expectedCount`. Fill the grid up\n // to the target (capped at the visible footprint) with skeleton tiles.\n const target = expectedCount ?? (loading ? maxVisible : undefined);\n const incoming = target !== undefined && images.length < target;\n const skeletonCount = incoming ? Math.max(0, Math.min(target, maxVisible) - images.length) : 0;\n\n // Loading announcement: the live region must exist BEFORE its text appears\n // (ARIA22), so the `sr-only` status span below is always mounted and its label\n // is set one tick later via this effect — reliable on NVDA + Chrome.\n const loadingFromEmpty = incoming && images.length === 0;\n const [liveMsg, setLiveMsg] = useState(\"\");\n useEffect(() => {\n setLiveMsg(loadingFromEmpty ? \"Loading images\" : \"\");\n }, [loadingFromEmpty]);\n\n const value = useMemo<GalleryContextValue>(\n () => ({\n images,\n maxVisible,\n aspectRatio,\n hideDownload,\n incoming,\n skeletonCount,\n renderMeta,\n state: { open, activeIndex },\n actions: { open: openAt, close, setActiveIndex, setOpen },\n }),\n [\n images,\n maxVisible,\n aspectRatio,\n hideDownload,\n incoming,\n skeletonCount,\n renderMeta,\n open,\n activeIndex,\n openAt,\n close,\n ],\n );\n\n let body: ReactNode;\n if (images.length === 0) {\n body = incoming ? <GallerySkeletonGrid /> : <GalleryEmpty />;\n } else if (incoming) {\n body = <GalleryGrid />;\n } else if (images.length === 1) {\n body = <GallerySingle />;\n } else {\n body = <GalleryGrid />;\n }\n\n return (\n <GalleryContext.Provider value={value}>\n <Card ref={ref} className={cn(\"p-2\", className)} {...props}>\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {liveMsg}\n </span>\n {body}\n </Card>\n {images.length > 0 ? <GalleryModal /> : null}\n </GalleryContext.Provider>\n );\n});\n"],"mappings":";;;AAoBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,UAAU;AACnB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB;AACjC,SAAS,UAAU,UAAU,iBAAiB;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAqIC,SAMJ,UANI,KAMJ,YANI;AAtDR,IAAM,iBAAiB,cAA0C,IAAI;AAErE,SAAS,aAAkC;AACzC,QAAM,MAAM,IAAI,cAAc;AAC9B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,4CAA4C;AACtE,SAAO;AACT;AAEA,SAAS,gBAAgB,OAA2B;AAClD,OAAK,YAAY,MAAM,eAAe,MAAM,KAAK,MAAM,YAAY;AACrE;AAOA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AACjB,GAYG;AACD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,SAAS,OAAyB,IAAI;AAG5C,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,QACC,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,SACE,iCACG;AAAA,oBAAgB,CAAC,SAChB,oBAAC,YAAS,WAAU,2CAA0C,IAC5D;AAAA,IACJ;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,MAAM,UAAU,IAAI;AAAA,QAC7B,QAAQ,MAAM,UAAU,IAAI;AAAA,QAC5B;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAGA,SAAS,sBAAsB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,EAAE,EAAE,IAAI,UAAU;AAExB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,MAAK;AAAA,MACL,SAAQ;AAAA,MACR;AAAA,MACA,cAAY,EAAE,0BAA0B;AAAA,MACxC,WAAW,GAAG,kDAAkD,SAAS;AAAA,MACzE,SAAS,CAAC,UAAU;AAClB,cAAM,gBAAgB;AACtB,wBAAgB,KAAK;AAAA,MACvB;AAAA,MAEA,8BAAC,YAAS,eAAY,QAAO;AAAA;AAAA,EAC/B;AAEJ;AAEA,IAAM,gBACJ;AAOF,IAAM,mBAAmB;AACzB,IAAM,eAAe;AACrB,IAAM,aAAa;AACnB,IAAM,gBAAgB,CAAC,SAAiB,SAAiB,UACvD,eAAe,gBAAgB,eAAe,OAAO,gBAAgB,OAAO,cAAc,KAAK;AACjG,IAAM,YAAY,cAAc,GAAG,GAAG,CAAC;AAEvC,SAAS,YAAY,EAAE,WAAW,SAAS,GAAgD;AACzF,QAAM,UAAU,iBAAiB;AACjC,QAAM,MAAM,OAAuB,IAAI;AACvC,QAAM,OAAO,OAAuB,IAAI;AACxC,QAAM,MAAM,OAAsB,IAAI;AAEtC;AAAA,IACE,MAAM,MAAM;AACV,UAAI,IAAI,WAAW,KAAM,sBAAqB,IAAI,OAAO;AAAA,IAC3D;AAAA,IACA,CAAC;AAAA,EACH;AAGA,MAAI,SAAS;AACX,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,QAAQ,CAAC,cAAsB;AACnC,QAAI,IAAI,QAAS,KAAI,QAAQ,MAAM,YAAY;AAAA,EACjD;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,EAAE,WAAW,UAAU;AAAA,MAC9B,gBAAgB,MAAM;AAEpB,aAAK,UAAU,IAAI,SAAS,sBAAsB,KAAK;AACvD,cAAM,cAAc,GAAG,GAAG,UAAU,CAAC;AAAA,MACvC;AAAA,MACA,eAAe,CAAC,UAAU;AACxB,cAAM,IAAI,KAAK;AACf,YAAI,CAAC,EAAG;AACR,cAAM,MAAM,MAAM,UAAU,EAAE,QAAQ,EAAE,QAAQ;AAChD,cAAM,MAAM,MAAM,UAAU,EAAE,OAAO,EAAE,SAAS;AAChD,YAAI,IAAI,WAAW,KAAM,sBAAqB,IAAI,OAAO;AACzD,YAAI,UAAU;AAAA,UAAsB,MAClC,MAAM,cAAc,CAAC,KAAK,cAAc,KAAK,cAAc,UAAU,CAAC;AAAA,QACxE;AAAA,MACF;AAAA,MACA,gBAAgB,MAAM;AACpB,YAAI,IAAI,WAAW,KAAM,sBAAqB,IAAI,OAAO;AACzD,cAAM,SAAS;AAAA,MACjB;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;AAEA,SAAS,eAAe;AACtB,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,SACE,qBAAC,SAAI,WAAU,kGACb;AAAA,wBAAC,YAAS,WAAU,UAAS,eAAY,QAAO;AAAA,IAChD,oBAAC,OAAE,WAAU,aAAa,YAAE,qBAAqB,GAAE;AAAA,KACrD;AAEJ;AAGA,SAAS,gBAAgB;AACvB,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,EAAE,QAAQ,aAAa,cAAc,QAAQ,IAAI,WAAW;AAClE,QAAM,QAAQ,OAAO,CAAC;AACtB,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,qBAAC,eACC;AAAA,wBAAC,eAAY,OAAO,aAAa,WAAU,8BACzC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,MAAM;AAAA,QACX,KAAK,MAAM;AAAA,QACX,SAAQ;AAAA,QACR,WAAU;AAAA;AAAA,IACZ,GACF;AAAA,IACA,qBAAC,SAAI,WAAU,qDACZ;AAAA,qBAAe,OAAO,oBAAC,yBAAsB,OAAc,WAAW,eAAe;AAAA,MACtF;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,cAAY,EAAE,wBAAwB;AAAA,UACtC,WAAU;AAAA,UACV,SAAS,MAAM,QAAQ,KAAK,CAAC;AAAA,UAE7B,8BAAC,aAAU,eAAY,QAAO;AAAA;AAAA,MAChC;AAAA,OACF;AAAA,KACF;AAEJ;AAGA,SAAS,YAAY,EAAE,OAAO,MAAM,GAA2C;AAC7E,QAAM,EAAE,QAAQ,aAAa,cAAc,QAAQ,IAAI,WAAW;AAClE,QAAM,QAAQ,OAAO;AACrB,QAAM,QAAQ,MAAM,UAAU,MAAM,OAAO,SAAS,QAAQ,CAAC,OAAO,KAAK;AAEzE,SACE,qBAAC,eACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM,QAAQ,KAAK,KAAK;AAAA,QACjC,cAAY;AAAA,QACZ,WAAU;AAAA,QAEV,8BAAC,eAAY,OAAO,aAAa,WAAU,8BAEzC;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,MAAM,gBAAgB,MAAM;AAAA,YACjC,KAAI;AAAA,YACJ,SAAQ;AAAA,YACR,WAAU;AAAA;AAAA,QACZ,GACF;AAAA;AAAA,IACF;AAAA,IACC,eAAe,OACd;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW,GAAG,6BAA6B,aAAa;AAAA;AAAA,IAC1D;AAAA,KAEJ;AAEJ;AAGA,SAAS,sBAAsB;AAC7B,QAAM,EAAE,QAAQ,YAAY,aAAa,QAAQ,IAAI,WAAW;AAChE,QAAM,QAAQ,OAAO;AACrB,QAAM,QAAQ,QAAQ;AAEtB,SACE,oBAAC,eACC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,MAAM,QAAQ,KAAK,UAAU;AAAA,MACtC,cAAY,YAAY,KAAK;AAAA,MAC7B,WAAU;AAAA,MAEV,8BAAC,eAAY,OAAO,aAAa,WAAU,8BACzC,+BAAC,UAAK,WAAU,0LAAyL;AAAA;AAAA,QACrM;AAAA,QAAM;AAAA,SACV,GACF;AAAA;AAAA,EACF,GACF;AAEJ;AAGA,SAAS,sBAAsB;AAC7B,QAAM,EAAE,YAAY,IAAI,WAAW;AACnC,SACE,oBAAC,eAAY,OAAO,aAAa,WAAU,8BACzC,8BAAC,YAAS,WAAU,0BAAyB,GAC/C;AAEJ;AAOA,SAAS,sBAAsB;AAC7B,QAAM,EAAE,cAAc,IAAI,WAAW;AACrC,SACE,oBAAC,SAAI,WAAU,yCAAwC,eAAY,QAChE,gBAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,aAAa,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,MAC1D,oBAAC,yBAAyB,YAAY,CAAC,EAAI,CAC5C,GACH;AAEJ;AAEA,SAAS,cAAc;AACrB,QAAM,EAAE,QAAQ,YAAY,UAAU,cAAc,IAAI,WAAW;AACnE,QAAM,QAAQ,OAAO;AAIrB,MAAI,UAAU;AACZ,WACE,qBAAC,SAAI,WAAU,yCACZ;AAAA,aAAO,IAAI,CAAC,OAAO,UAClB,oBAAC,eAA0C,OAAc,SAAvC,GAAG,MAAM,GAAG,IAAI,KAAK,EAAgC,CACxE;AAAA,MACA,MAAM,KAAK,EAAE,QAAQ,cAAc,CAAC,EAAE,IAAI,CAAC,GAAG,MAC7C,oBAAC,yBAAyB,YAAY,CAAC,EAAI,CAC5C;AAAA,OACH;AAAA,EAEJ;AAEA,QAAM,cAAc,QAAQ;AAC5B,QAAM,UAAU,cAAc,OAAO,MAAM,GAAG,UAAU,IAAI;AAE5D,SACE,qBAAC,SAAI,WAAU,yCACZ;AAAA,YAAQ,IAAI,CAAC,OAAO,UACnB,oBAAC,eAA0C,OAAc,SAAvC,GAAG,MAAM,GAAG,IAAI,KAAK,EAAgC,CACxE;AAAA,IACA,cAAc,oBAAC,uBAAoB,IAAK;AAAA,KAC3C;AAEJ;AAGA,SAAS,mBAAmB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,SAAS,aAAa,OAAO,KAAK;AACxC,MAAI,UAAU,MAAM;AAClB,WAAO,oBAAC,SAAI,WAAU,uBAAuB,kBAAO;AAAA,EACtD;AAEA,QAAM,UAAU,MAAM,QAAQ,QAAQ,MAAM,KAAK,SAAS;AAC1D,QAAM,UAAU,QAAQ,MAAM,WAAW,KAAK;AAE9C,MAAI,CAAC,SAAS;AACZ,WAAO,oBAAC,OAAE,WAAU,mCAAmC,YAAE,sBAAsB,GAAE;AAAA,EACnF;AAEA,SACE,qBAAC,SAAI,WAAU,uBACZ;AAAA,UAAM,cACL,oBAAC,OAAE,WAAU,mCAAmC,gBAAM,aAAY,IAChE;AAAA,IACH,UACC,oBAAC,QAAG,WAAU,uBACX,gBAAM,MAAM,IAAI,CAAC,MAAM,MACtB,qBAAC,SAA+B,WAAU,yBACxC;AAAA,0BAAC,QAAG,WAAU,mCAAmC,eAAK,OAAM;AAAA,MAC5D,oBAAC,QAAG,WAAU,6BAA6B,eAAK,OAAM;AAAA,SAF9C,GAAG,KAAK,KAAK,IAAI,CAAC,EAG5B,CACD,GACH,IACE;AAAA,KACN;AAEJ;AAGA,SAAS,kBAAkB;AACzB,QAAM,EAAE,EAAE,IAAI,UAAU;AACxB,QAAM,EAAE,QAAQ,OAAO,SAAS,aAAa,IAAI,WAAW;AAC5D,QAAM,CAAC,KAAK,MAAM,IAAI,SAAsB;AAC5C,QAAM,aAAa,OAAO,SAAS;AACnC,QAAM,EAAE,eAAe,IAAI;AAM3B,QAAM,CAAC,UAAU,IAAI,SAAS,MAAM,MAAM,WAAW;AACrD,QAAM,OAAO,QAAQ,OAAO,EAAE,YAAY,MAAM,MAAM,IAAI,CAAC,UAAU,CAAC;AAEtE,YAAU,MAAM;AACd,QAAI,CAAC,IAAK;AACV,UAAM,WAAW,MAAM,eAAe,IAAI,mBAAmB,CAAC;AAC9D,aAAS;AACT,QAAI,GAAG,UAAU,QAAQ;AACzB,QAAI,GAAG,UAAU,QAAQ;AACzB,WAAO,MAAM;AACX,UAAI,IAAI,UAAU,QAAQ;AAC1B,UAAI,IAAI,UAAU,QAAQ;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,KAAK,cAAc,CAAC;AAIxB,YAAU,MAAM;AACd,QAAI,CAAC,IAAK;AACV,UAAM,MAAM,sBAAsB,MAAM,IAAI,OAAO,CAAC;AACpD,WAAO,MAAM,qBAAqB,GAAG;AAAA,EACvC,GAAG,CAAC,GAAG,CAAC;AAER,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,WAAU;AAAA,MACV,cAAY,EAAE,kBAAkB;AAAA,MAEhC;AAAA,4BAAC,mBACE,iBAAO,IAAI,CAAC,OAAO,UAClB;AAAA,UAAC;AAAA;AAAA,YAEC,cAAY,GAAG,QAAQ,CAAC,OAAO,OAAO,MAAM;AAAA,YAE5C,+BAAC,SAAI,WAAU,8DACb;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,KAAK,MAAM;AAAA,kBACX,KAAK,MAAM;AAAA,kBACX,cAAc;AAAA,kBACd,WAAU;AAAA;AAAA,cACZ;AAAA,cACC,eAAe,OACd;AAAA,gBAAC;AAAA;AAAA,kBACC;AAAA,kBAGA,UAAU,UAAU,MAAM,cAAc,IAAI;AAAA,kBAC5C,WAAU;AAAA;AAAA,cACZ;AAAA,eAEJ;AAAA;AAAA,UAnBK,GAAG,MAAM,GAAG,IAAI,KAAK;AAAA,QAoB5B,CACD,GACH;AAAA,QACC,aACC,iCACE;AAAA,8BAAC,oBAAiB,WAAU,WAAU;AAAA,UACtC,oBAAC,gBAAa,WAAU,SAAQ;AAAA,WAClC,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,eAAe;AACtB,QAAM,EAAE,QAAQ,OAAO,SAAS,WAAW,IAAI,WAAW;AAC1D,QAAM,QAAQ,OAAO;AACrB,QAAM,SAAS,OAAO,MAAM,WAAW;AAEvC,QAAM,gBAAgB,SAClB,OAAO,OAAO,SAAS,MAAM,cAAc,CAAC,OAAO,KAAK,KACxD;AAEJ,SACE,oBAAC,UAAO,MAAM,MAAM,MAAM,cAAc,QAAQ,SAC9C;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAU;AAAA,MAEV;AAAA,4BAAC,SAAI,WAAU,8EACZ,mBAAS,oBAAC,mBAAgB,IAAK,MAClC;AAAA,QACA,qBAAC,WAAM,WAAU,+FACd;AAAA,kBAAQ,QACP,oBAAC,eAAY,WAAU,mBAAmB,iBAAO,OAAM,IAEvD,oBAAC,eAAY,WAAU,WAAW,yBAAc;AAAA,UAElD,oBAAC,qBAAkB,WAAU,WAC1B,kBAAQ,IACL,SAAS,MAAM,cAAc,CAAC,OAAO,KAAK,oCAC1C,iBACN;AAAA,UACC,SACC,oBAAC,SAAI,WAAU,eACb;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,OAAO,MAAM;AAAA,cACb;AAAA;AAAA,UACF,GACF,IACE;AAAA,WACN;AAAA,QAIA,oBAAC,UAAK,WAAU,WAAU,MAAK,UAAS,aAAU,UAC/C,oBAAU,QAAQ,IACf,GAAG,OAAO,SAAS,OAAO,GAAG,iBAAY,MAAM,cAAc,CAAC,OAAO,KAAK,KAC1E,IACN;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;AAEO,IAAM,UAAU,WAAyC,SAASA,SACvE;AAAA,EACE;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd,eAAe;AAAA,EACf,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAEhD,QAAM,SAAS,YAAY,CAAC,UAAkB;AAC5C,mBAAe,KAAK;AACpB,YAAQ,IAAI;AAAA,EACd,GAAG,CAAC,CAAC;AACL,QAAM,QAAQ,YAAY,MAAM,QAAQ,KAAK,GAAG,CAAC,CAAC;AAKlD,QAAM,SAAS,kBAAkB,UAAU,aAAa;AACxD,QAAM,WAAW,WAAW,UAAa,OAAO,SAAS;AACzD,QAAM,gBAAgB,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,UAAU,IAAI,OAAO,MAAM,IAAI;AAK7F,QAAM,mBAAmB,YAAY,OAAO,WAAW;AACvD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,EAAE;AACzC,YAAU,MAAM;AACd,eAAW,mBAAmB,mBAAmB,EAAE;AAAA,EACrD,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,MAAM,YAAY;AAAA,MAC3B,SAAS,EAAE,MAAM,QAAQ,OAAO,gBAAgB,QAAQ;AAAA,IAC1D;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO,WAAW,oBAAC,uBAAoB,IAAK,oBAAC,gBAAa;AAAA,EAC5D,WAAW,UAAU;AACnB,WAAO,oBAAC,eAAY;AAAA,EACtB,WAAW,OAAO,WAAW,GAAG;AAC9B,WAAO,oBAAC,iBAAc;AAAA,EACxB,OAAO;AACL,WAAO,oBAAC,eAAY;AAAA,EACtB;AAEA,SACE,qBAAC,eAAe,UAAf,EAAwB,OACvB;AAAA,yBAAC,QAAK,KAAU,WAAW,GAAG,OAAO,SAAS,GAAI,GAAG,OACnD;AAAA,0BAAC,UAAK,WAAU,WAAU,MAAK,UAAS,aAAU,UAC/C,mBACH;AAAA,MACC;AAAA,OACH;AAAA,IACC,OAAO,SAAS,IAAI,oBAAC,gBAAa,IAAK;AAAA,KAC1C;AAEJ,CAAC;","names":["Gallery"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/attachments.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button } from \"@elabs-ai/components-ui\";\nimport { HoverCard, HoverCardContent, HoverCardTrigger } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport type { FileUIPart, SourceDocumentUIPart } from \"ai\";\nimport {\n FileTextIcon,\n GlobeIcon,\n ImageIcon,\n Music2Icon,\n PaperclipIcon,\n VideoIcon,\n XIcon,\n} from \"lucide-react\";\nimport type { ComponentProps, HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, useCallback, useContext, useMemo } from \"react\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type AttachmentData =\n | (FileUIPart & { id: string })\n | (SourceDocumentUIPart & { id: string });\n\nexport type AttachmentMediaCategory =\n | \"image\"\n | \"video\"\n | \"audio\"\n | \"document\"\n | \"source\"\n | \"unknown\";\n\nexport type AttachmentVariant = \"grid\" | \"inline\" | \"list\";\n\nconst mediaCategoryIcons: Record<AttachmentMediaCategory, typeof ImageIcon> = {\n audio: Music2Icon,\n document: FileTextIcon,\n image: ImageIcon,\n source: GlobeIcon,\n unknown: PaperclipIcon,\n video: VideoIcon,\n};\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\nexport const getMediaCategory = (data: AttachmentData): AttachmentMediaCategory => {\n if (data.type === \"source-document\") {\n return \"source\";\n }\n\n const mediaType = data.mediaType ?? \"\";\n\n if (mediaType.startsWith(\"image/\")) {\n return \"image\";\n }\n if (mediaType.startsWith(\"video/\")) {\n return \"video\";\n }\n if (mediaType.startsWith(\"audio/\")) {\n return \"audio\";\n }\n if (mediaType.startsWith(\"application/\") || mediaType.startsWith(\"text/\")) {\n return \"document\";\n }\n\n return \"unknown\";\n};\n\nexport const getAttachmentLabel = (data: AttachmentData): string => {\n if (data.type === \"source-document\") {\n return data.title || data.filename || \"Source\";\n }\n\n const category = getMediaCategory(data);\n return data.filename || (category === \"image\" ? \"Image\" : \"Attachment\");\n};\n\nconst renderAttachmentImage = (url: string, filename: string | undefined, isGrid: boolean) =>\n isGrid ? (\n <img\n alt={filename || \"Image\"}\n className=\"size-full object-cover\"\n height={96}\n src={url}\n width={96}\n />\n ) : (\n <img\n alt={filename || \"Image\"}\n className=\"size-full rounded object-cover\"\n height={20}\n src={url}\n width={20}\n />\n );\n\n// ============================================================================\n// Contexts\n// ============================================================================\n\ninterface AttachmentsContextValue {\n variant: AttachmentVariant;\n}\n\nconst AttachmentsContext = createContext<AttachmentsContextValue | null>(null);\n\ninterface AttachmentContextValue {\n data: AttachmentData;\n mediaCategory: AttachmentMediaCategory;\n onRemove?: () => void;\n variant: AttachmentVariant;\n}\n\nconst AttachmentContext = createContext<AttachmentContextValue | null>(null);\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nexport const useAttachmentsContext = () =>\n useContext(AttachmentsContext) ?? { variant: \"grid\" as const };\n\nexport const useAttachmentContext = () => {\n const ctx = useContext(AttachmentContext);\n if (!ctx) {\n throw new Error(\"Attachment components must be used within <Attachment>\");\n }\n return ctx;\n};\n\n// ============================================================================\n// Attachments - Container\n// ============================================================================\n\nexport type AttachmentsProps = HTMLAttributes<HTMLDivElement> & {\n variant?: AttachmentVariant;\n};\n\nexport const Attachments = ({\n variant = \"grid\",\n className,\n children,\n ...props\n}: AttachmentsProps) => {\n const contextValue = useMemo(() => ({ variant }), [variant]);\n\n return (\n <AttachmentsContext.Provider value={contextValue}>\n <div\n className={cn(\n \"flex items-start\",\n variant === \"list\" ? \"flex-col gap-2\" : \"flex-wrap gap-2\",\n variant === \"grid\" && \"ms-auto w-fit\",\n className,\n )}\n {...props}\n >\n {children}\n </div>\n </AttachmentsContext.Provider>\n );\n};\n\n// ============================================================================\n// Attachment - Item\n// ============================================================================\n\nexport type AttachmentProps = HTMLAttributes<HTMLDivElement> & {\n data: AttachmentData;\n onRemove?: () => void;\n};\n\nexport const Attachment = ({ data, onRemove, className, children, ...props }: AttachmentProps) => {\n const { variant } = useAttachmentsContext();\n const mediaCategory = getMediaCategory(data);\n\n const contextValue = useMemo<AttachmentContextValue>(\n () => ({ data, mediaCategory, onRemove, variant }),\n [data, mediaCategory, onRemove, variant],\n );\n\n return (\n <AttachmentContext.Provider value={contextValue}>\n <div\n className={cn(\n \"group relative\",\n variant === \"grid\" && \"size-24 overflow-hidden rounded-lg\",\n variant === \"inline\" && [\n \"flex h-8 cursor-pointer select-none items-center gap-1.5\",\n \"rounded-md border border-border px-1.5\",\n \"font-medium text-body transition-colors duration-fast ease-standard motion-reduce:transition-none\",\n \"hover:bg-accent/50 hover:text-accent-foreground\",\n ],\n variant === \"list\" && [\n \"flex w-full items-center gap-3 rounded-lg border p-3\",\n \"hover:bg-accent/50\",\n ],\n className,\n )}\n {...props}\n >\n {children}\n </div>\n </AttachmentContext.Provider>\n );\n};\n\n// ============================================================================\n// AttachmentPreview - Media preview\n// ============================================================================\n\nexport type AttachmentPreviewProps = HTMLAttributes<HTMLDivElement> & {\n fallbackIcon?: ReactNode;\n};\n\nexport const AttachmentPreview = ({\n fallbackIcon,\n className,\n ...props\n}: AttachmentPreviewProps) => {\n const { data, mediaCategory, variant } = useAttachmentContext();\n\n const iconSize = variant === \"inline\" ? \"size-3\" : \"size-4\";\n\n const renderIcon = (Icon: typeof ImageIcon) => (\n <Icon className={cn(iconSize, \"text-muted-foreground\")} />\n );\n\n const renderContent = () => {\n if (mediaCategory === \"image\" && data.type === \"file\" && data.url) {\n return renderAttachmentImage(data.url, data.filename, variant === \"grid\");\n }\n\n if (mediaCategory === \"video\" && data.type === \"file\" && data.url) {\n return <video className=\"size-full object-cover\" muted src={data.url} />;\n }\n\n const Icon = mediaCategoryIcons[mediaCategory];\n return fallbackIcon ?? renderIcon(Icon);\n };\n\n return (\n <div\n className={cn(\n \"flex shrink-0 items-center justify-center overflow-hidden\",\n variant === \"grid\" && \"size-full bg-muted\",\n variant === \"inline\" && \"size-5 rounded bg-background\",\n variant === \"list\" && \"size-12 rounded bg-muted\",\n className,\n )}\n {...props}\n >\n {renderContent()}\n </div>\n );\n};\n\n// ============================================================================\n// AttachmentInfo - Name and type display\n// ============================================================================\n\nexport type AttachmentInfoProps = HTMLAttributes<HTMLDivElement> & {\n showMediaType?: boolean;\n};\n\nexport const AttachmentInfo = ({\n showMediaType = false,\n className,\n ...props\n}: AttachmentInfoProps) => {\n const { data, variant } = useAttachmentContext();\n const label = getAttachmentLabel(data);\n\n if (variant === \"grid\") {\n return null;\n }\n\n return (\n <div className={cn(\"min-w-0 flex-1\", className)} {...props}>\n <span className=\"block truncate\">{label}</span>\n {showMediaType && data.mediaType && (\n <span className=\"block truncate text-muted-foreground text-meta\">{data.mediaType}</span>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// AttachmentRemove - Remove button\n// ============================================================================\n\nexport type AttachmentRemoveProps = ComponentProps<typeof Button> & {\n label?: string;\n};\n\nexport const AttachmentRemove = ({\n label = \"Remove\",\n className,\n children,\n ...props\n}: AttachmentRemoveProps) => {\n const { onRemove, variant } = useAttachmentContext();\n\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation();\n onRemove?.();\n },\n [onRemove],\n );\n\n if (!onRemove) {\n return null;\n }\n\n return (\n <Button\n aria-label={label}\n className={cn(\n variant === \"grid\" && [\n \"absolute top-2 end-2 size-6 rounded-full p-0\",\n \"bg-background/80 backdrop-blur-sm\",\n \"opacity-0 transition-opacity group-hover:opacity-100\",\n \"hover:bg-background\",\n \"[&>svg]:size-3\",\n ],\n variant === \"inline\" && [\n \"size-5 rounded p-0\",\n \"opacity-0 transition-opacity group-hover:opacity-100\",\n \"[&>svg]:size-2.5\",\n ],\n variant === \"list\" && [\"size-8 shrink-0 rounded p-0\", \"[&>svg]:size-4\"],\n className,\n )}\n onClick={handleClick}\n type=\"button\"\n variant=\"ghost\"\n {...props}\n >\n {children ?? <XIcon />}\n <span className=\"sr-only\">{label}</span>\n </Button>\n );\n};\n\n// ============================================================================\n// AttachmentHoverCard - Hover preview\n// ============================================================================\n\nexport type AttachmentHoverCardProps = ComponentProps<typeof HoverCard>;\n\nexport const AttachmentHoverCard = ({\n openDelay = 0,\n closeDelay = 0,\n ...props\n}: AttachmentHoverCardProps) => (\n <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />\n);\n\nexport type AttachmentHoverCardTriggerProps = ComponentProps<typeof HoverCardTrigger>;\n\nexport const AttachmentHoverCardTrigger = (props: AttachmentHoverCardTriggerProps) => (\n <HoverCardTrigger {...props} />\n);\n\nexport type AttachmentHoverCardContentProps = ComponentProps<typeof HoverCardContent>;\n\nexport const AttachmentHoverCardContent = ({\n align = \"start\",\n className,\n ...props\n}: AttachmentHoverCardContentProps) => (\n <HoverCardContent align={align} className={cn(\"w-auto p-2\", className)} {...props} />\n);\n\n// ============================================================================\n// AttachmentEmpty - Empty state\n// ============================================================================\n\nexport type AttachmentEmptyProps = HTMLAttributes<HTMLDivElement>;\n\nexport const AttachmentEmpty = ({ className, children, ...props }: AttachmentEmptyProps) => (\n <div\n className={cn(\n \"flex items-center justify-center p-4 text-muted-foreground text-body\",\n className,\n )}\n {...props}\n >\n {children ?? \"No attachments\"}\n </div>\n);\n"],"mappings":"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","names":[]}