@chai-ui/vue 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/style.css CHANGED
@@ -593,6 +593,75 @@
593
593
  opacity: 0.6;
594
594
  }
595
595
 
596
+ /* Create world's two prompts, side by side (stacked when narrow): each a
597
+ label and its own Enhance button above a filled box. */
598
+ .chai-composer__world-fields {
599
+ display: flex;
600
+ flex-wrap: wrap;
601
+ gap: var(--chai-space-4);
602
+ }
603
+
604
+ .chai-composer__world-field {
605
+ flex: 1 1 240px;
606
+ min-width: 0;
607
+ display: flex;
608
+ flex-direction: column;
609
+ gap: var(--chai-space-2);
610
+ }
611
+
612
+ .chai-composer__world-field-head {
613
+ display: flex;
614
+ align-items: center;
615
+ justify-content: space-between;
616
+ min-height: 24px;
617
+ }
618
+
619
+ .chai-composer__world-field-label {
620
+ font-size: var(--chai-type-size-xs);
621
+ font-weight: var(--chai-type-weight-material-medium);
622
+ line-height: var(--chai-type-leading-label-small);
623
+ letter-spacing: var(--chai-type-tracking-label-small);
624
+ color: var(--chai-color-semantic-control-text-strong);
625
+ }
626
+
627
+ .chai-composer__world-field-label strong {
628
+ font-weight: var(--chai-type-weight-semibold);
629
+ }
630
+
631
+ /* In the heading row, not over the text: no absolute position. */
632
+ .chai-composer__world-field-head .chai-composer__optimize {
633
+ position: static;
634
+ width: 24px;
635
+ }
636
+
637
+ .chai-composer__field--boxed {
638
+ min-height: 144px;
639
+ padding: var(--chai-space-3);
640
+ border-radius: var(--chai-radius-md);
641
+ background: var(--chai-color-semantic-control-field);
642
+ }
643
+
644
+ /* Smaller text than the bar's prompt, and no room kept for an Enhance button. */
645
+ .chai-composer__field--boxed .chai-composer__type {
646
+ padding-right: 0;
647
+ font-size: var(--chai-type-size-body-medium);
648
+ line-height: var(--chai-type-leading-body-medium);
649
+ letter-spacing: var(--chai-type-tracking-body-medium);
650
+ }
651
+
652
+ /* Why submit can't go yet (a world with no environment). */
653
+ .chai-composer__field-hint {
654
+ margin: 0;
655
+ font-size: var(--chai-type-size-xs);
656
+ line-height: var(--chai-type-leading-label-small);
657
+ letter-spacing: var(--chai-type-tracking-label-small);
658
+ color: var(--chai-composer-placeholder);
659
+ }
660
+
661
+ .chai-composer__field--boxed .chai-composer__reveal {
662
+ inset: var(--chai-space-3);
663
+ }
664
+
596
665
  /* The optimize/undo reveal (Composer.tsx's own doc comment on the overlay
597
666
  for the full mechanics) — the real textarea's text goes fully
598
667
  transparent so only the overlay's progressive, gradient-lit copy shows
@@ -682,6 +751,99 @@
682
751
  flex-wrap: wrap;
683
752
  }
684
753
 
754
+ .chai-composer__controls-end {
755
+ display: flex;
756
+ align-items: center;
757
+ gap: var(--chai-space-2);
758
+ }
759
+
760
+ /* Voice input: the mic left of submit. While recording it becomes a stop
761
+ button (a filled square in a ringed circle) and a waveform replaces the
762
+ prompt; while transcribing, a spinner. */
763
+ .chai-composer__voice {
764
+ display: flex;
765
+ align-items: center;
766
+ }
767
+
768
+ .chai-composer__voice-button {
769
+ display: flex;
770
+ align-items: center;
771
+ justify-content: center;
772
+ width: 40px;
773
+ height: 40px;
774
+ padding: 0;
775
+ border: 2px solid transparent;
776
+ border-radius: var(--chai-radius-full);
777
+ background: transparent;
778
+ color: var(--chai-composer-text);
779
+ cursor: pointer;
780
+ transition: background-color var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
781
+ }
782
+
783
+ .chai-composer__voice-button:hover:not(:disabled) {
784
+ background: var(--chai-composer-hover);
785
+ }
786
+
787
+ .chai-composer__voice-button:disabled {
788
+ cursor: not-allowed;
789
+ opacity: 0.5;
790
+ }
791
+
792
+ .chai-composer__voice-button svg {
793
+ width: 24px;
794
+ height: 24px;
795
+ }
796
+
797
+ .chai-composer__voice[data-status="recording"] .chai-composer__voice-button {
798
+ border-color: var(--chai-composer-text);
799
+ background: var(--chai-composer-hover);
800
+ }
801
+
802
+ .chai-composer__voice[data-status="recording"] .chai-composer__voice-button svg {
803
+ width: 18px;
804
+ height: 18px;
805
+ }
806
+
807
+ /* Same row height as the prompt it stands in for. */
808
+ .chai-composer__waveform {
809
+ display: flex;
810
+ align-items: center;
811
+ gap: var(--chai-space-3);
812
+ min-height: var(--chai-type-leading-body-large);
813
+ }
814
+
815
+ .chai-composer__waveform-bars {
816
+ flex: 1;
817
+ display: flex;
818
+ align-items: center;
819
+ justify-content: space-between;
820
+ height: var(--chai-type-leading-body-large);
821
+ min-width: 0;
822
+ }
823
+
824
+ .chai-composer__waveform-bar {
825
+ width: 2px;
826
+ border-radius: var(--chai-radius-full);
827
+ background: var(--chai-composer-text);
828
+ }
829
+
830
+ .chai-composer__waveform-time {
831
+ color: var(--chai-composer-text);
832
+ font-family: var(--chai-type-family-material);
833
+ font-size: var(--chai-type-size-label-large);
834
+ font-weight: var(--chai-type-weight-material-medium);
835
+ font-variant-numeric: tabular-nums;
836
+ }
837
+
838
+ .chai-composer__voice-spinner {
839
+ width: 20px;
840
+ height: 20px;
841
+ border-radius: 50%;
842
+ border: 2px solid var(--chai-color-semantic-control-border-faint);
843
+ border-top-color: var(--chai-color-semantic-accent);
844
+ animation: chai-spin var(--chai-motion-duration-steep) linear infinite;
845
+ }
846
+
685
847
  /* Neutral trigger text, not `--md-sys-color-primary` green (account
686
848
  holder correction: "don't make dropdown select menu buttons green when
687
849
  they don't need to be"). */
@@ -2339,7 +2501,9 @@
2339
2501
  draw `<md-focus-ring>`. The Composer's textarea is the one control that
2340
2502
  had none — its ring goes on the whole pill, matching MD3's ring width
2341
2503
  and color so every ring in a component reads as the same system. */
2342
- .chai-composer:has(.chai-composer__input:focus-visible) {
2504
+ .chai-composer:has(.chai-composer__field:not(.chai-composer__field--boxed) .chai-composer__input:focus-visible),
2505
+ /* A world's two boxes: the ring marks the box being typed in, not the bar. */
2506
+ .chai-composer__field--boxed:has(.chai-composer__input:focus-visible) {
2343
2507
  outline: 3px solid var(--chai-color-semantic-accent);
2344
2508
  outline-offset: 2px;
2345
2509
  }
@@ -2365,6 +2529,7 @@
2365
2529
  }
2366
2530
 
2367
2531
  [data-chai-focus-ring="hidden"] .chai-composer:has(.chai-composer__input:focus-visible),
2532
+ [data-chai-focus-ring="hidden"] .chai-composer__field--boxed:has(.chai-composer__input:focus-visible),
2368
2533
  [data-chai-focus-ring="hidden"] :is(
2369
2534
  .chai-composer,
2370
2535
  .chai-media-analyzer,
@@ -12,7 +12,7 @@ export declare function useComposer(options?: MaybeRefOrGetter<UseComposerOption
12
12
  submit: (payload: import("@chai-ui/core").ComposerSubmitPayload) => void;
13
13
  cancel: () => void;
14
14
  /** `undefined` without reasoning, so the Composer hides its Enhance button. */
15
- enhance: import("vue").ComputedRef<((value: string) => Promise<string>) | undefined>;
15
+ enhance: import("vue").ComputedRef<((value: string, field?: import("@chai-ui/core").ComposerPromptField, mode?: import("@chai-ui/core").GenerationUseCase) => Promise<string>) | undefined>;
16
16
  enhancing: import("vue").ComputedRef<boolean>;
17
17
  routing: import("vue").ComputedRef<boolean>;
18
18
  /** Props for an `EditCard`, and `regions` for `Composer`. `null` when there's no `editImage`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chai-ui/vue",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "CHAI UI Vue components — smart, drop-in UI for AI apps: Composer, Media Analyzer, Result Card, Edit Card, and the useComposer composable.",
5
5
  "license": "MIT",
6
6
  "author": "kazarazat",
@@ -46,8 +46,8 @@
46
46
  "dependencies": {
47
47
  "@fontsource/roboto": "^5.3.0",
48
48
  "@material/web": "^2.5.0",
49
- "@chai-ui/tokens": "0.7.1",
50
- "@chai-ui/core": "0.7.1"
49
+ "@chai-ui/core": "0.9.0",
50
+ "@chai-ui/tokens": "0.9.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@vitejs/plugin-vue": "^6.0.9",
package/src/Composer.vue CHANGED
@@ -9,30 +9,37 @@
9
9
  * Some controls show only with somewhere for them to go, so these listeners
10
10
  * are props, checked for: `@update:attachments` (add and remove media),
11
11
  * `@update:model-id` / `@update:model-ids` (the Model menu),
12
- * `@update:aspect-ratio`, `@update:use-cases` and `@clear-use-case` (chip
12
+ * `voiceInput` (the mic), `@update:aspect-ratio`, `@update:character` and `@update:frame-rate` (a
13
+ * world's fields), `@update:use-cases` and `@clear-use-case` (chip
13
14
  * remove), `@update:regions`, `@enhance` (the Enhance button), `@abort` (stop
14
15
  * while submitting) and `@attach-menu-select` (the + menu's use cases).
15
16
  */
16
17
  import "@material/web/fab/fab.js";
17
- import { computed, onScopeDispose, ref, watch } from "vue";
18
+ import { computed, onScopeDispose, ref, shallowRef, watch } from "vue";
18
19
  import {
19
20
  composerAttachmentsFromFiles,
20
21
  composerAttachMenuItems,
21
22
  composerView,
22
- enhanceRevealDurationMs,
23
- MAX_ANIMATE_ENHANCE_LENGTH,
23
+ createVoiceInput,
24
24
  type ComposerAttachment,
25
25
  type ComposerAttachMenuAction,
26
+ type ComposerPromptField,
27
+ type GenerationUseCase,
26
28
  type ComposerSubmitPayload,
27
29
  type ComposerUseCase,
28
30
  type EditRegion,
29
31
  type MediaKind,
30
32
  type ModelOption,
33
+ type OutputKind,
31
34
  type ParameterOption,
35
+ type VoiceInputConfig,
32
36
  } from "@chai-ui/core";
33
37
  import AttachMenu from "./composer/AttachMenu.vue";
34
38
  import Chip from "./composer/Chip.vue";
35
- import { ArrowForwardIcon, CloseSymbolIcon, ErrorIcon, ImageIcon, MusicNoteIcon, OptimizeIcon, RetryIcon, StopIcon, UndoIcon, VideocamIcon } from "./icons.js";
39
+ import PromptField from "./composer/PromptField.vue";
40
+ import VoiceButton from "./composer/VoiceButton.vue";
41
+ import VoiceWaveform from "./composer/VoiceWaveform.vue";
42
+ import { ArrowForwardIcon, CloseSymbolIcon, ErrorIcon, ImageIcon, MusicNoteIcon, RetryIcon, StopIcon, VideocamIcon } from "./icons.js";
36
43
  import SearchMenu from "./primitives/SearchMenu.vue";
37
44
 
38
45
  const props = withDefaults(
@@ -55,7 +62,7 @@ const props = withDefaults(
55
62
  useCases?: ComposerUseCase[];
56
63
  "onUpdate:useCases"?: (next: ComposerUseCase[]) => void;
57
64
  /** Models per use-case kind, for `multiSelectUseCases`. A kind left out gets Chai's suggested models. */
58
- modelsByKind?: Partial<Record<MediaKind, ModelOption[]>>;
65
+ modelsByKind?: Partial<Record<OutputKind, ModelOption[]>>;
59
66
  /** The end user can pick several models, each producing its own result, with `modelIds`. */
60
67
  multiSelectModels?: boolean;
61
68
  modelIds?: string[];
@@ -73,10 +80,16 @@ const props = withDefaults(
73
80
  aspectRatios?: ParameterOption<string>[];
74
81
  aspectRatio?: string | null;
75
82
  "onUpdate:aspectRatio"?: (value: string) => void;
76
- /** Shows the Enhance button; your handler runs the rewrite and updates the prompt. Gets the current prompt. */
77
- onEnhance?: (value: string) => void;
83
+ /** Shows the Enhance button; your handler runs the rewrite and updates the prompt. Gets the current text, which field, and the use case on screen (`mode`). */
84
+ onEnhance?: (value: string, field: ComposerPromptField, mode?: GenerationUseCase) => void;
78
85
  /** True while your Enhance handler is running. */
79
86
  enhancing?: boolean;
87
+ /** A world's character field (Create world), beside the environment (`v-model`). Shows with `v-model:character`. */
88
+ character?: string;
89
+ "onUpdate:character"?: (value: string) => void;
90
+ /** A world's frame rate menu, from the selected models' `frameRates`. */
91
+ frameRate?: number | null;
92
+ "onUpdate:frameRate"?: (value: number) => void;
80
93
  /** The last submit's failure: submit becomes retry, and the message shows inline. */
81
94
  submitError?: string | null;
82
95
  /** Overrides the built-in "empty prompt and no attachments" check. Ignored while `submitError` is set. */
@@ -90,6 +103,13 @@ const props = withDefaults(
90
103
  /** An edit's regions (from `useComposer`'s `edit`), shown as colored chips while editing. */
91
104
  regions?: EditRegion[];
92
105
  "onUpdate:regions"?: (next: EditRegion[]) => void;
106
+ /**
107
+ * The mic, left of submit: the transcript is added to the prompt, never
108
+ * submitted. `{ model: "fal-ai/wizper" }` goes through your
109
+ * `/api/chai/fal` route; `{ transcribe }` uses any provider. Off by
110
+ * default, with no default model.
111
+ */
112
+ voiceInput?: VoiceInputConfig;
93
113
  disabled?: boolean;
94
114
  }>(),
95
115
  {
@@ -99,6 +119,8 @@ const props = withDefaults(
99
119
  modelIds: () => [],
100
120
  modelId: null,
101
121
  aspectRatio: null,
122
+ character: "",
123
+ frameRate: null,
102
124
  submitError: null,
103
125
  // Left undefined unless set: Vue would make an absent boolean `false`,
104
126
  // which reads as "never disable".
@@ -159,64 +181,29 @@ function toggleModel(id: string) {
159
181
  else props["onUpdate:modelId"]?.(id);
160
182
  }
161
183
 
162
- // Grows with wrapped text from one line, instead of a fixed row count.
163
- const textarea = ref<HTMLTextAreaElement>();
164
- watch(
165
- () => props.modelValue,
166
- () => {
167
- const el = textarea.value;
168
- if (!el) return;
169
- el.style.height = "auto";
170
- el.style.height = `${el.scrollHeight}px`;
171
- },
172
- { flush: "post", immediate: true }
173
- );
184
+ // Which field's Enhance is running: `enhancing` is one flag for the bar.
185
+ const enhancingField = ref<ComposerPromptField>("prompt");
186
+ const enhanceField = (field: ComposerPromptField) =>
187
+ props.onEnhance &&
188
+ ((current: string) => {
189
+ enhancingField.value = field;
190
+ props.onEnhance?.(current, field, view.value.enhanceMode);
191
+ });
174
192
 
175
- // Enhance and undo. The prompt already holds the rewrite once `enhancing`
176
- // drops; the reveal is a decorative overlay painting it in, then gone.
177
- const originalValue = ref<string | null>(null);
178
- const enhancedValue = ref<string | null>(null);
179
- const animatingText = ref<string | null>(null);
180
- const revealedLength = ref(0);
181
- let raf = 0;
182
-
183
- watch(
184
- () => props.enhancing,
185
- (now, was) => {
186
- if (!was || now || originalValue.value == null || props.modelValue === originalValue.value) return;
187
- const value = props.modelValue;
188
- enhancedValue.value = value;
189
- // Long rewrites and reduced-motion requests appear at once.
190
- if (value.length > MAX_ANIMATE_ENHANCE_LENGTH || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
191
- animatingText.value = value;
192
- revealedLength.value = 0;
193
- const duration = enhanceRevealDurationMs(value.length);
194
- const start = performance.now();
195
- const tick = (time: number) => {
196
- revealedLength.value = Math.min(value.length, Math.floor(((time - start) / duration) * value.length));
197
- if (revealedLength.value < value.length) raf = requestAnimationFrame(tick);
198
- else animatingText.value = null;
199
- };
200
- raf = requestAnimationFrame(tick);
193
+ // Voice input: a transcript is added to the prompt, never submitted.
194
+ const voice = createVoiceInput(
195
+ () => props.voiceInput ?? { transcribe: async () => "" },
196
+ (text) => {
197
+ const current = props.modelValue;
198
+ emit("update:modelValue", current.trim() ? `${current.trimEnd()} ${text}` : text);
201
199
  }
202
200
  );
203
- onScopeDispose(() => cancelAnimationFrame(raf));
204
-
205
- const animating = computed(() => animatingText.value != null);
206
- const isEnhanced = computed(() => !animating.value && enhancedValue.value != null && props.modelValue === enhancedValue.value);
207
- const enhanceBusy = computed(() => Boolean(props.enhancing) || animating.value);
208
-
209
- function onOptimizeClick() {
210
- if (isEnhanced.value) {
211
- const original = originalValue.value;
212
- enhancedValue.value = null;
213
- originalValue.value = null;
214
- if (original != null) emit("update:modelValue", original);
215
- return;
216
- }
217
- originalValue.value = props.modelValue; // for undo, before the rewrite lands
218
- props.onEnhance?.(props.modelValue);
219
- }
201
+ const voiceState = shallowRef(voice.getState());
202
+ const stopVoice = voice.subscribe(() => (voiceState.value = voice.getState()));
203
+ onScopeDispose(() => {
204
+ stopVoice();
205
+ voice.dispose();
206
+ });
220
207
 
221
208
  async function onFiles(files: FileList | null) {
222
209
  const onAttachments = props["onUpdate:attachments"];
@@ -268,36 +255,44 @@ function onSubmitClick() {
268
255
  </li>
269
256
  </ul>
270
257
 
271
- <div class="chai-composer__field">
272
- <textarea
273
- ref="textarea"
274
- :class="['chai-composer__input chai-composer__type', { 'chai-composer__input--revealing': animating }]"
258
+ <VoiceWaveform v-if="voiceInput && voiceState.status === 'recording'" :levels="voiceState.levels" :elapsed-ms="voiceState.elapsedMs" />
259
+ <div v-else-if="view.world" class="chai-composer__world-fields">
260
+ <PromptField
275
261
  :value="modelValue"
276
262
  :placeholder="view.placeholder"
277
- :aria-label="promptLabel ?? view.placeholder"
278
- rows="1"
279
- :disabled="disabled || animating"
280
- @input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
263
+ heading="Environment"
264
+ note="(required)"
265
+ :hint="view.environmentHint"
266
+ :on-enhance="enhanceField('environment')"
267
+ :enhancing="Boolean(enhancing) && enhancingField === 'environment'"
268
+ :disabled="disabled"
269
+ @change="emit('update:modelValue', $event)"
270
+ />
271
+ <PromptField
272
+ v-if="props['onUpdate:character']"
273
+ :value="character"
274
+ :placeholder="view.characterPlaceholder"
275
+ heading="Character"
276
+ note="(optional)"
277
+ :on-enhance="enhanceField('character')"
278
+ :enhancing="Boolean(enhancing) && enhancingField === 'character'"
279
+ :disabled="disabled"
280
+ @change="props['onUpdate:character']?.($event)"
281
281
  />
282
- <!-- The Enhance reveal: decorative, so hidden from screen readers, which read the textarea's final text. -->
283
- <div v-if="animating" class="chai-composer__reveal chai-composer__type" aria-hidden="true">
284
- <span class="chai-composer__reveal-text">{{ animatingText!.slice(0, revealedLength) }}</span>
285
- </div>
286
- <!-- Optimize, or once a rewrite lands, undo it: until the prompt is edited by hand. -->
287
- <button
288
- v-if="onEnhance"
289
- type="button"
290
- :class="['chai-composer__optimize', { 'chai-composer__optimize--active': enhanceBusy }]"
291
- :disabled="disabled || (isEnhanced ? false : !modelValue.trim() || enhanceBusy)"
292
- :aria-label="isEnhanced ? 'Revert to original prompt' : 'Optimize prompt'"
293
- :title="isEnhanced ? 'Revert to original prompt' : 'Optimize prompt'"
294
- @click="onOptimizeClick"
295
- >
296
- <UndoIcon v-if="isEnhanced" /><OptimizeIcon v-else />
297
- </button>
298
282
  </div>
283
+ <PromptField
284
+ v-else
285
+ :value="modelValue"
286
+ :placeholder="view.placeholder"
287
+ :label="promptLabel"
288
+ :on-enhance="enhanceField('prompt')"
289
+ :enhancing="Boolean(enhancing)"
290
+ :disabled="disabled"
291
+ @change="emit('update:modelValue', $event)"
292
+ />
299
293
 
300
294
  <p v-if="submitError" class="chai-composer__error" role="alert"><ErrorIcon />{{ submitError }}</p>
295
+ <p v-if="voiceInput && voiceState.status === 'error'" class="chai-composer__error" role="alert"><ErrorIcon />{{ voiceState.error }}</p>
301
296
 
302
297
  <div class="chai-composer__controls">
303
298
  <div class="chai-composer__controls-start">
@@ -350,10 +345,26 @@ function onSubmitClick() {
350
345
  :disabled="disabled"
351
346
  @select="props['onUpdate:aspectRatio']?.($event.id)"
352
347
  />
348
+
349
+ <SearchMenu
350
+ v-if="view.frameRateOptions.length > 0 && props['onUpdate:frameRate']"
351
+ :options="view.frameRateOptions.map((r) => ({ id: String(r), label: `${r}fps` }))"
352
+ :value="view.offeredFrameRate == null ? null : String(view.offeredFrameRate)"
353
+ :trigger-label="view.offeredFrameRate == null ? 'Frame rate' : `${view.offeredFrameRate}fps`"
354
+ menu-label="Frame rate"
355
+ :searchable="false"
356
+ trigger-variant="text"
357
+ :disabled="disabled"
358
+ @select="props['onUpdate:frameRate']?.(Number($event.id))"
359
+ />
353
360
  </div>
354
361
 
355
- <!-- MD3's md-fab has no disabled state of its own; Chai's unfilled-until-ready look comes from `data-ready` in style.css. `label=""` keeps it a plain icon FAB. -->
362
+ <div class="chai-composer__controls-end">
363
+ <VoiceButton v-if="voiceInput" :voice="voice" :state="voiceState" :disabled="disabled" />
364
+ <!-- MD3's md-fab has no disabled state of its own; Chai's unfilled-until-ready look comes from `data-ready` in style.css. `label=""` keeps it a plain icon FAB.
365
+ Hidden while a recording is made or transcribed: the transcript is sent once it's in the prompt to check. -->
356
366
  <md-fab
367
+ v-if="!(voiceInput && (voiceState.status === 'recording' || voiceState.status === 'transcribing'))"
357
368
  class="chai-composer__submit"
358
369
  size="small"
359
370
  variant="secondary"
@@ -367,6 +378,7 @@ function onSubmitClick() {
367
378
  >
368
379
  <span slot="icon"><StopIcon v-if="view.showStop" /><RetryIcon v-else-if="submitError" /><ArrowForwardIcon v-else /></span>
369
380
  </md-fab>
381
+ </div>
370
382
  </div>
371
383
  </div>
372
384
  </template>
@@ -3,7 +3,7 @@
3
3
  import { ref } from "vue";
4
4
  import type { ComposerAttachMenuAction, composerAttachMenuItems } from "@chai-ui/core";
5
5
  import { useDismiss } from "../dismiss.js";
6
- import { AttachFileIcon, CloseSymbolIcon, ContentCutIcon, ImageIcon, PlusIcon, VideocamIcon } from "../icons.js";
6
+ import { AttachFileIcon, CloseSymbolIcon, ContentCutIcon, ImageIcon, PlusIcon, GlobeIcon, VideocamIcon } from "../icons.js";
7
7
 
8
8
  defineProps<{
9
9
  items: ReturnType<typeof composerAttachMenuItems>;
@@ -13,7 +13,7 @@ defineProps<{
13
13
  }>();
14
14
  const emit = defineEmits<{ select: [action: ComposerAttachMenuAction]; files: [files: FileList | null] }>();
15
15
 
16
- const ICONS = { "add-media": AttachFileIcon, "create-image": ImageIcon, "create-video": VideocamIcon, "edit-media": ContentCutIcon };
16
+ const ICONS = { "add-media": AttachFileIcon, "create-image": ImageIcon, "create-video": VideocamIcon, "create-world": GlobeIcon, "edit-media": ContentCutIcon };
17
17
  const open = ref(false);
18
18
  const root = ref<HTMLElement>();
19
19
  const input = ref<HTMLInputElement>();