@chai-ui/react 0.6.0 → 0.7.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 (47) hide show
  1. package/DESIGN.md +19 -3
  2. package/dist/ChaiProvider.d.ts +3 -42
  3. package/dist/ChaiProvider.d.ts.map +1 -1
  4. package/dist/ChaiProvider.js +5 -43
  5. package/dist/ChaiProvider.js.map +1 -1
  6. package/dist/Composer.d.ts +4 -59
  7. package/dist/Composer.d.ts.map +1 -1
  8. package/dist/Composer.js +50 -169
  9. package/dist/Composer.js.map +1 -1
  10. package/dist/EditCard.d.ts +2 -16
  11. package/dist/EditCard.d.ts.map +1 -1
  12. package/dist/EditCard.js +16 -115
  13. package/dist/EditCard.js.map +1 -1
  14. package/dist/MediaAnalyzer.d.ts +3 -49
  15. package/dist/MediaAnalyzer.d.ts.map +1 -1
  16. package/dist/MediaAnalyzer.js +13 -141
  17. package/dist/MediaAnalyzer.js.map +1 -1
  18. package/dist/ResultCard.d.ts +13 -0
  19. package/dist/ResultCard.d.ts.map +1 -1
  20. package/dist/ResultCard.js +2 -48
  21. package/dist/ResultCard.js.map +1 -1
  22. package/dist/useComposer.d.ts +11 -86
  23. package/dist/useComposer.d.ts.map +1 -1
  24. package/dist/useComposer.js +26 -291
  25. package/dist/useComposer.js.map +1 -1
  26. package/dist/useMediaAnalyzer.d.ts +8 -28
  27. package/dist/useMediaAnalyzer.d.ts.map +1 -1
  28. package/dist/useMediaAnalyzer.js +9 -106
  29. package/dist/useMediaAnalyzer.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/ChaiProvider.tsx +12 -70
  32. package/src/Composer.tsx +73 -258
  33. package/src/EditCard.tsx +46 -139
  34. package/src/MediaAnalyzer.tsx +31 -176
  35. package/src/ResultCard.tsx +13 -38
  36. package/src/useComposer.ts +33 -411
  37. package/src/useMediaAnalyzer.ts +15 -146
  38. package/dist/media-actions.d.ts +0 -18
  39. package/dist/media-actions.d.ts.map +0 -1
  40. package/dist/media-actions.js +0 -56
  41. package/dist/media-actions.js.map +0 -1
  42. package/dist/regions.d.ts +0 -3
  43. package/dist/regions.d.ts.map +0 -1
  44. package/dist/regions.js +0 -7
  45. package/dist/regions.js.map +0 -1
  46. package/src/media-actions.ts +0 -54
  47. package/src/regions.ts +0 -7
package/DESIGN.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # CHAI UI — DESIGN.md
2
2
 
3
- The rules for building with `@chai-ui/react`, written for the people and
4
- the coding agents who integrate it. The API reference is the JSDoc on each
3
+ The rules for building with `@chai-ui/react` or `@chai-ui/vue`, written for
4
+ the people and the coding agents who integrate them. Both packages ship
5
+ this file. It uses React's names; "In Vue" below maps them. The API reference is the JSDoc on each
5
6
  component's props and the docs site's `llms.txt`. This file covers how to
6
7
  use the pieces correctly: which component to pick, which tokens to use,
7
8
  which states you get for free, and what not to build.
@@ -28,7 +29,8 @@ use these names: `Results`, `Compare`, `ModelPicker`,
28
29
  ## 2. Integration rules
29
30
 
30
31
  1. **Load the CSS in order:** `@chai-ui/tokens/css` first, then
31
- `@chai-ui/react/style.css`. Component styles read the token variables.
32
+ `@chai-ui/react/style.css` (or `@chai-ui/vue/style.css`). Component
33
+ styles read the token variables.
32
34
  2. **Everything is controlled.** Every value comes in as a prop and every
33
35
  change comes out as a callback (`value` / `onChange`). Keep the state in
34
36
  your app.
@@ -59,6 +61,20 @@ use these names: `Results`, `Compare`, `ModelPicker`,
59
61
  Don't show model rankings or leaderboard positions you can't keep up to
60
62
  date. Don't show a control the selected model can't honor.
61
63
 
64
+ ### In Vue
65
+
66
+ The same components, rules and behavior, with Vue's names:
67
+
68
+ - `value` / `onChange` is `v-model`; every other `x` / `onXChange` pair is
69
+ `v-model:x` (`v-model:attachments`, `v-model:model-id`, `v-model:regions`, …).
70
+ - Other callbacks are events: `@submit`, `@action`, `@enhance`, `@abort`.
71
+ Rule 4 holds: a control that needs a callback shows only with its
72
+ listener (`@enhance` for the optimize button, `@abort` for stop,
73
+ `@update:model-id` for the Model menu).
74
+ - `useComposer` and `useMediaAnalyzer` are composables returning refs.
75
+ Pass a getter for options that change: `useComposer(() => ({ engine, editImage: image.value }))`.
76
+ - `ChaiProvider` is a component with the same props.
77
+
62
78
  ## 3. Material components, and where Chai uses its own
63
79
 
64
80
  Chai builds on Material Design 3's web components where they fit, and
@@ -1,49 +1,10 @@
1
1
  import { type ReactNode } from "react";
2
- import { type DroppedMedia, type GenerationEngine, type MediaKind } from "@chai-ui/core";
3
- /**
4
- * The suggested reasoning model for text and images: prompt enhancement,
5
- * auto-select routing and image analysis. A default, not a requirement —
6
- * pass any model id `reasoningEngine` can run.
7
- */
8
- export declare const DEFAULT_REASONING_MODEL = "anthropic/claude-opus-5";
9
- /**
10
- * Suggested per-kind overrides for media Claude can't read. Gemini takes
11
- * video and audio directly. For a cheaper option, swap in
12
- * `"qwen/qwen3.8-omni-flash"`, which takes both too.
13
- */
14
- export declare const DEFAULT_REASONING_MODEL_BY_KIND: Partial<Record<MediaKind, string>>;
15
- /** The reasoning engine and models every CHAI component shares. */
16
- export interface Reasoning {
17
- engine: GenerationEngine;
18
- /** The model for text and any attachment kind without its own entry in `modelByKind`. */
19
- model: string;
20
- modelByKind: Partial<Record<MediaKind, string>>;
21
- }
22
- export interface ChaiProviderProps {
23
- /** Text, image and routing model. Defaults to `DEFAULT_REASONING_MODEL`. */
24
- reasoningModel?: string;
25
- /**
26
- * Per-attachment-kind models, merged over `DEFAULT_REASONING_MODEL_BY_KIND`
27
- * one kind at a time, e.g. `{ video: "qwen/qwen3.8-omni-flash" }` keeps
28
- * the default for audio. Map a kind to `reasoningModel`'s value to use
29
- * one model for everything.
30
- */
31
- reasoningModelByKind?: Partial<Record<MediaKind, string>>;
32
- /**
33
- * The engine the reasoning models run on. Defaults to OpenRouter,
34
- * independent of the generation engine: Fal's own LLM access is an
35
- * OpenRouter proxy, so defaulting to it would double-proxy.
36
- */
37
- reasoningEngine?: GenerationEngine;
2
+ import { type Reasoning, type ReasoningConfig } from "@chai-ui/core";
3
+ export { DEFAULT_REASONING_MODEL, DEFAULT_REASONING_MODEL_BY_KIND, reasoningModelFor, type Reasoning, } from "@chai-ui/core";
4
+ export interface ChaiProviderProps extends ReasoningConfig {
38
5
  children?: ReactNode;
39
6
  }
40
7
  export declare function ChaiProvider({ reasoningModel, reasoningModelByKind, reasoningEngine, children }: ChaiProviderProps): import("react").JSX.Element;
41
8
  /** The nearest `ChaiProvider`'s reasoning engine and models, or `undefined` outside one. */
42
9
  export declare function useReasoning(): Reasoning | undefined;
43
- /**
44
- * The reasoning model for a call with these attachments: the first
45
- * attachment whose kind has its own model wins (a clip plus a still goes
46
- * to the model that can read the clip), else `reasoning.model`.
47
- */
48
- export declare function reasoningModelFor(reasoning: Reasoning, attachments?: DroppedMedia[]): string;
49
10
  //# sourceMappingURL=ChaiProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChaiProvider.d.ts","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,EAA0B,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAEjH;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AAEjE;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAG9E,CAAC;AAEF,mEAAmE;AACnE,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,gBAAgB,CAAC;IACzB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;CACjD;AAID,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D;;;;OAIG;IACH,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAgBlH;AAMD,4FAA4F;AAC5F,wBAAgB,YAAY,IAAI,SAAS,GAAG,SAAS,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,GAAE,YAAY,EAAO,GAAG,MAAM,CAMhG"}
1
+ {"version":3,"file":"ChaiProvider.d.ts","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3E,OAAO,EAAuC,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAE1G,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,iBAAiB,EACjB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAQlH;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,IAAI,SAAS,GAAG,SAAS,CAEpD"}
@@ -1,54 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo } from "react";
3
- import { createOpenRouterEngine } from "@chai-ui/core";
4
- /**
5
- * The suggested reasoning model for text and images: prompt enhancement,
6
- * auto-select routing and image analysis. A default, not a requirement —
7
- * pass any model id `reasoningEngine` can run.
8
- */
9
- export const DEFAULT_REASONING_MODEL = "anthropic/claude-opus-5";
10
- /**
11
- * Suggested per-kind overrides for media Claude can't read. Gemini takes
12
- * video and audio directly. For a cheaper option, swap in
13
- * `"qwen/qwen3.8-omni-flash"`, which takes both too.
14
- */
15
- export const DEFAULT_REASONING_MODEL_BY_KIND = {
16
- video: "google/gemini-3.8-flash",
17
- audio: "google/gemini-3.8-flash",
18
- };
3
+ import { createReasoning, reasoningEngineFor } from "@chai-ui/core";
4
+ export { DEFAULT_REASONING_MODEL, DEFAULT_REASONING_MODEL_BY_KIND, reasoningModelFor, } from "@chai-ui/core";
19
5
  const ReasoningContext = createContext(undefined);
20
6
  export function ChaiProvider({ reasoningModel, reasoningModelByKind, reasoningEngine, children }) {
21
- const engine = useMemo(() => reasoningEngine ?? createOpenRouterEngine(), [reasoningEngine]);
22
- const model = reasoningModel ?? DEFAULT_REASONING_MODEL;
23
- const video = reasoningModelByKind?.video;
24
- const audio = reasoningModelByKind?.audio;
25
- const image = reasoningModelByKind?.image;
26
- const text = reasoningModelByKind?.text;
27
- const value = useMemo(() => ({
28
- engine,
29
- model,
30
- modelByKind: { ...DEFAULT_REASONING_MODEL_BY_KIND, ...dropUndefined({ video, audio, image, text }) },
31
- }), [engine, model, video, audio, image, text]);
7
+ const engine = useMemo(() => reasoningEngineFor({ reasoningEngine }), [reasoningEngine]);
8
+ const { video, audio, image, text } = reasoningModelByKind ?? {};
9
+ const value = useMemo(() => createReasoning({ reasoningModel, reasoningModelByKind: { video, audio, image, text } }, engine), [engine, reasoningModel, video, audio, image, text]);
32
10
  return _jsx(ReasoningContext.Provider, { value: value, children: children });
33
11
  }
34
- function dropUndefined(obj) {
35
- return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
36
- }
37
12
  /** The nearest `ChaiProvider`'s reasoning engine and models, or `undefined` outside one. */
38
13
  export function useReasoning() {
39
14
  return useContext(ReasoningContext);
40
15
  }
41
- /**
42
- * The reasoning model for a call with these attachments: the first
43
- * attachment whose kind has its own model wins (a clip plus a still goes
44
- * to the model that can read the clip), else `reasoning.model`.
45
- */
46
- export function reasoningModelFor(reasoning, attachments = []) {
47
- for (const { kind } of attachments) {
48
- const model = reasoning.modelByKind[kind];
49
- if (model)
50
- return model;
51
- }
52
- return reasoning.model;
53
- }
54
16
  //# sourceMappingURL=ChaiProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChaiProvider.js","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAA4D,MAAM,eAAe,CAAC;AAEjH;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,yBAAyB,CAAC;AAEjE;;;;GAIG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAuC;IACjF,KAAK,EAAE,yBAAyB;IAChC,KAAK,EAAE,yBAAyB;CACjC,CAAC;AAUF,MAAM,gBAAgB,GAAG,aAAa,CAAwB,SAAS,CAAC,CAAC;AAqBzE,MAAM,UAAU,YAAY,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,eAAe,EAAE,QAAQ,EAAqB;IACjH,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,eAAe,IAAI,sBAAsB,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAC7F,MAAM,KAAK,GAAG,cAAc,IAAI,uBAAuB,CAAC;IACxD,MAAM,KAAK,GAAG,oBAAoB,EAAE,KAAK,CAAC;IAC1C,MAAM,KAAK,GAAG,oBAAoB,EAAE,KAAK,CAAC;IAC1C,MAAM,KAAK,GAAG,oBAAoB,EAAE,KAAK,CAAC;IAC1C,MAAM,IAAI,GAAG,oBAAoB,EAAE,IAAI,CAAC;IACxC,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,MAAM;QACN,KAAK;QACL,WAAW,EAAE,EAAE,GAAG,+BAA+B,EAAE,GAAG,aAAa,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE;KACrG,CAAC,EACF,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAC3C,CAAC;IACF,OAAO,KAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA6B,CAAC;AACzF,CAAC;AAED,SAAS,aAAa,CAAmB,GAAM;IAC7C,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAe,CAAC;AAClG,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC,gBAAgB,CAAC,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAoB,EAAE,cAA8B,EAAE;IACtF,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,WAAW,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,SAAS,CAAC,KAAK,CAAC;AACzB,CAAC"}
1
+ {"version":3,"file":"ChaiProvider.js","sourceRoot":"","sources":["../src/ChaiProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAwC,MAAM,eAAe,CAAC;AAE1G,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB,MAAM,gBAAgB,GAAG,aAAa,CAAwB,SAAS,CAAC,CAAC;AAMzE,MAAM,UAAU,YAAY,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,eAAe,EAAE,QAAQ,EAAqB;IACjH,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IACzF,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,oBAAoB,IAAI,EAAE,CAAC;IACjE,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,cAAc,EAAE,oBAAoB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,EACtG,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CACpD,CAAC;IACF,OAAO,KAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA6B,CAAC;AACzF,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC,gBAAgB,CAAC,CAAC;AACtC,CAAC"}
@@ -1,63 +1,8 @@
1
1
  import * as React from "react";
2
- import type { DroppedMedia, EditRegion, ModelOption, ParameterOption, MediaKind } from "@chai-ui/core";
3
- export interface ComposerAttachment extends DroppedMedia {
4
- id: string;
5
- }
6
- export type ComposerAttachMenuAction = "add-media" | "create-image" | "create-video" | "edit-media";
7
- /**
8
- * The selected generation use case, shown as a removable chip (reference:
9
- * the mock's "Video"/"Image" chip) — deliberately independent of
10
- * `attachments`: a use case can be picked from the attach menu with nothing
11
- * attached yet (e.g. "Create video"). Composer never derives this from
12
- * `attachments` itself (that's app-level policy, not a Composer opinion —
13
- * the design). With none picked, `defaultUseCase` applies.
14
- */
15
- export interface ComposerUseCase {
16
- kind: MediaKind;
17
- label: string;
18
- /**
19
- * An edit of the attached image rather than a new one: no aspect ratio,
20
- * region chips when `regions` is set, and a submit needs only an image and
21
- * either a prompt or a region. Images only for now.
22
- */
23
- edit?: boolean;
24
- }
25
- /** A plain text request — what a submit means when no use case is picked, unless the builder sets `defaultUseCase`. */
26
- export declare const TEXT_USE_CASE: ComposerUseCase;
27
- /** Editing the attached image, the attach menu's "Edit image". Pair it with `useComposer`'s `editImage` and an `EditCard`. */
28
- export declare const EDIT_IMAGE_USE_CASE: ComposerUseCase;
29
- /** One use case to run and the models picked for it. Empty `modelIds` means the engine's default model. */
30
- export interface ComposerSelection {
31
- useCase: ComposerUseCase;
32
- modelIds: string[];
33
- /** The models offered for this use case: what auto-select routes among. */
34
- models: ModelOption[];
35
- }
36
- /**
37
- * Everything Composer itself tracks, assembled into one object at submit
38
- * time — not the the design `Request` shape (no `mode`/`params`/
39
- * `presetId`; those belong to the run store this package doesn't have yet,
40
- * see the package README), just an honest snapshot of this component's own
41
- * state. Handed to `onSubmit` so a builder doesn't have to re-derive it
42
- * from five separately-controlled props they already hold — mainly a
43
- * correctness/ergonomics win for a coding agent wiring this up from the
44
- * type signature alone.
45
- */
46
- export interface ComposerSubmitPayload {
47
- value: string;
48
- attachments: ComposerAttachment[];
49
- /** The use case to run — the picked one, or `defaultUseCase` when none is picked. The first one with `multiSelectUseCases`. */
50
- useCase: ComposerUseCase;
51
- /** The first picked model, or null for the engine's default. */
52
- modelId: string | null;
53
- aspectRatio: string | null;
54
- /** Everything to run: one entry per use case with its picked models. A single entry unless `multiSelectUseCases` is on. */
55
- selections: ComposerSelection[];
56
- /** When true, the model is chosen at submit (`useComposer` routes among each selection's `models`), not by the picks. */
57
- autoSelectModel: boolean;
58
- /** An edit's marked regions, each with its own instruction. Empty unless the use case is an edit. */
59
- regions?: EditRegion[];
60
- }
2
+ import { type ComposerAttachMenuAction } from "@chai-ui/core";
3
+ import type { EditRegion, ModelOption, ParameterOption, MediaKind } from "@chai-ui/core";
4
+ import type { ComposerAttachment, ComposerSubmitPayload, ComposerUseCase } from "@chai-ui/core";
5
+ export { EDIT_IMAGE_USE_CASE, TEXT_USE_CASE, type ComposerAttachMenuAction, type ComposerAttachment, type ComposerSelection, type ComposerSubmitPayload, type ComposerUseCase, } from "@chai-ui/core";
61
6
  export interface ComposerProps {
62
7
  /** The prompt text. Controlled — Composer owns no text state of its own. */
63
8
  value: string;
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../src/Composer.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../src/Composer.tsx"],"names":[],"mappings":"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"}
package/dist/Composer.js CHANGED
@@ -4,9 +4,9 @@ import { useEffect, useRef, useState } from "react";
4
4
  import { createComponent } from "@lit/react";
5
5
  import * as React from "react";
6
6
  import { MdFab } from "@material/web/fab/fab.js";
7
- import { deriveGenerationUseCase, suggestedModels } from "@chai-ui/core";
7
+ import { composerAttachmentsFromFiles, composerAttachMenuItems, composerView, enhanceRevealDurationMs, MAX_ANIMATE_ENHANCE_LENGTH, regionColor, TEXT_USE_CASE, } from "@chai-ui/core";
8
8
  import { SearchMenu } from "./primitives/SearchMenu.js";
9
- import { regionColor } from "./regions.js";
9
+ export { EDIT_IMAGE_USE_CASE, TEXT_USE_CASE, } from "@chai-ui/core";
10
10
  /** See primitives/Toggle.tsx for why `createComponent` is used instead of raw JSX on custom-element tags. */
11
11
  const MdFabElement = createComponent({
12
12
  react: React,
@@ -14,35 +14,6 @@ const MdFabElement = createComponent({
14
14
  elementClass: MdFab,
15
15
  events: { onClick: "click" },
16
16
  });
17
- const ACCEPTED_KINDS = ["image", "video", "audio"];
18
- function detectKind(file) {
19
- const prefix = file.type.split("/")[0];
20
- return ACCEPTED_KINDS.find((k) => k === prefix);
21
- }
22
- // Past this length the rewritten prompt just appears instantly — a
23
- // multi-hundred-character reveal reads as sluggish, not delightful, and a
24
- // long prompt is exactly the case where a person wants the result now, not
25
- // a show. Picked to comfortably cover a typical Enhance rewrite (usually a
26
- // sentence or two of added detail) while bailing out before it doesn't.
27
- const MAX_ANIMATE_ENHANCE_LENGTH = 260;
28
- // Time-driven (see the `requestAnimationFrame` loop that uses this), not
29
- // count-driven — a fixed ms-per-character reveal on a slow frame would
30
- // drift or stall instead of just finishing a beat later. Capped on both
31
- // ends: a two-word rewrite shouldn't take as long as a five-word one to
32
- // feel intentional, and even a near-max-length one should still resolve
33
- // under a second.
34
- function enhanceRevealDurationMs(charCount) {
35
- return Math.min(900, Math.max(300, charCount * 6));
36
- }
37
- function nextAttachmentId() {
38
- return typeof crypto !== "undefined" && "randomUUID" in crypto
39
- ? crypto.randomUUID()
40
- : `attachment-${Date.now()}-${Math.random().toString(36).slice(2)}`;
41
- }
42
- /** A plain text request — what a submit means when no use case is picked, unless the builder sets `defaultUseCase`. */
43
- export const TEXT_USE_CASE = { kind: "text", label: "Text" };
44
- /** Editing the attached image, the attach menu's "Edit image". Pair it with `useComposer`'s `editImage` and an `EditCard`. */
45
- export const EDIT_IMAGE_USE_CASE = { kind: "image", label: "Edit image", edit: true };
46
17
  /**
47
18
  * The `bar`-layout Composer —
48
19
  * reference: the "Compose Component" design, whose four states (minimal / model+aspect select / auto-select with
@@ -54,84 +25,36 @@ export const EDIT_IMAGE_USE_CASE = { kind: "image", label: "Edit image", edit: t
54
25
  * `--chai-composer-*` custom properties in style.css.
55
26
  */
56
27
  export function Composer({ value, onChange, placeholder, promptLabel, attachments = [], onAttachmentsChange, onUnsupportedFile, useCase = null, onClearUseCase, defaultUseCase = TEXT_USE_CASE, multiSelectUseCases = false, useCases = [], onUseCasesChange, modelsByKind, multiSelectModels = false, modelIds = [], onModelIdsChange, models, modelId = null, onModelChange, autoSelectModel = false, showAutoSelectToggle = false, onAutoSelectModelChange, aspectRatios, aspectRatio = null, onAspectRatioChange, onEnhance, enhancing = false, onSubmit, submitError = null, submitDisabled, submitting = false, onAbort, onAttachMenuSelect, attachMenuActions, regions = [], onRegionsChange, disabled = false, }) {
57
- // What the person picked, and what actually runs: with nothing picked,
58
- // the builder's default use case (a text request unless they chose
59
- // otherwise), so a typed prompt always goes somewhere.
60
- const pickedUseCases = multiSelectUseCases ? useCases : useCase ? [useCase] : [];
61
- const activeUseCases = pickedUseCases.length > 0 ? pickedUseCases : [defaultUseCase];
62
- // An edit needs an image, then a change: a prompt for the whole image, a
63
- // region with its own instruction, or both.
64
- const editing = activeUseCases.some((u) => u.edit);
65
- const hasImage = attachments.some((a) => a.kind === "image");
66
- const hasContent = editing
67
- ? hasImage && (value.trim().length > 0 || regions.length > 0)
68
- : value.trim().length > 0 || attachments.length > 0;
69
- const isSubmitDisabled = submitError ? disabled : (submitDisabled ?? (disabled || !hasContent));
70
- const effectivePlaceholder = placeholder ??
71
- (editing
72
- ? hasImage
73
- ? "Describe a change across the whole image, or leave this empty"
74
- : "Attach an image to edit"
75
- : "Describe media to create");
76
- // The stop icon only appears with somewhere for its click to go — without
77
- // `onAbort`, `submitting` just disables the FAB instead (below), same as
78
- // it would with no opt-in wired at all.
79
- const showStop = submitting && Boolean(onAbort);
80
- const multiModel = multiSelectModels || multiSelectUseCases;
81
- const rawPickedIds = multiModel ? modelIds : modelId ? [modelId] : [];
82
- // The app's list for a use case, or Chai's suggested models when it gave none.
83
- const rosterFor = (u) => {
84
- const given = multiSelectUseCases ? modelsByKind?.[u.kind] : models;
85
- if (given)
86
- return { options: given, suggested: false };
87
- return { options: suggestedModels(u.edit ? "image-edit" : deriveGenerationUseCase(u, attachments)), suggested: true };
88
- };
89
- // One menu section per use case with multi-use-case select; otherwise one flat roster.
90
- const modelSections = (multiSelectUseCases ? activeUseCases : activeUseCases.slice(0, 1))
91
- .map((u) => ({ label: multiSelectUseCases ? u.label : "", kind: u.kind, ...rosterFor(u) }))
92
- .filter((s) => s.options.length > 0);
93
- const allModels = modelSections.flatMap((s) => s.options);
94
- // A pick that isn't in the current list (e.g. the edit model after leaving
95
- // edit mode) is shown as unpicked and never submitted. With no list at
96
- // all, the app's pick is passed through as given. With nothing picked
97
- // from a suggested list (and auto-select off), its first model is the pick.
98
- const validPicks = allModels.length > 0 ? rawPickedIds.filter((id) => allModels.some((m) => m.id === id)) : rawPickedIds;
99
- const pickedModelIds = validPicks.length > 0 || autoSelectModel
100
- ? validPicks
101
- : modelSections.filter((s) => s.suggested).map((s) => s.options[0].id);
102
- const pickedModels = allModels.filter((m) => pickedModelIds.includes(m.id));
103
- // No menu that can't change anything: without its handler, the Model menu
104
- // hides (the default pick still runs).
105
- const canPickModel = multiModel ? Boolean(onModelIdsChange) : Boolean(onModelChange);
106
- const modelTriggerLabel = pickedModels.length > 1
107
- ? `${pickedModels.length} models`
108
- : pickedModels[0]?.label ?? (autoSelectModel ? "Auto-select" : "Select models");
109
- // An edit keeps the source image's shape.
110
- const showAspectRatio = activeUseCases.some((u) => !u.edit && (u.kind === "image" || u.kind === "video"));
111
- // Only ratios every model that might run takes: the picked ones, or with
112
- // none picked (or auto-select on) the whole list, so whichever runs can
113
- // honor it. A model with no `aspectRatios` sets its own shape: no menu.
114
- const ratioModels = pickedModels.length > 0 && !autoSelectModel ? pickedModels : allModels;
115
- const sharedRatios = ratioModels.reduce((shared, m) => (shared ?? m.aspectRatios ?? []).filter((r) => m.aspectRatios?.includes(r)), null) ?? [];
116
- const aspectOptions = aspectRatios
117
- ? aspectRatios.filter((o) => sharedRatios.includes(o.value))
118
- : sharedRatios.map((r) => ({ value: r, label: r }));
119
- // A pick the current models don't take is shown as unpicked and never sent.
120
- const offeredAspect = showAspectRatio && aspectOptions.some((o) => o.value === aspectRatio) ? aspectRatio : null;
121
- const selectedAspect = aspectOptions.find((o) => o.value === offeredAspect) ?? null;
28
+ const view = composerView({
29
+ value,
30
+ placeholder,
31
+ attachments,
32
+ useCase,
33
+ defaultUseCase,
34
+ multiSelectUseCases,
35
+ useCases,
36
+ modelsByKind,
37
+ multiSelectModels,
38
+ modelIds,
39
+ models,
40
+ modelId,
41
+ onModelChange,
42
+ onModelIdsChange,
43
+ autoSelectModel,
44
+ aspectRatios,
45
+ aspectRatio,
46
+ submitError,
47
+ submitDisabled,
48
+ submitting,
49
+ onAbort,
50
+ regions,
51
+ disabled,
52
+ });
53
+ const { editing, isSubmitDisabled, showStop, multiModel, modelSections, pickedModelIds, aspectOptions, offeredAspect } = view;
122
54
  function toggleModel(id) {
123
55
  if (!multiModel)
124
56
  return onModelChange?.(id);
125
- onModelIdsChange?.(modelIds.includes(id) ? modelIds.filter((m) => m !== id) : [...modelIds, id]);
126
- }
127
- function buildSelections() {
128
- return activeUseCases.map((u) => ({
129
- useCase: u,
130
- models: rosterFor(u).options,
131
- modelIds: multiSelectUseCases
132
- ? pickedModelIds.filter((id) => rosterFor(u).options.some((m) => m.id === id))
133
- : pickedModelIds,
134
- }));
57
+ onModelIdsChange?.(view.toggledModelIds(id));
135
58
  }
136
59
  // Auto-select chooses at submit, from the prompt (`useComposer` routes
137
60
  // with the reasoning model). Turning it on clears the current pick, so
@@ -218,57 +141,26 @@ export function Composer({ value, onChange, placeholder, promptLabel, attachment
218
141
  setOriginalValue(value); // snapshot now — before the rewrite lands, for undo
219
142
  onEnhance?.(value);
220
143
  }
221
- function handleFilesSelected(fileList) {
144
+ async function handleFilesSelected(fileList) {
222
145
  if (!fileList || fileList.length === 0 || !onAttachmentsChange)
223
146
  return;
224
147
  // Edit mode works on one image: a new one replaces it, and the edit starts over.
225
- if (editing) {
226
- const image = Array.from(fileList).find((f) => detectKind(f) === "image");
227
- if (!image) {
228
- Array.from(fileList).forEach((f) => onUnsupportedFile?.(f.name));
229
- return;
230
- }
231
- const reader = new FileReader();
232
- reader.onload = () => onAttachmentsChange([{ id: nextAttachmentId(), src: String(reader.result), kind: "image", name: image.name, size: image.size }]);
233
- reader.onerror = () => onUnsupportedFile?.(image.name);
234
- reader.readAsDataURL(image);
235
- return;
236
- }
237
- const next = [...attachments];
238
- let pending = fileList.length;
239
- const settle = () => {
240
- pending -= 1;
241
- if (pending === 0)
242
- onAttachmentsChange(next);
243
- };
244
- Array.from(fileList).forEach((file) => {
245
- const kind = detectKind(file);
246
- if (!kind) {
247
- onUnsupportedFile?.(file.name);
248
- settle();
249
- return;
250
- }
251
- const reader = new FileReader();
252
- reader.onload = () => {
253
- next.push({ id: nextAttachmentId(), src: String(reader.result), kind, name: file.name, size: file.size });
254
- settle();
255
- };
256
- reader.onerror = () => {
257
- onUnsupportedFile?.(file.name);
258
- settle();
259
- };
260
- reader.readAsDataURL(file);
261
- });
148
+ const { attachments: next, unsupported } = await composerAttachmentsFromFiles(fileList, attachments, editing);
149
+ unsupported.forEach((name) => onUnsupportedFile?.(name));
150
+ if (next)
151
+ onAttachmentsChange(next);
262
152
  }
263
153
  // Only items that can work: no menu item that silently does nothing.
264
- const menuItems = ATTACH_MENU_ITEMS.filter((item) => (attachMenuActions ?? ATTACH_MENU_ITEMS.map((i) => i.action)).includes(item.action) &&
265
- (item.action === "add-media" ? Boolean(onAttachmentsChange) : Boolean(onAttachMenuSelect)));
154
+ const menuItems = composerAttachMenuItems(attachMenuActions, {
155
+ canAddMedia: Boolean(onAttachmentsChange),
156
+ canSelect: Boolean(onAttachMenuSelect),
157
+ });
266
158
  function handleRemoveAttachment(id) {
267
159
  onAttachmentsChange?.(attachments.filter((a) => a.id !== id));
268
160
  }
269
- return (_jsxs("div", { className: "chai-composer", children: [attachments.length > 0 && (_jsx("div", { className: "chai-composer__thumbnails", children: attachments.map((a) => (_jsx(ComposerThumbnail, { attachment: a, onRemove: onAttachmentsChange ? () => handleRemoveAttachment(a.id) : undefined }, a.id))) })), editing && regions.length > 0 && (_jsx("ul", { className: "chai-composer__regions", "aria-label": "Edit regions", children: regions.map((r) => (_jsx("li", { children: _jsx(RegionChip, { region: r, onRemove: onRegionsChange ? () => onRegionsChange(regions.filter((x) => x.id !== r.id)) : undefined, disabled: disabled }) }, r.id))) })), _jsxs("div", { className: "chai-composer__field", children: [_jsx("textarea", { ref: textareaRef, className: `chai-composer__input chai-composer__type${animating ? " chai-composer__input--revealing" : ""}`, value: value, onChange: (e) => onChange(e.target.value), placeholder: effectivePlaceholder, "aria-label": promptLabel ?? effectivePlaceholder, rows: 1, disabled: disabled || animating }), animating && (_jsx("div", { className: "chai-composer__reveal chai-composer__type", "aria-hidden": "true", children: _jsx("span", { className: "chai-composer__reveal-text", children: animatingText.slice(0, revealedLength) }) })), onEnhance && (_jsx("button", { type: "button", className: `chai-composer__optimize${enhanceBusy ? " chai-composer__optimize--active" : ""}`, onClick: handleOptimizeClick, disabled: disabled || (isEnhanced ? false : !value.trim() || enhanceBusy), "aria-label": isEnhanced ? "Revert to original prompt" : "Optimize prompt", title: isEnhanced ? "Revert to original prompt" : "Optimize prompt", children: isEnhanced ? _jsx(UndoIcon, {}) : _jsx(OptimizeIcon, {}) }))] }), submitError && (_jsxs("p", { className: "chai-composer__error", role: "alert", children: [_jsx(ErrorIcon, {}), submitError] })), _jsxs("div", { className: "chai-composer__controls", children: [_jsxs("div", { className: "chai-composer__controls-start", children: [menuItems.length > 0 && (_jsx(ComposerAttachMenu, { items: menuItems, onSelect: onAttachMenuSelect, onAddMediaFiles: handleFilesSelected, imagesOnly: editing, disabled: disabled })), pickedUseCases.map((u) => (_jsx(UseCaseChip, { useCase: u, onRemove: multiSelectUseCases
161
+ return (_jsxs("div", { className: "chai-composer", children: [attachments.length > 0 && (_jsx("div", { className: "chai-composer__thumbnails", children: attachments.map((a) => (_jsx(ComposerThumbnail, { attachment: a, onRemove: onAttachmentsChange ? () => handleRemoveAttachment(a.id) : undefined }, a.id))) })), editing && regions.length > 0 && (_jsx("ul", { className: "chai-composer__regions", "aria-label": "Edit regions", children: regions.map((r) => (_jsx("li", { children: _jsx(RegionChip, { region: r, onRemove: onRegionsChange ? () => onRegionsChange(regions.filter((x) => x.id !== r.id)) : undefined, disabled: disabled }) }, r.id))) })), _jsxs("div", { className: "chai-composer__field", children: [_jsx("textarea", { ref: textareaRef, className: `chai-composer__input chai-composer__type${animating ? " chai-composer__input--revealing" : ""}`, value: value, onChange: (e) => onChange(e.target.value), placeholder: view.placeholder, "aria-label": promptLabel ?? view.placeholder, rows: 1, disabled: disabled || animating }), animating && (_jsx("div", { className: "chai-composer__reveal chai-composer__type", "aria-hidden": "true", children: _jsx("span", { className: "chai-composer__reveal-text", children: animatingText.slice(0, revealedLength) }) })), onEnhance && (_jsx("button", { type: "button", className: `chai-composer__optimize${enhanceBusy ? " chai-composer__optimize--active" : ""}`, onClick: handleOptimizeClick, disabled: disabled || (isEnhanced ? false : !value.trim() || enhanceBusy), "aria-label": isEnhanced ? "Revert to original prompt" : "Optimize prompt", title: isEnhanced ? "Revert to original prompt" : "Optimize prompt", children: isEnhanced ? _jsx(UndoIcon, {}) : _jsx(OptimizeIcon, {}) }))] }), submitError && (_jsxs("p", { className: "chai-composer__error", role: "alert", children: [_jsx(ErrorIcon, {}), submitError] })), _jsxs("div", { className: "chai-composer__controls", children: [_jsxs("div", { className: "chai-composer__controls-start", children: [menuItems.length > 0 && (_jsx(ComposerAttachMenu, { items: menuItems, onSelect: onAttachMenuSelect, onAddMediaFiles: handleFilesSelected, imagesOnly: editing, disabled: disabled })), view.pickedUseCases.map((u) => (_jsx(UseCaseChip, { useCase: u, onRemove: multiSelectUseCases
270
162
  ? onUseCasesChange && (() => onUseCasesChange(useCases.filter((x) => x.kind !== u.kind)))
271
- : onClearUseCase, disabled: disabled }, u.kind))), modelSections.length > 0 && canPickModel && (_jsx(SearchMenu, { ...(multiSelectUseCases
163
+ : onClearUseCase, disabled: disabled }, u.kind))), modelSections.length > 0 && view.canPickModel && (_jsx(SearchMenu, { ...(multiSelectUseCases
272
164
  ? { sections: modelSections.map((s) => ({ label: s.label, options: s.options })) }
273
165
  : { options: modelSections[0].options }), value: multiModel ? pickedModelIds : modelId, multiple: multiModel, toggleHeader: showAutoSelectToggle
274
166
  ? {
@@ -276,33 +168,22 @@ export function Composer({ value, onChange, placeholder, promptLabel, attachment
276
168
  value: autoSelectModel,
277
169
  onChange: (v) => onAutoSelectModelChange?.(v),
278
170
  }
279
- : undefined, triggerLabel: modelTriggerLabel, menuLabel: multiModel ? "Select models" : "Select model", onSelect: (opt) => toggleModel(opt.id), searchable: false, triggerVariant: "text", disabled: disabled || (autoSelectModel && !showAutoSelectToggle), panelWidth: showAutoSelectToggle ? 320 : undefined })), showAspectRatio && aspectOptions.length > 0 && onAspectRatioChange && (_jsx(SearchMenu, { options: aspectOptions.map((o) => ({ id: o.value, label: o.label })), value: offeredAspect, triggerLabel: selectedAspect?.label ?? "Aspect ratio", menuLabel: "Aspect ratio", onSelect: (opt) => onAspectRatioChange?.(opt.id), searchable: false, triggerVariant: "text", disabled: disabled }))] }), _jsx(MdFabElement, { className: "chai-composer__submit", size: "small", variant: "secondary", label: "", "data-ready": showStop || !isSubmitDisabled || undefined, "data-error": Boolean(submitError) || undefined, "data-stop": showStop || undefined, ...{
171
+ : undefined, triggerLabel: view.modelTriggerLabel, menuLabel: multiModel ? "Select models" : "Select model", onSelect: (opt) => toggleModel(opt.id), searchable: false, triggerVariant: "text", disabled: disabled || (autoSelectModel && !showAutoSelectToggle), panelWidth: showAutoSelectToggle ? 320 : undefined })), view.showAspectRatio && aspectOptions.length > 0 && onAspectRatioChange && (_jsx(SearchMenu, { options: aspectOptions.map((o) => ({ id: o.value, label: o.label })), value: offeredAspect, triggerLabel: view.selectedAspect?.label ?? "Aspect ratio", menuLabel: "Aspect ratio", onSelect: (opt) => onAspectRatioChange?.(opt.id), searchable: false, triggerVariant: "text", disabled: disabled }))] }), _jsx(MdFabElement, { className: "chai-composer__submit", size: "small", variant: "secondary", label: "", "data-ready": showStop || !isSubmitDisabled || undefined, "data-error": Boolean(submitError) || undefined, "data-stop": showStop || undefined, ...{
280
172
  ariaLabel: showStop ? "Stop" : submitError ? "Retry" : "Submit",
281
173
  ariaDisabled: showStop ? "false" : submitting ? "true" : isSubmitDisabled ? "true" : "false",
282
174
  }, onClick: () => {
283
175
  if (showStop)
284
176
  onAbort?.();
285
- else if (!isSubmitDisabled && !submitting) {
286
- const selections = buildSelections();
287
- onSubmit({
288
- value,
289
- attachments,
290
- useCase: activeUseCases[0],
291
- modelId: pickedModelIds[0] ?? null,
292
- aspectRatio: offeredAspect,
293
- selections,
294
- autoSelectModel,
295
- regions: editing ? regions : [],
296
- });
297
- }
177
+ else if (!isSubmitDisabled && !submitting)
178
+ onSubmit(view.payload());
298
179
  }, children: _jsx("span", { slot: "icon", children: showStop ? _jsx(StopIcon, {}) : submitError ? _jsx(RefreshIcon, {}) : _jsx(ArrowForwardIcon, {}) }) })] })] }));
299
180
  }
300
- const ATTACH_MENU_ITEMS = [
301
- { action: "add-media", label: "Add media", icon: _jsx(AttachFileIcon, {}), divider: true },
302
- { action: "create-image", label: "Create image", icon: _jsx(ImageIcon, {}) },
303
- { action: "create-video", label: "Create video", icon: _jsx(VideocamIcon, {}) },
304
- { action: "edit-media", label: "Edit image", icon: _jsx(ContentCutIcon, {}) },
305
- ];
181
+ const ATTACH_MENU_ICONS = {
182
+ "add-media": _jsx(AttachFileIcon, {}),
183
+ "create-image": _jsx(ImageIcon, {}),
184
+ "create-video": _jsx(VideocamIcon, {}),
185
+ "edit-media": _jsx(ContentCutIcon, {}),
186
+ };
306
187
  /**
307
188
  * The "+" / "×" attach control (reference: Composer_01 "Menu open" state,
308
189
  * Figma "Menu - Icon button Example") — hand-rolled positioned
@@ -338,7 +219,7 @@ function ComposerAttachMenu({ items, onSelect, onAddMediaFiles, imagesOnly, disa
338
219
  inputRef.current?.click();
339
220
  onSelect?.(action);
340
221
  }
341
- return (_jsxs("div", { className: "chai-composer__attach", ref: rootRef, children: [_jsx("button", { type: "button", className: `chai-composer__attach-toggle${open ? " chai-composer__attach-toggle--open" : ""}`, onClick: () => setOpen((v) => !v), disabled: disabled, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": open ? "Close attach menu" : "Add media", children: open ? _jsx(CloseIcon, {}) : _jsx(PlusIcon, {}) }), open && (_jsx("ul", { className: "chai-composer__attach-menu", role: "menu", children: items.map((item, i) => (_jsxs("li", { role: "none", children: [_jsxs("button", { type: "button", role: "menuitem", className: "chai-composer__attach-menu-item", onClick: () => handleItemClick(item.action), children: [_jsx("span", { className: "chai-composer__attach-menu-icon", children: item.icon }), item.label] }), item.divider && i < items.length - 1 && _jsx("hr", { className: "chai-composer__attach-menu-divider", role: "separator" })] }, item.action))) })), _jsx("input", { ref: inputRef, type: "file", accept: imagesOnly ? "image/*" : "image/*,video/*,audio/*", multiple: !imagesOnly, hidden: true, disabled: disabled, onChange: (e) => {
222
+ return (_jsxs("div", { className: "chai-composer__attach", ref: rootRef, children: [_jsx("button", { type: "button", className: `chai-composer__attach-toggle${open ? " chai-composer__attach-toggle--open" : ""}`, onClick: () => setOpen((v) => !v), disabled: disabled, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": open ? "Close attach menu" : "Add media", children: open ? _jsx(CloseIcon, {}) : _jsx(PlusIcon, {}) }), open && (_jsx("ul", { className: "chai-composer__attach-menu", role: "menu", children: items.map((item, i) => (_jsxs("li", { role: "none", children: [_jsxs("button", { type: "button", role: "menuitem", className: "chai-composer__attach-menu-item", onClick: () => handleItemClick(item.action), children: [_jsx("span", { className: "chai-composer__attach-menu-icon", children: ATTACH_MENU_ICONS[item.action] }), item.label] }), item.divider && i < items.length - 1 && _jsx("hr", { className: "chai-composer__attach-menu-divider", role: "separator" })] }, item.action))) })), _jsx("input", { ref: inputRef, type: "file", accept: imagesOnly ? "image/*" : "image/*,video/*,audio/*", multiple: !imagesOnly, hidden: true, disabled: disabled, onChange: (e) => {
342
223
  onAddMediaFiles(e.target.files);
343
224
  e.target.value = "";
344
225
  } })] }));