1- import { ref , computed , inject , onMounted , shallowRef , watch , nextTick } from "vue"
1+ import { ref , computed , inject , onMounted , onUnmounted , shallowRef , watch , nextTick } from "vue"
22import { useClient , useAuth } from "@servicestack/vue"
33import { Authenticate } from "dtos"
44
5+ import { Features , Components , FeatureGroups } from "./mjs/components/Features.mjs" ;
56import 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"
147import UiHome from "/mjs/components/UiHome.mjs"
158import SignInForm from "/mjs/components/SignInForm.mjs"
169import ShellCommand from "./mjs/components/ShellCommand.mjs"
10+ import CommandPalette from "./mjs/components/CommandPalette.mjs"
1711import { prefixes , icons , uiLabel } from "/mjs/utils.mjs"
1812
1913const HomeSection = {
@@ -22,26 +16,17 @@ const HomeSection = {
2216 component : UiHome
2317}
2418
25- const components = {
26- Chat,
27- TextToImage,
28- ImageToText,
29- ImageToImage,
30- ImageUpscale,
31- SpeechToText,
32- TextToSpeech,
33- // Transform,
34- }
35-
3619export default {
3720 components : {
3821 SignIn,
3922 SignInForm,
4023 ShellCommand,
41- ...components ,
24+ CommandPalette,
25+ ...Components ,
4226 } ,
4327 template : `
4428<div class="min-h-full">
29+ <CommandPalette v-if="showPalette" @done="showPalette=false" />
4530 <nav class="border-b border-gray-200 bg-white">
4631 <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
4732 <div class="flex h-16 justify-between">
@@ -52,13 +37,26 @@ export default {
5237 </a>
5338 </div>
5439 <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']">
5743 <span class="lg:hidden xl:inline mr-2" :title="section.label">
5844 <img :src="section.icon" class="w-6 h-6" :alt="section.label">
5945 </span>
6046 <span class="hidden lg:inline whitespace-nowrap">{{section.label}}</span>
6147 </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>
6260 </div>
6361 </div>
6462 <div class="hidden sm:ml-6 sm:flex sm:items-center">
@@ -81,17 +79,17 @@ export default {
8179 <SecondaryButton @click="routes.to({ admin:'SignIn' })">Sign In</SecondaryButton>
8280 </div>
8381 </div>
84- <div class="-mr-2 flex items-center sm:hidden">
82+ <div @click="showMobileMenu=!showMobileMenu" class="-mr-2 flex items-center sm:hidden">
8583 <!-- Mobile menu button -->
8684 <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">
8785 <span class="absolute -inset-0.5"></span>
8886 <span class="sr-only">Open main menu</span>
8987 <!-- 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">
9189 <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
9290 </svg>
9391 <!-- 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">
9593 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
9694 </svg>
9795 </button>
@@ -100,7 +98,7 @@ export default {
10098 </div>
10199
102100 <!-- 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">
104102 <div class="space-y-1 pb-3 pt-2">
105103 <!-- 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" -->
106104 <a v-href="{admin:section.id,id:undefined}" v-for="section in sections"
@@ -134,7 +132,31 @@ export default {
134132 <div class="mx-auto max-w-7xl pb-8 lg:px-6 lg:px-8">
135133 <SignIn v-if="routes.admin=='SignIn'" />
136134 <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>
138160 </div>
139161 </main>
140162 </div>
@@ -147,22 +169,17 @@ export default {
147169 const { user, hasRole, signIn, signOut } = useAuth ( )
148170 const profileUrl = ref ( localStorage . getItem ( 'profileUrl' ) || user . value ?. profileUrl )
149171 const refreshKey = ref ( 1 )
172+ const showMobileMenu = ref ( false )
150173 const showUserMenu = ref ( false )
174+ const showPalette = ref ( false )
151175
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 )
159177
160178 const overrides = {
161179 ImageUpscale : {
162180 label : 'Upscale' ,
163181 } ,
164182 }
165-
166183 Object . keys ( overrides ) . forEach ( id => {
167184 const section = sections . find ( x => x . id === id )
168185 if ( section ) {
@@ -173,21 +190,36 @@ export default {
173190 }
174191 } )
175192
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 ) ) )
177195
178196 function navTo ( id , args , pushState = true ) {
179197 if ( ! args ) args = { }
180198
181199 refreshKey . value ++
182200 activeSection . value = sections . find ( x => x . id === id ) || HomeSection
201+ activeFeature . value = FeatureGroups . find ( f => f . features . some ( p => p . id === routes . admin ) )
183202 routes . to ( { admin : id , ...args } )
184203 }
185204 watch ( ( ) => routes . admin , ( ) => {
186205 activeSection . value = sections . find ( x => x . id === routes . admin ) || HomeSection
206+ activeFeature . value = FeatureGroups . find ( f => f . features . some ( p => p . id === routes . admin ) )
187207 if ( ! profileUrl . value ) profileUrl . value = localStorage . getItem ( 'profileUrl' ) || user . value ?. profileUrl
188208 refreshKey . value ++
189209 } )
190210
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+
191223 onMounted ( async ( ) => {
192224 const api = await client . api ( new Authenticate ( ) )
193225 if ( api . response ) {
@@ -199,11 +231,19 @@ export default {
199231
200232 console . log ( 'routes.admin' , routes . admin )
201233
234+ window . addEventListener ( 'keydown' , handleKeyDown )
235+
202236 nextTick ( ( ) =>
203237 activeSection . value = sections . find ( x => x . id === routes . admin ) || HomeSection )
204238 } )
205239
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,
207247 icons, navTo,
208248 }
209249 }
0 commit comments