Skip to content

Conversation

@MatthewAry
Copy link
Contributor

@MatthewAry MatthewAry commented Dec 4, 2025

Description

Adds a new VCommandPalette component

Markup:

<script setup lang="ts">
  /* eslint-disable no-console */
  import { ref } from 'vue'
  import { useTheme } from 'vuetify'

  const model = ref(false)
  const showSnack = ref(false)
  const lastExecutedCommand = ref('')
  const search = ref('')

  // Theme management
  const theme = useTheme()

  const items = [
    // File Operations
    {
      type: 'subheader' as const,
      title: 'File Operations',
    },
    {
      title: 'New File',
      subtitle: 'Create a new file',
      value: 'new:file',
      prependIcon: 'mdi-file-plus',
      onClick: () => {
        lastExecutedCommand.value = 'New File created'
        showSnack.value = true
        console.log('Creating new file')
      },
    },
    {
      title: 'New Folder',
      subtitle: 'Create a new folder',
      value: 'new:folder',
      prependIcon: 'mdi-folder-plus',
      onClick: () => {
        lastExecutedCommand.value = 'New Folder created'
        showSnack.value = true
        console.log('Creating new folder')
      },
    },
    {
      title: 'Open File',
      subtitle: 'Open an existing file',
      value: 'file:open',
      prependIcon: 'mdi-folder-open',
      onClick: () => {
        lastExecutedCommand.value = 'Open File dialog opened'
        showSnack.value = true
        console.log('Opening file dialog')
      },
    },
    {
      title: 'Save',
      subtitle: 'Save the current file',
      value: 'file:save',
      prependIcon: 'mdi-content-save',
      hotkey: 'ctrl+s',
      onClick: () => {
        lastExecutedCommand.value = 'File saved'
        showSnack.value = true
        console.log('Saving file')
      },
    },
    {
      type: 'divider' as const,
    },
    // Git Operations
    {
      type: 'subheader' as const,
      title: 'Git Operations',
    },
    {
      title: 'Commit',
      subtitle: 'Commit changes',
      value: 'git:commit',
      prependIcon: 'mdi-source-commit',
      hotkey: 'ctrl+shift+c',
      onClick: () => {
        lastExecutedCommand.value = 'Git Commit'
        showSnack.value = true
        console.log('Committing changes')
      },
    },
    {
      title: 'Push',
      subtitle: 'Push changes to remote',
      value: 'git:push',
      prependIcon: 'mdi-source-pull',
      hotkey: 'ctrl+shift+p',
      onClick: () => {
        lastExecutedCommand.value = 'Git Push'
        showSnack.value = true
        console.log('Pushing changes')
      },
    },
    {
      title: 'Fetch',
      subtitle: 'Fetch from remote',
      value: 'git:fetch',
      prependIcon: 'mdi-source-branch',
      onClick: () => {
        lastExecutedCommand.value = 'Git Fetch'
        showSnack.value = true
        console.log('Fetching changes')
      },
    },
    {
      type: 'divider' as const,
    },
    // Preferences
    {
      type: 'subheader' as const,
      title: 'Preferences',
    },
    {
      title: 'Settings',
      subtitle: 'Open settings',
      value: 'pref:settings',
      prependIcon: 'mdi-cog',
      onClick: () => {
        lastExecutedCommand.value = 'Settings opened'
        showSnack.value = true
        console.log('Opening settings')
      },
    },
    {
      title: 'Toggle Theme',
      subtitle: 'Switch between light and dark theme',
      value: 'theme:toggle',
      prependIcon: 'mdi-palette',
      hotkey: 'ctrl+t',
      onClick: () => {
        theme.toggle()
        lastExecutedCommand.value = `Switched to ${theme.name.value} theme`
        showSnack.value = true
      },
    },
    {
      type: 'divider' as const,
    },
    // Search & Tools
    {
      type: 'subheader' as const,
      title: 'Search & Tools',
    },
    {
      title: 'Find',
      subtitle: 'Find in the current file',
      value: 'find',
      prependIcon: 'mdi-magnify',
      hotkey: 'ctrl+f',
      onClick: () => {
        lastExecutedCommand.value = 'Find dialog opened'
        showSnack.value = true
        console.log('Finding in file')
      },
    },
    {
      title: 'Find in Files',
      subtitle: 'Find in the entire workspace',
      value: 'find:files',
      prependIcon: 'mdi-file-find',
      hotkey: 'ctrl+shift+f',
      onClick: () => {
        lastExecutedCommand.value = 'Find in Files opened'
        showSnack.value = true
        console.log('Finding in files')
      },
    },
    {
      title: 'Replace',
      subtitle: 'Find and replace text',
      value: 'find:replace',
      prependIcon: 'mdi-find-replace',
      hotkey: 'ctrl+h',
      onClick: () => {
        lastExecutedCommand.value = 'Find and Replace opened'
        showSnack.value = true
        console.log('Find and replace')
      },
    },
  ]
</script>

<template>
  <v-app>
    <v-container class="pa-4">
      <v-row align="center" class="mb-4">
        <v-col cols="auto">
          <h1 class="text-h4">VCommandPalette Playground</h1>
        </v-col>
        <v-spacer />
        <v-col cols="auto">
          <v-btn
            :icon="theme.name.value === 'dark' ? 'mdi-weather-sunny' : 'mdi-weather-night'"
            :title="`Switch to ${theme.name.value === 'dark' ? 'light' : 'dark'} theme`"
            @click="theme.toggle()"
          />
        </v-col>
      </v-row>

      <v-row class="mb-4">
        <v-col>
          <v-card>
            <v-card-title>Command Palette Demo</v-card-title>
            <v-card-text>
              <p>This playground demonstrates the VCommandPalette component.</p>
              <p class="text-caption text-disabled mb-2"><strong>Keyboard shortcuts:</strong></p>
              <ul class="text-caption" style="list-style-position: inside;">
                <li class="mb-1">
                  <v-hotkey keys="ctrl+shift+p" size="x-small" inline /> - Open command palette
                </li>
                <li class="mb-1">
                  <v-hotkey keys="arrowup arrowdown" size="x-small" inline /> - Navigate items
                </li>
                <li class="mb-1">
                  <v-hotkey keys="enter" size="x-small" inline /> - Execute selected item
                </li>
                <li class="mb-1">
                  <v-hotkey keys="escape" size="x-small" inline /> - Close palette
                </li>
                <li class="mb-1">
                  <v-hotkey keys="ctrl+s" size="x-small" inline /> - Save (item hotkey)
                </li>
                <li class="mb-1">
                  <v-hotkey keys="ctrl+t" size="x-small" inline /> - Toggle theme (item hotkey)
                </li>
                <li>
                  <v-hotkey keys="ctrl+f" size="x-small" inline /> - Find (item hotkey)
                </li>
              </ul>
            </v-card-text>
            <v-card-actions>
              <v-btn
                color="primary"
                prepend-icon="mdi-console"
                @click="model = !model"
              >
                Open Command Palette
              </v-btn>
              <v-spacer />
              <v-chip v-if="search" size="small">
                Search: {{ search }}
              </v-chip>
            </v-card-actions>
          </v-card>
        </v-col>
      </v-row>

      <v-row v-if="lastExecutedCommand">
        <v-col>
          <v-alert
            type="info"
            variant="tonal"
            closable
            @click:close="lastExecutedCommand = ''"
          >
            Last executed: <strong>{{ lastExecutedCommand }}</strong>
          </v-alert>
        </v-col>
      </v-row>
    </v-container>

    <!-- VCommandPalette Component -->
    <v-command-palette
      v-model="model"
      v-model:search="search"
      :items="items"
      hotkey="ctrl+shift+p"
      max-height="450px"
      max-width="700px"
      placeholder="Type a command or search..."
    >
      <template #prepend>
        <div class="pa-2 text-center text-caption text-disabled">
          <strong>💡 Tip:</strong> Use
          <v-hotkey keys="ctrl+shift+p" size="small" inline />
          to open anytime
        </div>
      </template>

      <template #append>
        <v-divider class="mt-2" />
        <div class="pa-2 text-center text-caption text-disabled">
          <v-hotkey keys="arrowup arrowdown" size="x-small" inline />
          to navigate •
          <v-hotkey keys="enter" size="x-small" inline />
          to select •
          <v-hotkey keys="escape" size="x-small" inline />
          to close
        </div>
      </template>
    </v-command-palette>

    <!-- Snackbar for feedback -->
    <v-snackbar
      v-model="showSnack"
      :timeout="2000"
      location="bottom"
    >
      {{ lastExecutedCommand }}
    </v-snackbar>
  </v-app>
</template>

- Added VCommandPalette component for a keyboard-driven command interface.
- Implemented props for items, search, hotkeys, and dialog configuration.
- Included examples and documentation for usage, API, and accessibility features.
- Enhanced navigation and filtering capabilities
@MatthewAry MatthewAry changed the base branch from master to dev December 4, 2025 15:38
- Removed unnecessary comments in VCommandPalette and its related files.
- Updated slot descriptions in VList.json for better guidance.
- Changes to useCommandPaletteNavigation for improved item selection.
- Simplified props handling in VCommandPaletteItem
- Simplified item rendering logic in VCommandPaletteItemComponent.
- Enhanced navigation logic to prevent unnecessary index resets in useCommandPaletteNavigation.
- Revised localization strings for clarity and conciseness in VCommandPalette.json.
- Updated examples and documentation to reflect changes in hotkey usage and item properties.
- Improved descriptions for props, events, and slots in the command palette documentation.
@MatthewAry MatthewAry marked this pull request as ready for review December 5, 2025 15:24
@J-Sek J-Sek added the C: New Component This issue would need a new component to be developed. label Dec 6, 2025
Copy link
Contributor

@J-Sek J-Sek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how slim it got. Great job.
These are just some quick comments.
Hopefully, I will have some time next week to go through examples and maybe add some more.

Comment on lines 259 to 264
prependInnerIcon="mdi-magnify"
singleLine
hideDetails
variant="solo"
flat
bgColor="transparent"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably supplement a slot #input and input-icon prop

>
{ slots.prepend?.() }

<div class="px-4 py-2">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v-command-palette__input-container?
Hard to name it, but we need some class to push the padding to Scss

key={ `item-${props.index}` }
item={ props }
index={ props.index }
onExecute={ navigation.executeSelected }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat wrong - assumes the item is selected, but gets passed down directly to onClick inside.. clearly a bug when I mouse-click on an unselected item (the selected is executed instead).

I'd probably just bind straight to onClick here and call navigation.execute(props.item.key) or *.index if we are sure it will never be virtualized (or out of sync for any reason).

}}
/>
) : (
<div key="no-data" class="pa-4 text-center text-disabled">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v-command-palette__no-data to push the utilities to Scss

- Added `inputIcon` property to customize the search input icon in VCommandPalette.
- Introduced custom input slot for more flexible search input implementations.
- Updated styles for input container and no-data display for improved UI consistency.
- Implemented cleanup for the hotkey listener to prevent memory leaks.
- Added `onUnmounted` lifecycle hook to clear the DOM reference and unsubscribe from hotkey events.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C: New Component This issue would need a new component to be developed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants