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,125 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../../internal/primitive';
|
||||
import type { Orientation } from '../../utilities/roving-focus';
|
||||
|
||||
/**
|
||||
* Nests a second navigation menu inside a `NavigationMenuContent` panel, with its
|
||||
* own independent active value while inheriting the parent's timing and direction.
|
||||
* Use it to build multi-level menus where a content panel itself contains a list of
|
||||
* triggers and sub-panels.
|
||||
*/
|
||||
export interface NavigationMenuSubProps extends PrimitiveProps {
|
||||
/** Uncontrolled initial value. */
|
||||
defaultValue?: string;
|
||||
/** Submenu orientation. @default 'horizontal' */
|
||||
orientation?: Orientation;
|
||||
}
|
||||
|
||||
export interface NavigationMenuSubEmits {
|
||||
'update:modelValue': [value: string];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { ref, shallowRef, watchEffect } from 'vue';
|
||||
|
||||
import { useForwardExpose, useId } from '@robonen/vue';
|
||||
import { useCollectionProvider } from '../../utilities/collection';
|
||||
import { Primitive } from '../../internal/primitive';
|
||||
import { provideNavigationMenuContext, useNavigationMenuContext } from './context';
|
||||
import { NAVIGATION_MENU_COLLECTION_KEY } from './utils';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const { defaultValue, orientation = 'horizontal', as = 'div' } = defineProps<NavigationMenuSubProps>();
|
||||
|
||||
defineEmits<NavigationMenuSubEmits>();
|
||||
|
||||
defineSlots<{
|
||||
default?: (props: { modelValue: string }) => unknown;
|
||||
}>();
|
||||
|
||||
const localValue = ref<string>(defaultValue ?? '');
|
||||
/** Controlled active value of the submenu. Use `v-model`. */
|
||||
const modelValue = defineModel<string | undefined>({
|
||||
default: undefined,
|
||||
get: v => v ?? localValue.value,
|
||||
set: (v) => {
|
||||
const next = v ?? '';
|
||||
localValue.value = next;
|
||||
return next;
|
||||
},
|
||||
}) as unknown as Ref<string>;
|
||||
|
||||
const previousValue = ref<string>('');
|
||||
|
||||
const parentContext = useNavigationMenuContext();
|
||||
const { forwardRef, currentElement } = useForwardExpose();
|
||||
|
||||
const indicatorTrack = shallowRef<HTMLElement | undefined>(undefined);
|
||||
const viewport = shallowRef<HTMLElement | undefined>(undefined);
|
||||
const activeTrigger = shallowRef<HTMLElement | undefined>(undefined);
|
||||
|
||||
const { getItems, CollectionSlot } = useCollectionProvider<{ value: string }>(NAVIGATION_MENU_COLLECTION_KEY);
|
||||
|
||||
const baseId = useId(undefined, 'primitives-navigation-menu-sub');
|
||||
|
||||
watchEffect(() => {
|
||||
if (!modelValue.value) return;
|
||||
const items = getItems().map(i => i.ref);
|
||||
const matched = items.find(item => item.id.includes(`-trigger-${modelValue.value}`));
|
||||
if (matched) activeTrigger.value = matched;
|
||||
});
|
||||
|
||||
provideNavigationMenuContext({
|
||||
...parentContext,
|
||||
isRootMenu: false,
|
||||
modelValue,
|
||||
previousValue,
|
||||
baseId,
|
||||
orientation,
|
||||
rootNavigationMenu: currentElement,
|
||||
activeTrigger,
|
||||
onActiveTriggerChange: (el) => { activeTrigger.value = el; },
|
||||
indicatorTrack,
|
||||
onIndicatorTrackChange: (el) => { indicatorTrack.value = el; },
|
||||
viewport,
|
||||
onViewportChange: (el) => { viewport.value = el; },
|
||||
onTriggerEnter: (val) => {
|
||||
modelValue.value = val;
|
||||
},
|
||||
onTriggerLeave: () => {
|
||||
/* submenus don't auto-close on trigger leave */
|
||||
},
|
||||
onContentEnter: () => {
|
||||
/* no-op for submenus */
|
||||
},
|
||||
onContentLeave: () => {
|
||||
/* no-op for submenus */
|
||||
},
|
||||
onItemSelect: (val) => {
|
||||
previousValue.value = modelValue.value;
|
||||
modelValue.value = val;
|
||||
},
|
||||
onItemDismiss: () => {
|
||||
previousValue.value = modelValue.value;
|
||||
modelValue.value = '';
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollectionSlot>
|
||||
<Primitive
|
||||
:ref="forwardRef"
|
||||
:as="as"
|
||||
:data-orientation="orientation"
|
||||
data-primitives-navigation-menu
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot :model-value="modelValue" />
|
||||
</Primitive>
|
||||
</CollectionSlot>
|
||||
</template>
|
||||
Reference in New Issue
Block a user