Skip to content

Commit e818f2d

Browse files
committed
feat: oops twenty more features
1 parent 2170f85 commit e818f2d

File tree

11 files changed

+489
-99
lines changed

11 files changed

+489
-99
lines changed

app/components/CommandPalette.client.vue

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
<script setup lang="ts">
22
import type { CommandPaletteCommand } from '~/types/command-palette'
33
4-
const { isOpen, query, close, toggle, view, setView } = useCommandPalette()
5-
const { groupedCommands, flatCommands, hasResults, submitSearchQuery, trimmedQuery } =
6-
useCommandPaletteCommands()
4+
const { isOpen, query, close, toggle, setView } = useCommandPalette()
5+
const {
6+
groupedCommands,
7+
flatCommands,
8+
hasResults,
9+
submitSearchQuery,
10+
trailingCommands,
11+
trimmedQuery,
12+
viewMeta,
13+
} = useCommandPaletteCommands()
714
const keyboardShortcuts = useKeyboardShortcuts()
815
const route = useRoute()
916
@@ -25,10 +32,6 @@ const statusId = `${dialogId}-status`
2532
const resultsId = `${dialogId}-results`
2633
2734
const inputDescribedBy = computed(() => `${descriptionId} ${statusId}`)
28-
const isLanguageView = computed(() => view.value === 'languages')
29-
const modalSubtitle = computed(() =>
30-
isLanguageView.value ? $t('command_palette.subtitle_languages') : $t('command_palette.subtitle'),
31-
)
3235
3336
const statusMessage = computed(() => {
3437
const count = flatCommands.value.length
@@ -56,6 +59,11 @@ function getInputElement() {
5659
return document.querySelector<HTMLInputElement>(`#${inputId}`)
5760
}
5861
62+
function isInputEventTarget(target: EventTarget | null) {
63+
const input = getInputElement()
64+
return !!input && (target === input || document.activeElement === input)
65+
}
66+
5967
function getCommandElements() {
6068
return Array.from(
6169
getDialog()?.querySelectorAll<HTMLButtonElement>('[data-command-item="true"]') ?? [],
@@ -120,7 +128,22 @@ function handleGlobalKeydown(event: KeyboardEvent) {
120128
return
121129
}
122130
123-
if (event.key === 'Enter' && document.activeElement === getInputElement()) {
131+
if (
132+
event.key === 'ArrowLeft' &&
133+
viewMeta.value.canGoBack &&
134+
!query.value &&
135+
!event.altKey &&
136+
!event.ctrlKey &&
137+
!event.metaKey &&
138+
!event.shiftKey &&
139+
isInputEventTarget(event.target)
140+
) {
141+
event.preventDefault()
142+
handleBack()
143+
return
144+
}
145+
146+
if (event.key === 'Enter' && isInputEventTarget(event.target)) {
124147
const firstCommand = flatCommands.value[0]
125148
if (!firstCommand) {
126149
if (!trimmedQuery.value) return
@@ -201,8 +224,8 @@ useEventListener(document, 'keydown', handleGlobalKeydown)
201224
ref="modalRef"
202225
:id="dialogId"
203226
:modalTitle="$t('command_palette.title')"
204-
:modalSubtitle="modalSubtitle"
205-
class="max-w-2xl p-0 overflow-hidden"
227+
:modalSubtitle="viewMeta.subtitle"
228+
class="max-w-[48rem] overflow-hidden p-0"
206229
@close="handleDialogClose"
207230
>
208231
<div class="-mx-6 -mt-6">
@@ -215,7 +238,7 @@ useEventListener(document, 'keydown', handleGlobalKeydown)
215238

216239
<div class="border-b border-border/70 bg-bg-subtle/60 px-4 py-4 sm:px-5">
217240
<button
218-
v-if="isLanguageView"
241+
v-if="viewMeta.canGoBack"
219242
type="button"
220243
class="mb-3 inline-flex min-h-9 items-center gap-2 rounded-lg border border-transparent px-2.5 py-1.5 text-sm text-fg-muted lowercase transition-colors duration-150 hover:border-border/70 hover:bg-bg hover:text-fg focus-visible:outline-accent/70"
221244
@click="handleBack"
@@ -228,8 +251,8 @@ useEventListener(document, 'keydown', handleGlobalKeydown)
228251
:id="inputId"
229252
ref="inputRef"
230253
v-model="query"
231-
type="search"
232-
:placeholder="$t('command_palette.placeholder')"
254+
type="text"
255+
:placeholder="viewMeta.placeholder"
233256
no-correct
234257
size="large"
235258
class="w-full"
@@ -274,7 +297,7 @@ useEventListener(document, 'keydown', handleGlobalKeydown)
274297
<li v-for="command in group.items" :key="command.id">
275298
<button
276299
type="button"
277-
class="min-h-12 w-full cursor-pointer rounded-lg border border-transparent px-3 py-2.5 text-start transition-colors duration-150 hover:border-border/80 hover:bg-bg focus-visible:outline-accent/70"
300+
class="min-h-12 w-full rounded-lg border border-transparent px-3 py-2.5 text-start transition-colors duration-150 hover:border-border/80 hover:bg-bg focus-visible:outline-accent/70"
278301
:class="
279302
activeIndex === (commandIndexMap.get(command.id) ?? -1)
280303
? 'border-border/80 bg-bg'
@@ -309,6 +332,12 @@ useEventListener(document, 'keydown', handleGlobalKeydown)
309332
>
310333
{{ command.activeLabel || $t('command_palette.current') }}
311334
</span>
335+
<span
336+
v-if="command.previewColor"
337+
class="inline-block h-3.5 w-3.5 shrink-0 rounded-full border border-border/70"
338+
:style="{ backgroundColor: command.previewColor }"
339+
aria-hidden="true"
340+
/>
312341
<span
313342
v-if="command.external"
314343
class="i-lucide:external-link inline-block h-3.5 w-3.5 shrink-0 text-fg-subtle"
@@ -322,6 +351,38 @@ useEventListener(document, 'keydown', handleGlobalKeydown)
322351
</li>
323352
</ul>
324353
</section>
354+
355+
<ul v-if="trailingCommands.length" class="m-0 flex list-none flex-col gap-1 p-0">
356+
<li v-for="command in trailingCommands" :key="command.id">
357+
<button
358+
type="button"
359+
class="min-h-12 w-full rounded-xl border border-border/70 bg-bg-subtle/70 px-3 py-2.5 text-start transition-colors duration-150 hover:border-border/80 hover:bg-bg focus-visible:outline-accent/70"
360+
:class="
361+
activeIndex === (commandIndexMap.get(command.id) ?? -1)
362+
? 'border-border/80 bg-bg'
363+
: ''
364+
"
365+
data-command-item="true"
366+
:aria-current="command.active ? 'true' : undefined"
367+
@click="void handleCommandSelect(command)"
368+
@focus="activeIndex = commandIndexMap.get(command.id) ?? -1"
369+
@mouseenter="activeIndex = commandIndexMap.get(command.id) ?? -1"
370+
>
371+
<span class="flex items-center gap-3">
372+
<span
373+
class="inline-block h-4 w-4 shrink-0 text-fg-subtle"
374+
:class="command.iconClass"
375+
aria-hidden="true"
376+
/>
377+
<span class="min-w-0 flex-1">
378+
<span class="block truncate text-base text-fg lowercase">
379+
{{ command.label }}
380+
</span>
381+
</span>
382+
</span>
383+
</button>
384+
</li>
385+
</ul>
325386
</div>
326387
</div>
327388
</div>

app/components/Terminal/Install.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ useCommandPaletteContextCommands(
184184
{
185185
id: 'package-open-create-info',
186186
group: 'package',
187-
label: $t('package.create.view', { packageName: props.createPackageInfo.packageName }),
187+
label: $t('command_palette.package_actions.open_create', {
188+
packageName: props.createPackageInfo.packageName,
189+
}),
188190
keywords: [props.packageName, props.createPackageInfo.packageName],
189191
iconClass: 'i-lucide:info',
190192
action: async () => {

app/composables/useCommandPaletteCommands.ts

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const GROUP_ORDER: CommandPaletteGroup[] = [
1414
'navigation',
1515
'connections',
1616
'links',
17-
'theme',
17+
'settings',
18+
'help',
1819
'npmx',
1920
'versions',
2021
]
@@ -45,6 +46,8 @@ export function useCommandPaletteCommands() {
4546
switch (group) {
4647
case 'actions':
4748
return t('command_palette.groups.actions')
49+
case 'help':
50+
return t('command_palette.groups.help')
4851
case 'language':
4952
return t('command_palette.groups.language')
5053
case 'package':
@@ -57,8 +60,8 @@ export function useCommandPaletteCommands() {
5760
return t('command_palette.groups.connections')
5861
case 'links':
5962
return t('command_palette.groups.links')
60-
case 'theme':
61-
return t('command_palette.groups.theme')
63+
case 'settings':
64+
return t('nav.settings')
6265
case 'npmx':
6366
return t('command_palette.groups.npmx')
6467
case 'versions':
@@ -82,9 +85,15 @@ export function useCommandPaletteCommands() {
8285
}
8386

8487
const trimmedQuery = computed(() => query.value.trim())
85-
const { globalCommands, languageCommands } = useCommandPaletteGlobalCommands({
88+
const { globalCommands, viewDefinitions } = useCommandPaletteGlobalCommands({
8689
submitSearchQuery,
8790
})
91+
const currentViewDefinition = computed(() =>
92+
view.value === 'root' ? null : viewDefinitions.value[view.value],
93+
)
94+
const rootSearchCommands = computed<CommandPaletteCommand[]>(() =>
95+
Object.values(viewDefinitions.value).flatMap(definition => definition.rootSearchCommands ?? []),
96+
)
8897

8998
const registeredCommands = computed<CommandPaletteCommand[]>(() =>
9099
contextCommands.value.flatMap(entry =>
@@ -104,9 +113,28 @@ export function useCommandPaletteCommands() {
104113
...globalCommands.value,
105114
...registeredCommands.value,
106115
])
116+
const trailingSearchCommand = computed<CommandPaletteCommand | null>(() => {
117+
if (view.value !== 'root' || !trimmedQuery.value) return null
118+
119+
const searchCommand = rootViewCommands.value.find(command => command.id === 'search')
120+
if (!searchCommand) return null
121+
122+
return {
123+
...searchCommand,
124+
label: t('command_palette.actions.search_for', { query: trimmedQuery.value }),
125+
keywords: [...searchCommand.keywords, trimmedQuery.value],
126+
}
127+
})
107128

108129
const commands = computed(() =>
109-
view.value === 'languages' ? languageCommands.value : rootViewCommands.value,
130+
view.value !== 'root'
131+
? (currentViewDefinition.value?.commands ?? [])
132+
: trimmedQuery.value
133+
? [
134+
...rootViewCommands.value.filter(command => command.id !== 'search'),
135+
...rootSearchCommands.value,
136+
]
137+
: rootViewCommands.value,
110138
)
111139

112140
const { results } = useFuse(query, commands, {
@@ -121,16 +149,6 @@ export function useCommandPaletteCommands() {
121149
const matchedCommands = computed(() => {
122150
let nextCommands = results.value.map(result => result.item)
123151

124-
if (view.value === 'root' && trimmedQuery.value) {
125-
const searchCommand = rootViewCommands.value.find(command => command.id === 'search')
126-
if (searchCommand) {
127-
nextCommands = [
128-
searchCommand,
129-
...nextCommands.filter(command => command.id !== searchCommand.id),
130-
]
131-
}
132-
}
133-
134152
queryOverrides.value.forEach(({ scopeId, group }) => {
135153
const resolve = resolveQueryOverride(scopeId, group)
136154
if (!resolve) return
@@ -147,6 +165,9 @@ export function useCommandPaletteCommands() {
147165

148166
return nextCommands
149167
})
168+
const trailingCommands = computed<CommandPaletteCommand[]>(() =>
169+
trailingSearchCommand.value ? [trailingSearchCommand.value] : [],
170+
)
150171
const groupedCommands = computed<CommandPaletteCommandGroup[]>(() =>
151172
GROUP_ORDER.map(group => {
152173
const items = matchedCommands.value.filter(command => command.group === group)
@@ -159,12 +180,21 @@ export function useCommandPaletteCommands() {
159180
}).filter(group => group.items.length > 0),
160181
)
161182

162-
const flatCommands = computed(() => groupedCommands.value.flatMap(group => group.items))
183+
const flatCommands = computed(() => [
184+
...groupedCommands.value.flatMap(group => group.items),
185+
...trailingCommands.value,
186+
])
163187

164188
return {
165189
groupedCommands,
166190
flatCommands,
191+
trailingCommands,
167192
hasResults: computed(() => flatCommands.value.length > 0),
193+
viewMeta: computed(() => ({
194+
canGoBack: view.value !== 'root',
195+
placeholder: currentViewDefinition.value?.placeholder ?? t('command_palette.placeholder'),
196+
subtitle: currentViewDefinition.value?.subtitle ?? t('command_palette.subtitle'),
197+
})),
168198
submitSearchQuery,
169199
trimmedQuery,
170200
}

0 commit comments

Comments
 (0)