@chai-ui/vue 0.10.1 → 0.11.1
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/DESIGN.md +6 -0
- package/README.md +4 -0
- package/dist/Composer.vue.d.ts +17 -1
- package/dist/Composer.vue.d.ts.map +1 -1
- package/dist/composer/AttachMenu.vue.d.ts +2 -1
- package/dist/composer/AttachMenu.vue.d.ts.map +1 -1
- package/dist/icons.d.ts +1 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +96 -31
- package/dist/index.js.map +1 -1
- package/dist/primitives/SearchMenu.vue.d.ts +3 -0
- package/dist/primitives/SearchMenu.vue.d.ts.map +1 -1
- package/dist/style.css +47 -12
- package/package.json +3 -3
- package/src/Composer.vue +54 -0
- package/src/composer/AttachMenu.vue +24 -6
- package/src/icons.ts +1 -0
- package/src/index.ts +6 -0
- package/src/primitives/SearchMenu.vue +9 -3
|
@@ -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":"
|
|
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
|
-
|
|
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-
|
|
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-
|
|
361
|
+
font-size: var(--chai-type-size-label-large);
|
|
345
362
|
font-weight: var(--chai-type-weight-material-medium);
|
|
346
|
-
|
|
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:
|
|
878
|
-
height:
|
|
897
|
+
width: 40px;
|
|
898
|
+
height: 40px;
|
|
899
|
+
margin: -8px;
|
|
879
900
|
padding: 0;
|
|
880
901
|
border: none;
|
|
881
|
-
border-radius: var(--chai-radius-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
950
|
-
|
|
951
|
-
|
|
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.
|
|
3
|
+
"version": "0.11.1",
|
|
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/
|
|
51
|
-
"@chai-ui/
|
|
50
|
+
"@chai-ui/core": "0.11.1",
|
|
51
|
+
"@chai-ui/tokens": "0.11.1"
|
|
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
|
|
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
|
|
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="
|
|
122
|
+
placeholder="Search"
|
|
117
123
|
:value.prop="search"
|
|
118
124
|
@input="search = ($event.target as HTMLInputElement).value"
|
|
119
125
|
>
|