eefd7abf83
Reorganize components into category folders (forms/canvas/overlays/etc.); add the media-editor headless family (timeline, curve-editor, waveform, crop, color picker, etc.); apply perf fixes (O(1) collection lookups, plain-object drag state, gesture-leak teardown, shallowRef color state, rect caching) and replace source `any` with proper types.
107 lines
3.1 KiB
Vue
107 lines
3.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue';
|
|
|
|
import {
|
|
ListboxContent,
|
|
ListboxFilter,
|
|
ListboxGroup,
|
|
ListboxGroupLabel,
|
|
ListboxItem,
|
|
ListboxItemIndicator,
|
|
ListboxRoot,
|
|
} from '@robonen/primitives';
|
|
|
|
interface Fruit {
|
|
value: string;
|
|
label: string;
|
|
group: 'Citrus' | 'Berries';
|
|
}
|
|
|
|
const fruits: Fruit[] = [
|
|
{ value: 'orange', label: 'Orange', group: 'Citrus' },
|
|
{ value: 'lemon', label: 'Lemon', group: 'Citrus' },
|
|
{ value: 'lime', label: 'Lime', group: 'Citrus' },
|
|
{ value: 'strawberry', label: 'Strawberry', group: 'Berries' },
|
|
{ value: 'blueberry', label: 'Blueberry', group: 'Berries' },
|
|
{ value: 'raspberry', label: 'Raspberry', group: 'Berries' },
|
|
];
|
|
|
|
const selected = ref<string[]>(['lemon']);
|
|
const query = ref('');
|
|
|
|
const groups = ['Citrus', 'Berries'] as const;
|
|
|
|
const filtered = computed(() => {
|
|
const q = query.value.trim().toLowerCase();
|
|
return q ? fruits.filter(f => f.label.toLowerCase().includes(q)) : fruits;
|
|
});
|
|
|
|
function itemsFor(group: (typeof groups)[number]) {
|
|
return filtered.value.filter(f => f.group === group);
|
|
}
|
|
|
|
function labelFor(value: string) {
|
|
return fruits.find(f => f.value === value)?.label ?? value;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex w-full max-w-xs flex-col gap-3">
|
|
<ListboxRoot
|
|
v-model="selected"
|
|
multiple
|
|
highlight-on-hover
|
|
class="overflow-hidden rounded-lg border border-border bg-bg-elevated"
|
|
>
|
|
<div class="border-b border-border p-2">
|
|
<ListboxFilter
|
|
v-model="query"
|
|
placeholder="Filter fruit..."
|
|
class="w-full rounded-md border border-border bg-bg-inset px-2.5 py-1.5 text-sm text-fg outline-none placeholder:text-fg-subtle focus:border-accent focus:ring-2 focus:ring-ring"
|
|
/>
|
|
</div>
|
|
|
|
<ListboxContent class="max-h-64 overflow-y-auto p-1 outline-none">
|
|
<p
|
|
v-if="filtered.length === 0"
|
|
class="px-3 py-6 text-center text-sm text-fg-subtle"
|
|
>
|
|
No fruit found.
|
|
</p>
|
|
|
|
<ListboxGroup
|
|
v-for="group in groups"
|
|
v-show="itemsFor(group).length"
|
|
:key="group"
|
|
class="mb-1 last:mb-0"
|
|
>
|
|
<ListboxGroupLabel
|
|
class="demo-label px-2 py-1"
|
|
>
|
|
{{ group }}
|
|
</ListboxGroupLabel>
|
|
|
|
<ListboxItem
|
|
v-for="fruit in itemsFor(group)"
|
|
:key="fruit.value"
|
|
:value="fruit.value"
|
|
class="flex cursor-pointer items-center justify-between rounded-md px-2 py-1.5 text-sm text-fg outline-none data-[highlighted]:bg-bg-subtle data-[state=checked]:text-accent data-[disabled]:opacity-50"
|
|
>
|
|
<span>{{ fruit.label }}</span>
|
|
<ListboxItemIndicator class="text-accent">
|
|
<span aria-hidden="true">✓</span>
|
|
</ListboxItemIndicator>
|
|
</ListboxItem>
|
|
</ListboxGroup>
|
|
</ListboxContent>
|
|
</ListboxRoot>
|
|
|
|
<p class="text-sm text-fg-muted">
|
|
Selected:
|
|
<span class="font-medium text-fg">
|
|
{{ selected.length ? selected.map(labelFor).join(', ') : 'none' }}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</template>
|