Skip to content

Commit 7700a09

Browse files
authored
Command Palette (#21)
* feat: initial work on command palette. * feat: finetune command palette. * feat: finetune command palette. * chore: removed unused command palette css. * feat: add async search. * fix: address CodeRabbit review feedback for command palette - Add fallback timeout to close() for prefers-reduced-motion - Fix sub-action keyboard activation to close palette without emitting select - Guard ArrowUp navigation when result list is empty * fead: add tab transition. * feat: made shortcut optional. feat: updated keyboard shortcut using tag. * chore: fine tuning. * chore: update keyboard shortcuts docs. * chore: remove global. * feat: backdrop closes command palette. fix: make sure that the search bar stays focused. * fix: select first result when searching. * feat: optional group label. * chore: fine tuning. * fix: use semantic list markup with stable key in KeyboardShortcuts
1 parent 2eaa43e commit 7700a09

File tree

22 files changed

+1876
-1
lines changed

22 files changed

+1876
-1
lines changed

docs/.vitepress/component-navigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const navigation: SidebarItem = {
5353
{text: 'Select', link: '/guide/components/color/select'},
5454
]
5555
},
56+
{text: 'Command palette', link: '/guide/components/command-palette'},
5657
{text: 'Comment', link: '/guide/components/comment', image: '/assets/components/comment.svg'},
5758
{text: 'Data table', link: '/guide/components/data-table'},
5859
{text: 'Date picker', link: '/guide/components/date-picker'},

docs/.vitepress/theme/FrontmatterDocs.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
<Props v-if="frontmatter.props"/>
55
<Emits v-if="frontmatter.emits"/>
66
<Slots v-if="frontmatter.slots"/>
7+
<KeyboardShortcuts v-if="frontmatter.keyboardShortcuts"/>
78
</template>
89

910
<script
1011
lang="ts"
1112
setup>
1213
import { useData } from 'vitepress';
1314
import Emits from './Emits.vue';
15+
import KeyboardShortcuts from './KeyboardShortcuts.vue';
1416
import Props from './Props.vue';
1517
import RequiredIcons from './RequiredIcons.vue';
1618
import Slots from './Slots.vue';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<template>
2+
<h2 id="keyboard-shortcuts">Keyboard shortcuts</h2>
3+
4+
<ul>
5+
<li v-for="({key, action}) of shortcuts" :key="`${key}-${action}`">
6+
<code><strong>{{ key }}</strong></code>
7+
<br>
8+
{{ action }}
9+
</li>
10+
</ul>
11+
</template>
12+
13+
<script
14+
lang="ts"
15+
setup>
16+
import { useData } from 'vitepress';
17+
import { computed, unref } from 'vue';
18+
19+
const {frontmatter} = useData();
20+
21+
const shortcuts = computed(() => unref(frontmatter).keyboardShortcuts || []);
22+
</script>

docs/.vitepress/theme/icons.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ export {
99
faArrowDownAZ,
1010
faArrowDownToLine,
1111
faArrowUpArrowDown,
12+
faArrowRightFromBracket,
1213
faArrowUpFromSquare,
1314
faArrowUpAZ,
1415
faBolt,
16+
faBox,
17+
faBuilding,
1518
faCalendar,
1619
faCalendarRange,
20+
faChevronRight,
1721
faCheck,
1822
faCircleArrowUp,
1923
faCircleCheck,

docs/.vitepress/theme/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import ComponentGridIndex from './ComponentGridIndex.vue';
2525
import ComponentGridItem from './ComponentGridItem.vue';
2626
import Emits from './Emits.vue';
2727
import FluxView from './FluxView.vue';
28+
import KeyboardShortcuts from './KeyboardShortcuts.vue';
2829
import FrontmatterDocs from './FrontmatterDocs.vue';
2930
import Layout from './Layout.vue';
3031
import Preview from './Preview.vue';
@@ -116,6 +117,7 @@ const theme: Theme = {
116117
app.component('Emits', Emits);
117118
app.component('FluxView', FluxView);
118119
app.component('FrontmatterDocs', FrontmatterDocs);
120+
app.component('KeyboardShortcuts', KeyboardShortcuts);
119121
app.component('Preview', Preview);
120122
app.component('Props', Props);
121123
app.component('RequiredIcons', RequiredIcons);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<template>
2+
<FluxSecondaryButton
3+
label="Open command palette"
4+
@click="commandPalette?.open()"/>
5+
6+
<FluxCommandPalette
7+
ref="commandPalette"
8+
placeholder="Search customers, pages..."
9+
:sources="sources"/>
10+
</template>
11+
12+
<script
13+
lang="ts"
14+
setup>
15+
import type { FluxCommandSource, FluxCommandSourceItem } from '@flux-ui/types';
16+
import { FluxCommandPalette, FluxSecondaryButton, showSnackbar } from '@flux-ui/components';
17+
import { ref } from 'vue';
18+
19+
const commandPalette = ref<InstanceType<typeof FluxCommandPalette>>();
20+
21+
const activate = (label: string) => showSnackbar({
22+
icon: 'circle-check',
23+
message: `Activated: ${label}`
24+
});
25+
26+
const customers = [
27+
{id: 1, name: 'Acme Corp', segment: 'Enterprise · New York'},
28+
{id: 2, name: 'Globex Inc', segment: 'SMB · London'},
29+
{id: 3, name: 'Stark Industries', segment: 'Enterprise · Los Angeles'},
30+
{id: 4, name: 'Wayne Enterprises', segment: 'Enterprise · Gotham'},
31+
{id: 5, name: 'Umbrella Corp', segment: 'SMB · Raccoon City'},
32+
{id: 6, name: 'Initech', segment: 'SMB · Austin'},
33+
{id: 7, name: 'Hooli', segment: 'Enterprise · Palo Alto'},
34+
{id: 8, name: 'Pied Piper', segment: 'Startup · Palo Alto'},
35+
{id: 9, name: 'Dunder Mifflin', segment: 'SMB · Scranton'},
36+
{id: 10, name: 'Sterling Cooper', segment: 'SMB · New York'}
37+
];
38+
39+
const sources: FluxCommandSource[] = [
40+
{
41+
key: 'navigation',
42+
label: '',
43+
items: [
44+
{id: 'dashboard', label: 'Dashboard', icon: 'grid-2', onActivate: () => activate('Dashboard')},
45+
{id: 'customers', label: 'Customers', icon: 'users', onActivate: () => activate('Customers')},
46+
{id: 'settings', label: 'Settings', icon: 'gear', onActivate: () => activate('Settings')}
47+
]
48+
},
49+
{
50+
key: 'customers',
51+
label: 'Customers',
52+
icon: 'users',
53+
tab: true,
54+
items: [
55+
{id: 'c1', label: 'Acme Corp', subLabel: 'Enterprise · New York', icon: 'building', onActivate: () => activate('Acme Corp')},
56+
{id: 'c2', label: 'Globex Inc', subLabel: 'SMB · London', icon: 'building', onActivate: () => activate('Globex Inc')},
57+
{id: 'c3', label: 'Stark Industries', subLabel: 'Enterprise · Los Angeles', icon: 'building', onActivate: () => activate('Stark Industries')}
58+
],
59+
fetchSearch: async (query: string): Promise<FluxCommandSourceItem[]> => {
60+
await new Promise(resolve => setTimeout(resolve, 1000));
61+
62+
return customers
63+
.filter(c => c.name.toLowerCase().includes(query.toLowerCase()) || c.segment.toLowerCase().includes(query.toLowerCase()))
64+
.map(c => ({
65+
id: `c${c.id}`,
66+
label: c.name,
67+
subLabel: c.segment,
68+
icon: 'building',
69+
onActivate: () => activate(c.name)
70+
}));
71+
}
72+
}
73+
];
74+
</script>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<template>
2+
<FluxSecondaryButton
3+
label="Open command palette"
4+
@click="commandPalette?.open()"/>
5+
6+
<FluxCommandPalette
7+
ref="commandPalette"
8+
:sources="sources"/>
9+
</template>
10+
11+
<script
12+
lang="ts"
13+
setup>
14+
import type { FluxCommandSource } from '@flux-ui/types';
15+
import { FluxCommandPalette, FluxSecondaryButton, showSnackbar } from '@flux-ui/components';
16+
import { ref } from 'vue';
17+
18+
const commandPalette = ref<InstanceType<typeof FluxCommandPalette>>();
19+
20+
const activate = (label: string) => showSnackbar({
21+
icon: 'circle-check',
22+
message: `Activated: ${label}`
23+
});
24+
25+
const sources: FluxCommandSource[] = [
26+
{
27+
key: 'navigation',
28+
label: '',
29+
items: [
30+
{
31+
id: 'dashboard',
32+
label: 'Dashboard',
33+
icon: 'grid-2',
34+
onActivate: () => activate('Dashboard')
35+
},
36+
{
37+
id: 'settings',
38+
label: 'Settings',
39+
icon: 'gear',
40+
onActivate: () => activate('Settings')
41+
},
42+
{
43+
id: 'users',
44+
label: 'Users',
45+
icon: 'users',
46+
onActivate: () => activate('Users')
47+
}
48+
]
49+
},
50+
{
51+
key: 'actions',
52+
label: 'Actions',
53+
tab: true,
54+
items: [
55+
{
56+
id: 'dark-mode',
57+
label: 'Toggle dark mode',
58+
icon: 'moon',
59+
command: '\u2318D',
60+
onActivate: () => activate('Toggle dark mode')
61+
},
62+
{
63+
id: 'logout',
64+
label: 'Log out',
65+
icon: 'arrow-right-from-bracket',
66+
onActivate: () => activate('Log out')
67+
}
68+
]
69+
}
70+
];
71+
</script>

0 commit comments

Comments
 (0)