1
- import { ref , computed , inject , onMounted , shallowRef , watch , nextTick } from "vue"
1
+ import { ref , computed , inject , onMounted , onUnmounted , shallowRef , watch , nextTick } from "vue"
2
2
import { useClient , useAuth } from "@servicestack/vue"
3
3
import { Authenticate } from "dtos"
4
4
5
+ import { Features , Components , FeatureGroups } from "./mjs/components/Features.mjs" ;
5
6
import SignIn from "/mjs/components/SignIn.mjs"
6
- import Chat from "/mjs/components/Chat.mjs"
7
- import TextToImage from "/mjs/components/TextToImage.mjs"
8
- import ImageToText from "/mjs/components/ImageToText.mjs"
9
- import ImageToImage from "/mjs/components/ImageToImage.mjs"
10
- import ImageUpscale from "/mjs/components/ImageUpscale.mjs"
11
- import SpeechToText from "/mjs/components/SpeechToText.mjs"
12
- import TextToSpeech from "/mjs/components/TextToSpeech.mjs"
13
- import Transform from "/mjs/components/Transform.mjs"
14
7
import UiHome from "/mjs/components/UiHome.mjs"
15
8
import SignInForm from "/mjs/components/SignInForm.mjs"
16
9
import ShellCommand from "./mjs/components/ShellCommand.mjs"
10
+ import CommandPalette from "./mjs/components/CommandPalette.mjs"
17
11
import { prefixes , icons , uiLabel } from "/mjs/utils.mjs"
18
12
19
13
const HomeSection = {
@@ -22,26 +16,17 @@ const HomeSection = {
22
16
component : UiHome
23
17
}
24
18
25
- const components = {
26
- Chat,
27
- TextToImage,
28
- ImageToText,
29
- ImageToImage,
30
- ImageUpscale,
31
- SpeechToText,
32
- TextToSpeech,
33
- // Transform,
34
- }
35
-
36
19
export default {
37
20
components : {
38
21
SignIn,
39
22
SignInForm,
40
23
ShellCommand,
41
- ...components ,
24
+ CommandPalette,
25
+ ...Components ,
42
26
} ,
43
27
template : `
44
28
<div class="min-h-full">
29
+ <CommandPalette v-if="showPalette" @done="showPalette=false" />
45
30
<nav class="border-b border-gray-200 bg-white">
46
31
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
47
32
<div class="flex h-16 justify-between">
@@ -52,13 +37,26 @@ export default {
52
37
</a>
53
38
</div>
54
39
<div class="hidden sm:-my-px sm:ml-2 lg:ml-4 sm:flex sm:space-x-4 xl:space-x-6">
55
- <a v-href="{admin:section.id,id:undefined}" v-for="section in sections"
56
- :class="['inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium', routes.admin==section.id ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700']" aria-current="page">
40
+ <a v-for="section in FeatureGroups" v-href="{admin:section.features[0]?.id,id:undefined}" aria-current="page"
41
+ :class="['inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium',
42
+ section.features.some(x => x.id === routes.admin) ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700']">
57
43
<span class="lg:hidden xl:inline mr-2" :title="section.label">
58
44
<img :src="section.icon" class="w-6 h-6" :alt="section.label">
59
45
</span>
60
46
<span class="hidden lg:inline whitespace-nowrap">{{section.label}}</span>
61
47
</a>
48
+ <div class="flex items-center">
49
+ <button type="button" aria-label="Search" @click="showPalette=!showPalette"
50
+ class="flex items-center gap-1 rounded-full bg-gray-50 px-2 py-1 ring-1 ring-gray-200 hover:ring-green-500 text-gray-400 hover:text-gray-600">
51
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" data-slot="icon" class="-ml-0.5 size-4 fill-gray-400 hover:fill-gray-600"><path fill-rule="evenodd" d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z" clip-rule="evenodd"></path></svg>
52
+ <span class="text-sm">Search</span>
53
+ <span class="hidden md:block text-gray-400 text-sm leading-5 py-0 px-1.5 mr-1.5 border border-gray-300 border-solid rounded-md" style="opacity: 1;">
54
+ <span class="sr-only">Press </span>
55
+ <kbd class="font-sans">/</kbd>
56
+ <span class="sr-only"> to search</span>
57
+ </span>
58
+ </button>
59
+ </div>
62
60
</div>
63
61
</div>
64
62
<div class="hidden sm:ml-6 sm:flex sm:items-center">
@@ -81,17 +79,17 @@ export default {
81
79
<SecondaryButton @click="routes.to({ admin:'SignIn' })">Sign In</SecondaryButton>
82
80
</div>
83
81
</div>
84
- <div class="-mr-2 flex items-center sm:hidden">
82
+ <div @click="showMobileMenu=!showMobileMenu" class="-mr-2 flex items-center sm:hidden">
85
83
<!-- Mobile menu button -->
86
84
<button type="button" class="relative inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" aria-controls="mobile-menu" aria-expanded="false">
87
85
<span class="absolute -inset-0.5"></span>
88
86
<span class="sr-only">Open main menu</span>
89
87
<!-- Menu open: "hidden", Menu closed: "block" -->
90
- <svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
88
+ <svg : class="showMobileMenu ? 'hidden' : ' block h-6 w-6' " fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
91
89
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
92
90
</svg>
93
91
<!-- Menu open: "block", Menu closed: "hidden" -->
94
- <svg class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
92
+ <svg : class="showMobileMenu ? 'block h-6 w-6': 'hidden' " fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
95
93
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
96
94
</svg>
97
95
</button>
@@ -100,7 +98,7 @@ export default {
100
98
</div>
101
99
102
100
<!-- Mobile menu, show/hide based on menu state. -->
103
- <div v-if="user" class="sm:hidden" id="mobile-menu">
101
+ <div v-if="user && showMobileMenu " class="sm:hidden" id="mobile-menu">
104
102
<div class="space-y-1 pb-3 pt-2">
105
103
<!-- Current: "border-indigo-500 bg-indigo-50 text-indigo-700", Default: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800" -->
106
104
<a v-href="{admin:section.id,id:undefined}" v-for="section in sections"
@@ -134,7 +132,31 @@ export default {
134
132
<div class="mx-auto max-w-7xl pb-8 lg:px-6 lg:px-8">
135
133
<SignIn v-if="routes.admin=='SignIn'" />
136
134
<SignInForm v-else-if="routes.admin && !user" />
137
- <component v-else :key="refreshKey" :is="activeSection.component"></component>
135
+ <div v-else :key="refreshKey">
136
+ <div v-if="activeFeature?.features?.length > 0" class="border-b py-2">
137
+ <div class="grid grid-cols-1 sm:hidden">
138
+ <!-- Use an "onChange" listener to redirect the user to the selected tab URL. -->
139
+ <select aria-label="Select a tab" class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-2 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600"
140
+ @change="routes.to({ admin:$event.target.value,id:undefined })">
141
+ <option v-for="feature in activeFeature.features" :value="feature.id" :selected="feature.id==routes.admin">{{feature.label}}</option>
142
+ </select>
143
+ <svg class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end fill-gray-500" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" data-slot="icon">
144
+ <path fill-rule="evenodd" d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
145
+ </svg>
146
+ </div>
147
+ <div class="hidden sm:block">
148
+ <nav class="flex space-x-4" aria-label="Tabs">
149
+ <!-- Current: "bg-indigo-100 text-indigo-700", Default: "text-gray-500 hover:text-gray-700" -->
150
+ <a v-for="feature in activeFeature.features" v-href="{admin:feature.id,id:undefined}"
151
+ :class="['rounded-md px-3 py-2 text-sm font-medium',
152
+ feature.id==routes.admin ? 'bg-indigo-100 text-indigo-700' : 'text-gray-500 hover:text-gray-700']">
153
+ {{feature.label}}
154
+ </a>
155
+ </nav>
156
+ </div>
157
+ </div>
158
+ <component :is="activeSection.component"></component>
159
+ </div>
138
160
</div>
139
161
</main>
140
162
</div>
@@ -147,22 +169,17 @@ export default {
147
169
const { user, hasRole, signIn, signOut } = useAuth ( )
148
170
const profileUrl = ref ( localStorage . getItem ( 'profileUrl' ) || user . value ?. profileUrl )
149
171
const refreshKey = ref ( 1 )
172
+ const showMobileMenu = ref ( false )
150
173
const showUserMenu = ref ( false )
174
+ const showPalette = ref ( false )
151
175
152
- const sections = Object . keys ( components ) . map ( id => ( {
153
- id,
154
- label : uiLabel ( id ) ,
155
- component : components [ id ] ,
156
- icon : icons [ prefixes [ id ] ] ,
157
- prefix : prefixes [ id ] ,
158
- } ) )
176
+ const sections = Object . values ( Features )
159
177
160
178
const overrides = {
161
179
ImageUpscale : {
162
180
label : 'Upscale' ,
163
181
} ,
164
182
}
165
-
166
183
Object . keys ( overrides ) . forEach ( id => {
167
184
const section = sections . find ( x => x . id === id )
168
185
if ( section ) {
@@ -173,21 +190,36 @@ export default {
173
190
}
174
191
} )
175
192
176
- const activeSection = shallowRef ( sections [ routes . admin ] || HomeSection )
193
+ const activeSection = shallowRef ( sections . find ( x => x . id === routes . admin ) || HomeSection )
194
+ const activeFeature = shallowRef ( FeatureGroups . find ( f => f . features . some ( p => p . id === routes . admin ) ) )
177
195
178
196
function navTo ( id , args , pushState = true ) {
179
197
if ( ! args ) args = { }
180
198
181
199
refreshKey . value ++
182
200
activeSection . value = sections . find ( x => x . id === id ) || HomeSection
201
+ activeFeature . value = FeatureGroups . find ( f => f . features . some ( p => p . id === routes . admin ) )
183
202
routes . to ( { admin : id , ...args } )
184
203
}
185
204
watch ( ( ) => routes . admin , ( ) => {
186
205
activeSection . value = sections . find ( x => x . id === routes . admin ) || HomeSection
206
+ activeFeature . value = FeatureGroups . find ( f => f . features . some ( p => p . id === routes . admin ) )
187
207
if ( ! profileUrl . value ) profileUrl . value = localStorage . getItem ( 'profileUrl' ) || user . value ?. profileUrl
188
208
refreshKey . value ++
189
209
} )
190
210
211
+ function handleKeyDown ( e ) {
212
+ //console.log('handleKeyDown', e)
213
+ if ( e . code === 'Slash' ) {
214
+ showPalette . value = true
215
+ e . preventDefault ( )
216
+ }
217
+ if ( e . code === 'Escape' ) {
218
+ showPalette . value = false
219
+ e . preventDefault ( )
220
+ }
221
+ }
222
+
191
223
onMounted ( async ( ) => {
192
224
const api = await client . api ( new Authenticate ( ) )
193
225
if ( api . response ) {
@@ -199,11 +231,19 @@ export default {
199
231
200
232
console . log ( 'routes.admin' , routes . admin )
201
233
234
+ window . addEventListener ( 'keydown' , handleKeyDown )
235
+
202
236
nextTick ( ( ) =>
203
237
activeSection . value = sections . find ( x => x . id === routes . admin ) || HomeSection )
204
238
} )
205
239
206
- return { routes, user, hasRole, sections, activeSection, profileUrl, refreshKey, showUserMenu,
240
+ onUnmounted ( ( ) => {
241
+ window . removeEventListener ( 'keydown' , handleKeyDown )
242
+ } )
243
+
244
+ return { refreshKey, routes, user, hasRole, profileUrl,
245
+ FeatureGroups, sections, activeSection, activeFeature,
246
+ showMobileMenu, showUserMenu, showPalette,
207
247
icons, navTo,
208
248
}
209
249
}
0 commit comments