@chai-ui/vue 0.10.1 → 0.11.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.
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import "@material/web/checkbox/checkbox.js";
8
8
  import "@material/web/textfield/outlined-text-field.js";
9
+ import { type MenuPlacement } from "@chai-ui/core";
9
10
  import type { SearchMenuOption, SearchMenuSection, SearchMenuToggleHeader } from "./search-menu.js";
10
11
  declare const __VLS_export: <T extends SearchMenuOption>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
11
12
  props: import("vue").PublicProps & __VLS_PrettifyLocal<{
@@ -29,6 +30,8 @@ declare const __VLS_export: <T extends SearchMenuOption>(__VLS_props: NonNullabl
29
30
  triggerVariant?: "outlined" | "text";
30
31
  /** Panel width in px; longer labels ellipsize. */
31
32
  panelWidth?: number;
33
+ /** Which way the panel opens: "auto" (default) below when it fits, else above when that fits, else toward the side with more room. */
34
+ placement?: MenuPlacement;
32
35
  } & {
33
36
  onSelect?: ((option: T) => any) | undefined;
34
37
  }> & (typeof globalThis extends {
@@ -1 +1 @@
1
- {"version":3,"file":"SearchMenu.vue.d.ts","sourceRoot":"","sources":["../../src/primitives/SearchMenu.vue"],"names":[],"mappings":"AA0KA;;;;;GAKG;AACH,OAAO,oCAAoC,CAAC;AAC5C,OAAO,gDAAgD,CAAC;AAKxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAEpG,QAAA,MAAM,YAAY,GAAK,CAAC,SAAS,gBAAgB,EAChD,aAAa,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,EAC9D,YAAY,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,EAC3G,gBAAgB,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,EAClE;WA2XO,OAAO,KAAK,EAAE,WAAW,GAAG,mBAAmB,CAAC;QAzXpD,0DAA0D;kBAChD,CAAC,EAAE;QACb,gFAAgF;mBACrE,iBAAiB,CAAC,CAAC,CAAC,EAAE;QACjC,0HAA0H;uBAC3G,sBAAsB;QACrC,uDAAuD;eAChD,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;QAC/B,mGAAmG;mBACxF,OAAO;sBACJ,MAAM;QACpB,2EAA2E;mBAChE,MAAM;mBACN,OAAO;QAClB,yDAAyD;qBAC5C,OAAO;QACpB,kFAAkF;yBACjE,UAAU,GAAG,MAAM;QACpC,kDAAkD;qBACrC,MAAM;;;KAsWoE,CAAC,GAAG,CAAC,OAAO,UAAU,SAAS;QAAE,oBAAoB,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,CAAC,GAAG,EAAE,CAAC;YAC5J,CAAC,OAAO,EAAE,EAAE,KAAK,IAAI;WACtB,GAAG;WACH,EAAE;;EAGN,KACQ,OAAO,KAAK,EAAE,KAAK,GAAG;IAAE,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAA;CAAI,CAAC;wBACpE,OAAO,YAAY;AAAxC,wBAAyC;AACzC,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,CAAC,GAAG,EAAE,CAAC"}
1
+ {"version":3,"file":"SearchMenu.vue.d.ts","sourceRoot":"","sources":["../../src/primitives/SearchMenu.vue"],"names":[],"mappings":"AAgLA;;;;;GAKG;AACH,OAAO,oCAAoC,CAAC;AAC5C,OAAO,gDAAgD,CAAC;AAIxD,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAEpG,QAAA,MAAM,YAAY,GAAK,CAAC,SAAS,gBAAgB,EAChD,aAAa,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,EAC9D,YAAY,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,EAC3G,gBAAgB,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,EAClE;WAkYO,OAAO,KAAK,EAAE,WAAW,GAAG,mBAAmB,CAAC;QAhYpD,0DAA0D;kBAChD,CAAC,EAAE;QACb,gFAAgF;mBACrE,iBAAiB,CAAC,CAAC,CAAC,EAAE;QACjC,0HAA0H;uBAC3G,sBAAsB;QACrC,uDAAuD;eAChD,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;QAC/B,mGAAmG;mBACxF,OAAO;sBACJ,MAAM;QACpB,2EAA2E;mBAChE,MAAM;mBACN,OAAO;QAClB,yDAAyD;qBAC5C,OAAO;QACpB,kFAAkF;yBACjE,UAAU,GAAG,MAAM;QACpC,kDAAkD;qBACrC,MAAM;QACnB,sIAAsI;oBAC1H,aAAa;;;KA2W8D,CAAC,GAAG,CAAC,OAAO,UAAU,SAAS;QAAE,oBAAoB,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,CAAC,GAAG,EAAE,CAAC;YAC5J,CAAC,OAAO,EAAE,EAAE,KAAK,IAAI;WACtB,GAAG;WACH,EAAE;;EAGN,KACQ,OAAO,KAAK,EAAE,KAAK,GAAG;IAAE,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAA;CAAI,CAAC;wBACpE,OAAO,YAAY;AAAxC,wBAAyC;AACzC,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,CAAC,GAAG,EAAE,CAAC"}
package/dist/style.css CHANGED
@@ -209,8 +209,17 @@
209
209
  gap: var(--chai-space-2);
210
210
  }
211
211
 
212
+ /* Opens upward: `placement="above"`, or `"auto"` with no room below. */
213
+ .chai-search-menu__panel--above {
214
+ top: auto;
215
+ bottom: calc(100% + 8px);
216
+ }
217
+
218
+ /* 44px tall, not Material's 56px: the field sits inside a compact menu. */
212
219
  .chai-search-menu__search {
213
220
  width: 100%;
221
+ --md-outlined-text-field-top-space: 10px;
222
+ --md-outlined-text-field-bottom-space: 10px;
214
223
  }
215
224
 
216
225
  .chai-search-menu__search-icon {
@@ -249,9 +258,13 @@
249
258
  border: none;
250
259
  border-radius: var(--chai-radius-sm);
251
260
  background: transparent;
252
- color: var(--chai-color-semantic-control-text-strong);
261
+ /* The same grey and 14px label type as the menu triggers and the rest
262
+ of the controls: 40px rows, the attach menu's density. */
263
+ color: var(--chai-color-semantic-control-text);
253
264
  font-family: var(--chai-type-family-material);
254
- font-size: var(--chai-type-size-body-large);
265
+ font-size: var(--chai-type-size-label-large);
266
+ line-height: var(--chai-type-leading-label-large);
267
+ letter-spacing: var(--chai-type-tracking-label-large);
255
268
  text-align: left;
256
269
  cursor: pointer;
257
270
  }
@@ -296,6 +309,9 @@
296
309
  .chai-search-menu__checkbox {
297
310
  flex: none;
298
311
  pointer-events: none;
312
+ /* 16px, not MD3's 18px, beside 14px labels. */
313
+ --md-checkbox-container-size: 16px;
314
+ --md-checkbox-icon-size: 16px;
299
315
  }
300
316
 
301
317
  .chai-search-menu__option svg {
@@ -339,11 +355,13 @@
339
355
  gap: var(--chai-space-3);
340
356
  }
341
357
 
358
+ /* The menu triggers' type: 14px medium, the controls' grey. */
342
359
  .chai-search-menu__toggle-header-text {
343
360
  font-family: var(--chai-type-family-material);
344
- font-size: var(--chai-type-size-body-large);
361
+ font-size: var(--chai-type-size-label-large);
345
362
  font-weight: var(--chai-type-weight-material-medium);
346
- color: var(--chai-color-semantic-control-text-strong);
363
+ letter-spacing: var(--chai-type-tracking-label-large);
364
+ color: var(--chai-color-semantic-control-text);
347
365
  }
348
366
 
349
367
  .chai-search-menu__empty {
@@ -870,18 +888,26 @@
870
888
  position: relative;
871
889
  }
872
890
 
891
+ /* A 40px rounded square, the size of the mic button's target, with
892
+ -8px margins so the 24px icon and the row's layout stay where they were. */
873
893
  .chai-composer__attach-toggle {
874
894
  display: flex;
875
895
  align-items: center;
876
896
  justify-content: center;
877
- width: 24px;
878
- height: 24px;
897
+ width: 40px;
898
+ height: 40px;
899
+ margin: -8px;
879
900
  padding: 0;
880
901
  border: none;
881
- border-radius: var(--chai-radius-full);
902
+ border-radius: var(--chai-radius-sm);
882
903
  background: transparent;
883
904
  color: var(--chai-composer-text);
884
905
  cursor: pointer;
906
+ transition: background-color var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
907
+ }
908
+
909
+ .chai-composer__attach-toggle:hover:not(:disabled) {
910
+ background: var(--chai-composer-hover);
885
911
  }
886
912
 
887
913
  .chai-composer__attach-toggle svg {
@@ -901,7 +927,7 @@
901
927
  .chai-composer__attach-menu {
902
928
  position: absolute;
903
929
  z-index: 10;
904
- bottom: calc(100% + 8px);
930
+ top: calc(100% + 8px);
905
931
  left: 0;
906
932
  min-width: 180px;
907
933
  margin: 0;
@@ -912,12 +938,21 @@
912
938
  box-shadow: var(--chai-shadow-menu);
913
939
  }
914
940
 
941
+ /* Opens upward: `menuPlacement="above"`, or `"auto"` with no room below. */
942
+ .chai-composer__attach-menu--above {
943
+ top: auto;
944
+ bottom: calc(100% + 8px);
945
+ }
946
+
947
+ /* MD3 menu density -2: 40px items (48px at density 0, 4px less per step),
948
+ so the longer menu with the audio group stays compact. */
915
949
  .chai-composer__attach-menu-item {
916
950
  display: flex;
917
951
  align-items: center;
918
952
  gap: var(--chai-space-3);
919
953
  width: 100%;
920
- padding: var(--chai-space-2) var(--chai-space-4);
954
+ min-height: 40px;
955
+ padding: 0 var(--chai-space-4);
921
956
  border: none;
922
957
  background: transparent;
923
958
  color: var(--chai-color-semantic-control-text-strong);
@@ -946,9 +981,9 @@
946
981
  height: 100%;
947
982
  }
948
983
 
949
- /* Reference: "Add media" (`MenuListItem1`) is the only item with a divider
950
- under it in the source (`showDivider: true`), separating the real
951
- attach action from the three generation-mode actions below it. */
984
+ /* Between menu groups (see `COMPOSER_ATTACH_MENU_ITEMS`): under "Add media",
985
+ separating the real attach action from the visual use cases, and above
986
+ the audio use cases. */
952
987
  .chai-composer__attach-menu-divider {
953
988
  margin: var(--chai-space-2) 0;
954
989
  border: none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chai-ui/vue",
3
- "version": "0.10.1",
3
+ "version": "0.11.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",
@@ -47,8 +47,8 @@
47
47
  "dependencies": {
48
48
  "@fontsource/roboto": "^5.3.0",
49
49
  "@material/web": "^2.5.0",
50
- "@chai-ui/tokens": "0.10.1",
51
- "@chai-ui/core": "0.10.1"
50
+ "@chai-ui/core": "0.11.0",
51
+ "@chai-ui/tokens": "0.11.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@vitejs/plugin-vue": "^6.0.9",
package/src/Composer.vue CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  type ComposerUseCase,
30
30
  type EditRegion,
31
31
  type MediaKind,
32
+ type MenuPlacement,
32
33
  type ModelOption,
33
34
  type OutputKind,
34
35
  type ParameterOption,
@@ -90,6 +91,19 @@ const props = withDefaults(
90
91
  /** A world's frame rate menu, from the selected models' `frameRates`. */
91
92
  frameRate?: number | null;
92
93
  "onUpdate:frameRate"?: (value: number) => void;
94
+ /** Create speech's Voice menu, from the one picked model's `voices` (`v-model:speech-voice`). Not `voiceInput`, which is dictation. */
95
+ speechVoice?: string | null;
96
+ "onUpdate:speechVoice"?: (value: string) => void;
97
+ /** Create music's Genre menu, several at once, from the picked models' `genres` (`v-model:genres`). */
98
+ genres?: string[];
99
+ "onUpdate:genres"?: (values: string[]) => void;
100
+ /**
101
+ * Which way the bar's menus open. "above" for a bar docked to the bottom
102
+ * of the screen, "below" for one docked to the top. "auto" (default)
103
+ * measures when a menu opens: below if it fits, else above if that fits,
104
+ * else toward the side with more room.
105
+ */
106
+ menuPlacement?: MenuPlacement;
93
107
  /** The last submit's failure: submit becomes retry, and the message shows inline. */
94
108
  submitError?: string | null;
95
109
  /** Overrides the built-in "empty prompt and no attachments" check. Ignored while `submitError` is set. */
@@ -121,6 +135,9 @@ const props = withDefaults(
121
135
  aspectRatio: null,
122
136
  character: "",
123
137
  frameRate: null,
138
+ speechVoice: null,
139
+ genres: () => [],
140
+ menuPlacement: "auto",
124
141
  submitError: null,
125
142
  // Left undefined unless set: Vue would make an absent boolean `false`,
126
143
  // which reads as "never disable".
@@ -154,6 +171,12 @@ const modelMenuOptions = computed(() =>
154
171
  ? { sections: view.value.modelSections.map((s) => ({ label: s.label, options: s.options })) }
155
172
  : { options: view.value.modelSections[0]?.options }
156
173
  );
174
+ const toMenuOption = (o: { value: string; label: string }) => ({ id: o.value, label: o.label });
175
+ const voiceMenuOptions = computed(() =>
176
+ view.value.voiceSections
177
+ ? { sections: view.value.voiceSections.map((s) => ({ label: s.label, options: s.options.map(toMenuOption) })) }
178
+ : { options: view.value.voiceOptions.map(toMenuOption) }
179
+ );
157
180
 
158
181
  // Auto-select chooses at submit, so turning it on clears the current pick
159
182
  // and the trigger reads "Auto-select" until routing fills in the model.
@@ -301,6 +324,7 @@ function onSubmitClick() {
301
324
  :items="menuItems"
302
325
  :images-only="view.editing"
303
326
  :disabled="disabled"
327
+ :placement="menuPlacement"
304
328
  @select="onAttachMenuSelect?.($event)"
305
329
  @files="onFiles"
306
330
  />
@@ -316,6 +340,7 @@ function onSubmitClick() {
316
340
 
317
341
  <!-- The auto-select switch lives inside the Model menu, so with it shown the trigger stays enabled: it's the only way back to the switch. -->
318
342
  <SearchMenu
343
+ :placement="menuPlacement"
319
344
  v-if="view.modelSections.length > 0 && view.canPickModel"
320
345
  v-bind="modelMenuOptions"
321
346
  :value="view.multiModel ? view.pickedModelIds : modelId"
@@ -335,6 +360,7 @@ function onSubmitClick() {
335
360
  />
336
361
 
337
362
  <SearchMenu
363
+ :placement="menuPlacement"
338
364
  v-if="view.showAspectRatio && view.aspectOptions.length > 0 && props['onUpdate:aspectRatio']"
339
365
  :options="view.aspectOptions.map((o) => ({ id: o.value, label: o.label }))"
340
366
  :value="view.offeredAspect"
@@ -347,6 +373,34 @@ function onSubmitClick() {
347
373
  />
348
374
 
349
375
  <SearchMenu
376
+ :placement="menuPlacement"
377
+ v-if="view.voiceOptions.length > 0 && props['onUpdate:speechVoice']"
378
+ v-bind="voiceMenuOptions"
379
+ :value="view.offeredVoice"
380
+ :trigger-label="view.voiceTriggerLabel"
381
+ menu-label="Voice"
382
+ :searchable="view.voiceOptions.length > view.searchableAt"
383
+ trigger-variant="text"
384
+ :disabled="disabled"
385
+ @select="props['onUpdate:speechVoice']?.($event.id)"
386
+ />
387
+
388
+ <SearchMenu
389
+ :placement="menuPlacement"
390
+ v-if="view.genreOptions.length > 0 && props['onUpdate:genres']"
391
+ :options="view.genreOptions.map(toMenuOption)"
392
+ :value="view.offeredGenres"
393
+ multiple
394
+ :trigger-label="view.genreTriggerLabel"
395
+ menu-label="Genre"
396
+ :searchable="view.genreOptions.length > view.searchableAt"
397
+ trigger-variant="text"
398
+ :disabled="disabled"
399
+ @select="props['onUpdate:genres']?.(view.toggledGenres($event.id))"
400
+ />
401
+
402
+ <SearchMenu
403
+ :placement="menuPlacement"
350
404
  v-if="view.frameRateOptions.length > 0 && props['onUpdate:frameRate']"
351
405
  :options="view.frameRateOptions.map((r) => ({ id: String(r), label: `${r}fps` }))"
352
406
  :value="view.offeredFrameRate == null ? null : String(view.offeredFrameRate)"
@@ -1,24 +1,42 @@
1
1
  <script setup lang="ts">
2
2
  /** The "+" / "×" attach control: a positioned popover of the use-case actions, and the hidden file picker behind "Add media". */
3
- import { ref } from "vue";
4
- import type { ComposerAttachMenuAction, composerAttachMenuItems } from "@chai-ui/core";
3
+ import { nextTick, ref, watch, type Component } from "vue";
4
+ import { menuOpensAbove, type ComposerAttachMenuAction, type composerAttachMenuItems, type MenuPlacement } from "@chai-ui/core";
5
5
  import { useDismiss } from "../dismiss.js";
6
- import { AttachFileIcon, CloseSymbolIcon, ContentCutIcon, ImageIcon, PlusIcon, GlobeIcon, VideocamIcon } from "../icons.js";
6
+ import { AttachFileIcon, CloseSymbolIcon, ContentCutIcon, ImageIcon, MusicNoteIcon, PlusIcon, GlobeIcon, VideocamIcon, VolumeUpIcon } from "../icons.js";
7
7
 
8
- defineProps<{
8
+ const props = defineProps<{
9
9
  items: ReturnType<typeof composerAttachMenuItems>;
10
10
  /** Edit mode: one image file at a time. */
11
11
  imagesOnly?: boolean;
12
12
  disabled?: boolean;
13
+ placement: MenuPlacement;
13
14
  }>();
14
15
  const emit = defineEmits<{ select: [action: ComposerAttachMenuAction]; files: [files: FileList | null] }>();
15
16
 
16
- const ICONS = { "add-media": AttachFileIcon, "create-image": ImageIcon, "create-video": VideocamIcon, "create-world": GlobeIcon, "edit-media": ContentCutIcon };
17
+ const ICONS: Record<ComposerAttachMenuAction, Component> = {
18
+ "add-media": AttachFileIcon,
19
+ "create-image": ImageIcon,
20
+ "create-video": VideocamIcon,
21
+ "create-world": GlobeIcon,
22
+ "edit-image": ContentCutIcon,
23
+ "create-speech": VolumeUpIcon,
24
+ "create-music": MusicNoteIcon,
25
+ };
17
26
  const open = ref(false);
27
+ const above = ref(false);
18
28
  const root = ref<HTMLElement>();
29
+ const menu = ref<HTMLElement>();
19
30
  const input = ref<HTMLInputElement>();
20
31
  useDismiss(root, open, () => (open.value = false));
21
32
 
33
+ // Up or down, before the first paint, so it never flashes on the wrong side.
34
+ watch(open, async (isOpen) => {
35
+ if (!isOpen) return;
36
+ await nextTick();
37
+ above.value = menuOpensAbove(props.placement, root.value!.getBoundingClientRect(), menu.value!.offsetHeight, document.documentElement.clientHeight);
38
+ });
39
+
22
40
  function pick(action: ComposerAttachMenuAction) {
23
41
  open.value = false;
24
42
  if (action === "add-media") input.value?.click();
@@ -45,7 +63,7 @@ function onFileChange(e: Event) {
45
63
  >
46
64
  <CloseSymbolIcon v-if="open" /><PlusIcon v-else />
47
65
  </button>
48
- <ul v-if="open" class="chai-composer__attach-menu" role="menu">
66
+ <ul v-if="open" ref="menu" :class="['chai-composer__attach-menu', { 'chai-composer__attach-menu--above': above }]" role="menu">
49
67
  <li v-for="(item, i) in items" :key="item.action" role="none">
50
68
  <button type="button" role="menuitem" class="chai-composer__attach-menu-item" @click="pick(item.action)">
51
69
  <span class="chai-composer__attach-menu-icon"><component :is="ICONS[item.action]" /></span>{{ item.label }}
package/src/icons.ts CHANGED
@@ -56,4 +56,5 @@ export const ImageIcon = icon("0 -960 960 960", [{ "d": "M180-120q-24 0-42-18t-1
56
56
  export const GlobeIcon = icon("0 -960 960 960", [{ "d": "M480.27-80q-82.74 0-155.5-31.5Q252-143 197.5-197.5t-86-127.34Q80-397.68 80-480.5t31.5-155.66Q143-709 197.5-763t127.34-85.5Q397.68-880 480.5-880t155.66 31.5Q709-817 763-763t85.5 127Q880-563 880-480.27q0 82.74-31.5 155.5Q817-252 763-197.68q-54 54.31-127 86Q563-80 480.27-80Zm-.27-60q142.38 0 241.19-99.5T820-480v-13q-6 26-27.41 43.5Q771.19-432 742-432h-80q-33 0-56.5-23.5T582-512v-40H422v-80q0-33 23.5-56.5T502-712h40v-22q0-16 13.5-40t30.5-29q-25-8-51.36-12.5Q508.29-820 480-820q-141 0-240.5 98.81T140-480h150q66 0 113 47t47 113v40H330v105q34 17 71.7 26t78.3 9Z", "fill": "currentColor" }]);
57
57
  export const VideocamIcon = icon("0 -960 960 960", [{ "d": "M140-160q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h520q24 0 42 18t18 42v215l160-160v410L720-435v215q0 24-18 42t-42 18H140Zm0-60h520v-520H140v520Zm0 0v-520 520Z", "fill": "currentColor" }]);
58
58
  export const ContentCutIcon = icon("0 -960 960 960", [{ "d": "M782-114 481-415 364-298q11 17 13.5 33t2.5 35q0 64-43 107T230-80q-64 0-107-43T80-230q0-64 43-107t107-43q18 0 35.5 5t36.5 15l116-116-118-118q-17 8-34.5 11t-35.5 3q-64 0-107-43T80-730q0-64 43-107t107-43q64 0 107 43t43 107q0 19-2.5 36T367-662l514 514v34h-99ZM599-527l-66-66 249-249h99v33L599-527ZM294-666q26-26 26-64t-26-64q-26-26-64-26t-64 26q-26 26-26 64t26 64q26 26 64 26t64-26Zm202.5 203.5Q502-468 502-476t-5.5-13.5Q491-495 483-495t-13.5 5.5Q464-484 464-476t5.5 13.5Q475-457 483-457t13.5-5.5ZM294-166q26-26 26-64t-26-64q-26-26-64-26t-64 26q-26 26-26 64t26 64q26 26 64 26t64-26Z", "fill": "currentColor" }]);
59
+ export const VolumeUpIcon = icon("0 -960 960 960", [{ "d": "M560-131v-62q97-28 158.5-107.5T780-481q0-101-61-181T560-769v-62q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm420 48v-337q55 17 87.5 64T660-480q0 57-33 104t-87 64ZM420-648 307-540H180v120h127l113 109v-337Zm-94 168Z", "fill": "currentColor" }]);
59
60
  export const MusicNoteIcon = icon("0 -960 960 960", [{ "d": "M286.5-163.5Q243-207 243-270t43.5-106.5Q330-420 393-420q28 0 50.5 8t39.5 22v-450h234v135H543v435q0 63-43.5 106.5T393-120q-63 0-106.5-43.5Z", "fill": "currentColor" }]);
package/src/index.ts CHANGED
@@ -26,6 +26,8 @@ export {
26
26
  DEFAULT_MODEL_ID,
27
27
  DEFAULT_REASONING_MODEL,
28
28
  DEFAULT_REASONING_MODEL_BY_KIND,
29
+ CREATE_MUSIC_USE_CASE,
30
+ CREATE_SPEECH_USE_CASE,
29
31
  EDIT_IMAGE_USE_CASE,
30
32
  TEXT_USE_CASE,
31
33
  PRECISE_EDIT_MODELS,
@@ -43,9 +45,11 @@ export {
43
45
  SUGGESTED_ANALYSIS_MODELS,
44
46
  reasoningModelFor,
45
47
  suggestedModels,
48
+ promptWithGenres,
46
49
  checkSuggestedModels,
47
50
  } from "@chai-ui/core";
48
51
  export type {
52
+ AudioUseCase,
49
53
  ComposerAttachMenuAction,
50
54
  ComposerPromptField,
51
55
  ComposerAttachment,
@@ -63,11 +67,13 @@ export type {
63
67
  MediaAnalyzerAttachment,
64
68
  MediaAnalyzerSubmitPayload,
65
69
  MediaKind,
70
+ MenuPlacement,
66
71
  ModelOption,
67
72
  OutputKind,
68
73
  Transcribe,
69
74
  VoiceClip,
70
75
  VoiceInputConfig,
76
+ VoiceOption,
71
77
  ParameterOption,
72
78
  Reasoning,
73
79
  RegionBox,
@@ -10,6 +10,7 @@ import "@material/web/textfield/outlined-text-field.js";
10
10
  import { computed, nextTick, ref, watch } from "vue";
11
11
  import { useDismiss } from "../dismiss.js";
12
12
  import { CheckIcon } from "../icons.js";
13
+ import { menuOpensAbove, type MenuPlacement } from "@chai-ui/core";
13
14
  import Toggle from "./Toggle.vue";
14
15
  import type { SearchMenuOption, SearchMenuSection, SearchMenuToggleHeader } from "./search-menu.js";
15
16
 
@@ -35,12 +36,15 @@ const props = withDefaults(
35
36
  triggerVariant?: "outlined" | "text";
36
37
  /** Panel width in px; longer labels ellipsize. */
37
38
  panelWidth?: number;
39
+ /** Which way the panel opens: "auto" (default) below when it fits, else above when that fits, else toward the side with more room. */
40
+ placement?: MenuPlacement;
38
41
  }>(),
39
- { searchable: true, triggerVariant: "outlined", panelWidth: 280 }
42
+ { searchable: true, triggerVariant: "outlined", panelWidth: 280, placement: "auto" }
40
43
  );
41
44
  const emit = defineEmits<{ select: [option: T] }>();
42
45
 
43
46
  const open = ref(false);
47
+ const above = ref(false);
44
48
  const search = ref("");
45
49
  const root = ref<HTMLElement>();
46
50
  const panel = ref<HTMLElement>();
@@ -60,6 +64,8 @@ watch(open, async (isOpen) => {
60
64
  const rect = el.getBoundingClientRect();
61
65
  const over = rect.right - (document.documentElement.clientWidth - 16);
62
66
  if (over > 0) el.style.left = `${-Math.min(over, Math.max(0, rect.left - 16))}px`;
67
+ // Up or down, before the first paint, so it never flashes on the wrong side.
68
+ above.value = menuOpensAbove(props.placement, root.value!.getBoundingClientRect(), el.offsetHeight, document.documentElement.clientHeight);
63
69
  });
64
70
 
65
71
  useDismiss(root, open, close);
@@ -101,7 +107,7 @@ function pick(option: T) {
101
107
  </svg>
102
108
  </button>
103
109
 
104
- <div v-if="open" ref="panel" class="chai-search-menu__panel" role="group" :aria-label="menuLabel" :style="{ width: `${panelWidth}px` }">
110
+ <div v-if="open" ref="panel" class="chai-search-menu__panel" :class="{ 'chai-search-menu__panel--above': above }" role="group" :aria-label="menuLabel" :style="{ width: `${panelWidth}px` }">
105
111
  <div v-if="toggleHeader" class="chai-search-menu__toggle-header">
106
112
  <span class="chai-search-menu__toggle-header-label">{{ menuLabel }}</span>
107
113
  <span class="chai-search-menu__toggle-header-row">
@@ -113,7 +119,7 @@ function pick(option: T) {
113
119
  v-if="searchable"
114
120
  class="chai-search-menu__search"
115
121
  :label="menuLabel"
116
- placeholder="Input"
122
+ placeholder="Search"
117
123
  :value.prop="search"
118
124
  @input="search = ($event.target as HTMLInputElement).value"
119
125
  >