👋 Introduction
vue-cmd-menu
lets you built a beatiful, fast and extensible command menu like the ones you may know from Vercel, GitHub and Linear or MacOS's Spotlight/Raycast. It can be invoked anywhere in your app via Command + k to perform actions users would typically be able to do via an interface.
-
💅 Beautiful by default, easy to style to your liking -
🔎 Built-in filtering with Fuse.js -
⌨️ Keyboard shortcuts for registering keystrokes to specific actions -
🧭 Easy navigation with your keyboard or mouse -
📁 Nested actions for folder like navigation experience -
🔩 Simple data structure to define and customise actions
🚀 Get started
Install it via NPM:
npm install vue-cmd-menu
It currently only supports Vue 2
Global Usage
import Vue from 'vue';
import CommandMenu from 'vue-cmd-menu';
Vue.component('CommandMenu', CommandMenu);
In Single File Components
import CommandMenu from 'vue-cmd-menu';
export default {
// ...
components: {
CommandMenu,
},
// ...
};
📚 Usage
While command menus sound easy to build in theory, in practice handling the different interactions, nagivation options and state management can quickly become complicated. vue-cmd-menu
provides a simple abstraction over this, simply pass it the actions you want your users to be able to perform and it will handle the rest.
<template>
<div id="app">
<CommandMenu :actions="commandItems"></CommandMenu>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import CommandMenu from 'vue-cmd-menu';
export default Vue.extend({
components: {
CommandMenu
},
computed: {
actions() {
return [
{
id: 'new',
text: 'Create New Page',
tag: 'New',
childPlaceholder: 'Page title',
action: (value) => {
// Handle the action, value contains the entered title
console.log(value)
}
},
{
id: 'docs',
text: 'Documentation',
keybindings: [ '?' ],
childPlaceholder: 'Search Docs',
childActions: [
{
id: 'overview',
text: 'Overview',
action: () => {}
},
{
id: 'get-started',
text: 'Get Started',
action: () => {}
}
]
}
]
}
}
})
</script>
Available Props
-
name:
string | boolean | null
- namespace the modal and the open event (default:''
) -
actions:
Array<any | Record<string, any>>
- the available actions to filter and execute (required
) -
keybinding:
Array<string> | null
- combination of keys that need to be pressed (default:['meta', 'k']
) -
placeholder:
string
- placeholder to show for the root command (default:Type a command or search
) -
shadow:
boolean
- add a shadow to the view box (default:true
) -
overlay:
boolean
- show an overlay under the view box (default:true
) -
theme:
string
- which theme to use, dark or light (default:light
) -
blur:
boolean
- enable background blur (default:true
) -
animations:
boolean
- enable animations (default:true
) -
nestedSearch:
boolean
- search/filter nested actions -
fuseOptions:
Fuse.IFuseOptions<string>
- options to pass to Fuse.js (see options page) (default:{}
)
Actions
There are different type of actions you can define. They all require at least a id
, text
and if it doesn't have any child actions, a action
handler:
const action ={
id: 'ID',
text: 'Title',
action: () => {}
}
Here are all the options available to an action:
name | description | type | required |
---|---|---|---|
id | Internal ID of the action | string | true |
text | Text which will be shown for each action in the list | string | true |
action | Hanlder which will be called when the action is selected | function | true |
icon | Icon to show before the text in the UI | Vue Component/string (only when slot is used) | false |
section | Section to group the action in | string | false |
keybindings | Keystrokes to attach to the action | string[] | false |
tag | Tag to show before the input field after the action is selected | string | false |
childPlaceholder | Placeholder to show when waiting for user input after the parent action is selected | string | false |
value | Value to insert into the search field when the action is selected | string | false |
hidden | Show the action in the result list | boolean | false |
childTitle | Display a title instead of the input field when the action is selected | string | false |
childActions | Array of child actions to show once the action is selected | Actions[] | false |
Icons
The icon
property either excepts a Vue component or a string which will be passed to the icon
slot:
<template v-slot:icon="{ icon }">
<Icon :name="icon" />
</template>
You can use the provided value with different icon libraries or with your custom one.
Inputs
If your action needs an input, specify a tag
which will be shown before the input. When the user hits enter your action handler will be executed with the value as its parameter.
Nested actions
Each of your actions can also have nested/child actions which will be shown once the parent action is selected. Specify them with the childActions
parameter:
[{
id: 'docs',
text: 'Documentation',
tag: 'Docs',
childPlaceholder: 'Search Docs',
childActions: [
{
id: 'overview',
text: 'Overview',
action: () => {}
},
{
id: 'get-started',
text: 'Get Started',
action: () => {}
},
]
}]
You can also define a childPlaceholder
which will be shown in the input field before any child action is selected and a tag
which will be shown before the input to indicate what type of actions are being shown (kind of like a breadcrumb).
Keybindings
Each of your actions can either be triggered by selecting it in the menu, or directly with a keyboard shortcut. You can define the keys to attach to a action with the keybindings
parameter:
[{
id: 'help',
keybindings: [ '?' ],
text: 'Help',
action: () => {}
}]
The shortcut by default listens to the meta key (CMD on Mac, Win on Windows) and then the defined keybinding.
Opening Programmatically
<!-- the `openCommandMenu` event can be called anyway and will trigger the modal to open -->
<button type="button" @click.prevent="$root.$emit('openCommandMenu')">Show Omnibar</button>
<!-- if there is a `name`, the event will have the name appended: `'openCommandMenu.myName'` -->
<button type="button" @click.prevent="$root.$emit('openCommandMenu.myName')">Show "myName" modal</button>
<!-- you can also close the modal with the opposite event: `'closeCommandMenu.myName'` -->
<button type="button" @click.prevent="$root.$emit('closeCommandMenu.myName')">Close "myName" modal</button>
Filtering
vue-cmd-menu uses Fuse.js under the hood to filter the specified actions. This allows for fuzzy searching i.e. the search term doesn't have to be a exact match. By default it searches the action's title and keys defined with the keywords
property.
If you want to search nested actions as well, enable it with the nestedSearch
prop.
You can further fine-tune fuse.js with the fuseOptions
prop.
📖 Examples
Here are some examples for different type of actions:
[
{
id: 'home',
keybindings: [ 'backspace' ],
text: 'Go home',
action: () => {
window.location.pathname = '/'
}
},
{
id: 'copy',
keybindings: [ 'c' ],
text: 'Copy',
action: () => {}
},
{
id: 'help',
keybindings: [ '?' ],
icon: HelpIcon,
text: 'Help',
action: () => {
window.location.pathname = '/'
}
},
{
id: 'new',
text: 'Create New Page',
tag: 'New',
childPlaceholder: 'Page title',
action: (value) => {
window.location.pathname = '/'
}
},
{
id: 'docs',
icon: DocsIcon,
text: 'Documentation',
childPlaceholder: 'Search Docs',
childActions: [
{
id: 'overview',
text: 'Overview',
action: () => {}
},
{
id: 'get-started',
text: 'Get Started',
action: () => {}
},
]
},
💻 Development
Issues and PRs are very welcome!
The actual source code of this library is in the src
folder.
# install dependencies
yarn install
# serve app with hot reload
yarn run dev
# build electron application for production
yarn run build
# lint all JS/Vue component files in `src/`
yarn run lint
📋 To Do
- [ ] Standardize parameter of Action handler
- [ ] Don't hard code meta key for keybindings
- [ ] Add options to change styling
- [x] Sections
- [x] Light mode
- [ ] Animations
- [ ] Hook to add actions dynamicly
- [ ] Improve naming of properties and options
- [ ] Keep focus on input when navigating through list
- [x] Limit modal height and add scrollbar
- [ ] Async actions and loading states
- [ ] Use slot to customize the rendering of the results
- [ ] Vue 3 support
❔ About
This project was developed by me (@betahuhn) in my free time. If you want to support me:
Credits
This Action was inspired by:
📄 License
Copyright 2022 Maximilian Schiller
This project is licensed under the MIT License - see the LICENSE file for details.