mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-16 21:18:05 +01:00
Compare commits
3 Commits
patch-1
...
feat/custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259a43930c | ||
|
|
b7ab65b0c9 | ||
|
|
18a1c17d98 |
@@ -10,8 +10,9 @@ const open = ref(false)
|
|||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
// const searchTermDebounced = refDebounced(searchTerm, 200)
|
// const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||||
const selected = ref([])
|
const selected = ref([])
|
||||||
|
const commandPalette = useTemplateRef('commandPalette')
|
||||||
|
|
||||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
const { data: _users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
// params: { q: searchTermDebounced },
|
// params: { q: searchTermDebounced },
|
||||||
transform: (data: User[]) => {
|
transform: (data: User[]) => {
|
||||||
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
||||||
@@ -22,10 +23,6 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const groups = computed(() => [{
|
const groups = computed(() => [{
|
||||||
id: 'users',
|
|
||||||
label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
|
|
||||||
items: users.value || []
|
|
||||||
}, {
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
items: [{
|
items: [{
|
||||||
label: 'Add new file',
|
label: 'Add new file',
|
||||||
@@ -74,6 +71,12 @@ const groups = computed(() => [{
|
|||||||
toast.add({ title: 'Label added!' })
|
toast.add({ title: 'Label added!' })
|
||||||
},
|
},
|
||||||
kbds: ['meta', 'L']
|
kbds: ['meta', 'L']
|
||||||
|
}, {
|
||||||
|
label: 'Set Wallpaper',
|
||||||
|
suffix: 'Choose from beautiful wallpaper collection.',
|
||||||
|
icon: 'i-lucide-image',
|
||||||
|
view: 'wallpaper',
|
||||||
|
placeholder: 'Search wallpapers...'
|
||||||
}, {
|
}, {
|
||||||
label: 'More actions',
|
label: 'More actions',
|
||||||
placeholder: 'Search actions...',
|
placeholder: 'Search actions...',
|
||||||
@@ -140,6 +143,116 @@ const labels = [{
|
|||||||
}]
|
}]
|
||||||
const label = ref()
|
const label = ref()
|
||||||
|
|
||||||
|
const wallpapers = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'red_distortion_1',
|
||||||
|
gradient: 'from-red-500 via-orange-500 to-pink-500',
|
||||||
|
category: 'Abstract',
|
||||||
|
featured: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'blue_distortion_1',
|
||||||
|
gradient: 'from-blue-600 via-purple-600 to-indigo-600',
|
||||||
|
category: 'Abstract',
|
||||||
|
featured: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'mono_dark_distortion_1',
|
||||||
|
gradient: 'from-gray-900 via-gray-700 to-gray-800',
|
||||||
|
category: 'Monochrome',
|
||||||
|
featured: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'chromatic_dark_1',
|
||||||
|
gradient: 'from-emerald-600 via-teal-600 to-cyan-600',
|
||||||
|
category: 'Chromatic',
|
||||||
|
featured: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'red_distortion_2',
|
||||||
|
gradient: 'from-rose-600 via-red-600 to-orange-600',
|
||||||
|
category: 'Abstract',
|
||||||
|
featured: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'purple_cosmic_1',
|
||||||
|
gradient: 'from-violet-700 via-purple-700 to-fuchsia-700',
|
||||||
|
category: 'Cosmic',
|
||||||
|
featured: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: 'golden_sunset_1',
|
||||||
|
gradient: 'from-yellow-500 via-orange-500 to-red-500',
|
||||||
|
category: 'Nature',
|
||||||
|
featured: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: 'ocean_deep_1',
|
||||||
|
gradient: 'from-blue-800 via-blue-900 to-indigo-900',
|
||||||
|
category: 'Nature',
|
||||||
|
featured: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
name: 'mono_light_distortion_1',
|
||||||
|
gradient: 'from-gray-200 via-gray-300 to-gray-400',
|
||||||
|
category: 'Monochrome',
|
||||||
|
featured: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: 'green_matrix_1',
|
||||||
|
gradient: 'from-green-800 via-emerald-700 to-teal-700',
|
||||||
|
category: 'Chromatic',
|
||||||
|
featured: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: 'pink_dreams_1',
|
||||||
|
gradient: 'from-pink-500 via-rose-500 to-purple-500',
|
||||||
|
category: 'Abstract',
|
||||||
|
featured: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
name: 'midnight_blue_1',
|
||||||
|
gradient: 'from-slate-900 via-blue-900 to-indigo-900',
|
||||||
|
category: 'Nature',
|
||||||
|
featured: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredWallpapers = computed(() => {
|
||||||
|
let filtered = wallpapers
|
||||||
|
|
||||||
|
// Filter by search term
|
||||||
|
if (searchTerm.value.trim()) {
|
||||||
|
const search = searchTerm.value.toLowerCase()
|
||||||
|
filtered = filtered.filter(w =>
|
||||||
|
w.name.toLowerCase().includes(search)
|
||||||
|
|| w.category.toLowerCase().includes(search)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
function setWallpaper(wallpaper: any) {
|
||||||
|
toast.add({
|
||||||
|
title: `Wallpaper set to ${wallpaper.name}!`,
|
||||||
|
description: 'Your desktop wallpaper has been updated.',
|
||||||
|
icon: 'i-lucide-image'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// function onSelect(item: typeof groups.value[number]['items'][number]) {
|
// function onSelect(item: typeof groups.value[number]['items'][number]) {
|
||||||
function onSelect(item: any) {
|
function onSelect(item: any) {
|
||||||
console.log('Selected', item)
|
console.log('Selected', item)
|
||||||
@@ -147,6 +260,12 @@ function onSelect(item: any) {
|
|||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
meta_k: () => open.value = !open.value,
|
meta_k: () => open.value = !open.value,
|
||||||
|
meta_shift_a: {
|
||||||
|
usingInput: true,
|
||||||
|
handler: () => {
|
||||||
|
commandPalette.value?.openView('askAI')
|
||||||
|
}
|
||||||
|
},
|
||||||
...extractShortcuts(groups.value)
|
...extractShortcuts(groups.value)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -154,6 +273,7 @@ defineShortcuts({
|
|||||||
<template>
|
<template>
|
||||||
<DefineTemplate>
|
<DefineTemplate>
|
||||||
<UCommandPalette
|
<UCommandPalette
|
||||||
|
ref="commandPalette"
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
v-model:search-term="searchTerm"
|
v-model:search-term="searchTerm"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
@@ -166,7 +286,51 @@ defineShortcuts({
|
|||||||
multiple
|
multiple
|
||||||
class="sm:max-h-80"
|
class="sm:max-h-80"
|
||||||
@update:model-value="onSelect"
|
@update:model-value="onSelect"
|
||||||
/>
|
>
|
||||||
|
<template #wallpaper>
|
||||||
|
<div class="flex-1 overflow-y-auto p-6">
|
||||||
|
<div class="grid grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="wallpaper in filteredWallpapers"
|
||||||
|
:key="wallpaper.id"
|
||||||
|
class="group relative cursor-pointer"
|
||||||
|
@click="setWallpaper(wallpaper)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="aspect-video rounded-lg bg-gradient-to-br shadow-lg ring-1 ring-black/5"
|
||||||
|
:class="wallpaper.gradient"
|
||||||
|
/>
|
||||||
|
<div class="mt-2 px-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h3 class="text-sm font-medium text-highlighted truncate">
|
||||||
|
{{ wallpaper.name }}
|
||||||
|
</h3>
|
||||||
|
<UChip
|
||||||
|
v-if="wallpaper.featured"
|
||||||
|
label="★"
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
class="shrink-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-dimmed">
|
||||||
|
{{ wallpaper.category }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #askAI>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-4 p-6">
|
||||||
|
<UIcon name="i-lucide-sparkles" class="size-8 text-primary" />
|
||||||
|
<span class="text-lg font-semibold text-highlighted">
|
||||||
|
Ask me anything...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCommandPalette>
|
||||||
</DefineTemplate>
|
</DefineTemplate>
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col gap-12 w-full max-w-lg">
|
<div class="flex-1 flex flex-col gap-12 w-full max-w-lg">
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
|||||||
*/
|
*/
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
children?: CommandPaletteItem[]
|
children?: CommandPaletteItem[]
|
||||||
|
/**
|
||||||
|
* Custom view to display instead of children items.
|
||||||
|
* When defined, clicking this item will show the custom view.
|
||||||
|
*/
|
||||||
|
view?: string
|
||||||
onSelect?(e?: Event): void
|
onSelect?(e?: Event): void
|
||||||
class?: any
|
class?: any
|
||||||
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
|
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
|
||||||
@@ -153,7 +158,7 @@ export type CommandPaletteSlots<G extends CommandPaletteGroup<T> = CommandPalett
|
|||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
'item-label': SlotProps<T>
|
'item-label': SlotProps<T>
|
||||||
'item-trailing': SlotProps<T>
|
'item-trailing': SlotProps<T>
|
||||||
} & Record<string, SlotProps<G>> & Record<string, SlotProps<T>>
|
} & Record<string, SlotProps<G>> & Record<string, SlotProps<T>> & Record<string, (props: { current: any, searchTerm: string, navigateBack: () => void, close: () => void }) => any>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -208,12 +213,17 @@ const fuse = computed(() => defu({}, props.fuse, {
|
|||||||
matchAllWhenSearchEmpty: true
|
matchAllWhenSearchEmpty: true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const history = ref<(CommandPaletteGroup & { placeholder?: string })[]>([])
|
const history = ref<(CommandPaletteGroup & { placeholder?: string, view?: string })[]>([])
|
||||||
|
|
||||||
const placeholder = computed(() => history.value[history.value.length - 1]?.placeholder || props.placeholder || t('commandPalette.placeholder'))
|
const placeholder = computed(() => history.value[history.value.length - 1]?.placeholder || props.placeholder || t('commandPalette.placeholder'))
|
||||||
|
|
||||||
const groups = computed(() => history.value?.length ? [history.value[history.value.length - 1] as G] : props.groups)
|
const groups = computed(() => history.value?.length ? [history.value[history.value.length - 1] as G] : props.groups)
|
||||||
|
|
||||||
|
const currentView = computed(() => {
|
||||||
|
const current = history.value[history.value.length - 1]
|
||||||
|
return current?.view ? current : null
|
||||||
|
})
|
||||||
|
|
||||||
const items = computed(() => groups.value?.filter((group) => {
|
const items = computed(() => groups.value?.filter((group) => {
|
||||||
if (!group.id) {
|
if (!group.id) {
|
||||||
console.warn(`[@nuxt/ui] CommandPalette group is missing an \`id\` property`)
|
console.warn(`[@nuxt/ui] CommandPalette group is missing an \`id\` property`)
|
||||||
@@ -279,8 +289,33 @@ const filteredGroups = computed(() => {
|
|||||||
|
|
||||||
const listboxRootRef = useTemplateRef('listboxRootRef')
|
const listboxRootRef = useTemplateRef('listboxRootRef')
|
||||||
|
|
||||||
|
// Exposed methods for programmatic control
|
||||||
|
function openView(viewName: string) {
|
||||||
|
history.value.push({
|
||||||
|
id: `view-${viewName}`,
|
||||||
|
label: viewName,
|
||||||
|
view: viewName,
|
||||||
|
items: []
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
searchTerm.value = ''
|
||||||
|
listboxRootRef.value?.highlightFirstItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeView() {
|
||||||
|
if (history.value.length > 0) {
|
||||||
|
navigateBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openView,
|
||||||
|
closeView,
|
||||||
|
navigateBack
|
||||||
|
})
|
||||||
|
|
||||||
function navigate(item: T) {
|
function navigate(item: T) {
|
||||||
if (!item.children?.length) {
|
if (!item.children?.length && !item.view) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +324,8 @@ function navigate(item: T) {
|
|||||||
label: item.label,
|
label: item.label,
|
||||||
slot: item.slot,
|
slot: item.slot,
|
||||||
placeholder: item.placeholder,
|
placeholder: item.placeholder,
|
||||||
items: item.children
|
view: item.view,
|
||||||
|
items: item.children || []
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
searchTerm.value = ''
|
searchTerm.value = ''
|
||||||
@@ -316,7 +352,7 @@ function onBackspace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSelect(e: Event, item: T) {
|
function onSelect(e: Event, item: T) {
|
||||||
if (item.children?.length) {
|
if (item.children?.length || item.view) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
navigate(item)
|
navigate(item)
|
||||||
@@ -371,7 +407,17 @@ function onSelect(e: Event, item: T) {
|
|||||||
</ListboxFilter>
|
</ListboxFilter>
|
||||||
|
|
||||||
<ListboxContent :class="ui.content({ class: props.ui?.content })">
|
<ListboxContent :class="ui.content({ class: props.ui?.content })">
|
||||||
<div v-if="filteredGroups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
|
<div v-if="currentView" :class="ui.viewport({ class: props.ui?.viewport })">
|
||||||
|
<slot
|
||||||
|
:name="currentView.view"
|
||||||
|
:current="currentView"
|
||||||
|
:search-term="searchTerm"
|
||||||
|
:navigate-back="navigateBack"
|
||||||
|
:close="closeView"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="filteredGroups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
|
||||||
<ListboxGroup v-for="group in filteredGroups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })">
|
<ListboxGroup v-for="group in filteredGroups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })">
|
||||||
<ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
|
<ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
|
||||||
{{ get(group, props.labelKey as string) }}
|
{{ get(group, props.labelKey as string) }}
|
||||||
@@ -415,7 +461,7 @@ function onSelect(e: Event, item: T) {
|
|||||||
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.ui?.itemTrailing] })">
|
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.ui?.itemTrailing] })">
|
||||||
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||||
<UIcon
|
<UIcon
|
||||||
v-if="item.children && item.children.length > 0"
|
v-if="(item.children && item.children.length > 0) || item.view"
|
||||||
:name="trailingIcon || appConfig.ui.icons.chevronRight"
|
:name="trailingIcon || appConfig.ui.icons.chevronRight"
|
||||||
:class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })"
|
:class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user