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.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user