Files
tools/vue/primitives/src/selection/listbox/demo.vue
T
robonen eefd7abf83 feat(primitives): media-editor components, category reorg, perf + type cleanup
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.
2026-06-15 16:54:29 +07:00

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>