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:
2026-06-15 16:54:29 +07:00
parent 661a55719e
commit eefd7abf83
1029 changed files with 65815 additions and 9449 deletions
@@ -0,0 +1,89 @@
<script lang="ts">
import type { PrimitiveProps } from '../../internal/primitive';
/**
* The scrollable region inside the content that wraps the options. Marked
* `role="presentation"` (the listbox role lives on the content element), caps
* its height to the available space, and scrolls when the list overflows. In
* `item-aligned` mode it grows the panel as you scroll (MacOS-style). Pair it
* with the scroll buttons for an item-aligned menu.
*/
export interface SelectViewportProps extends PrimitiveProps {
/**
* CSP `nonce` for the injected scrollbar-hiding `<style>` tag. Falls back to
* the active `ConfigProvider` nonce.
*/
nonce?: string;
}
</script>
<script setup lang="ts">
import { ref, toRef, watchPostEffect } from 'vue';
import { useForwardExpose } from '@robonen/vue';
import { useNonce } from '../../utilities/config-provider';
import { Primitive } from '../../internal/primitive';
import { useSelectContentContext, useSelectItemAlignedPositionContext } from './context';
import { CONTENT_MARGIN } from './utils';
const { as = 'div', nonce: propNonce } = defineProps<SelectViewportProps>();
const { forwardRef, currentElement } = useForwardExpose();
const contentCtx = useSelectContentContext();
const nonce = useNonce(toRef(() => propNonce));
const alignedCtx = contentCtx.position === 'item-aligned'
? useSelectItemAlignedPositionContext(null as never)
: undefined;
watchPostEffect(() => contentCtx.onViewportChange(currentElement.value));
const prevScrollTopRef = ref(0);
// Expand-on-scroll for item-aligned mode: grow the wrapper as the user scrolls
// so more options become visible, matching the native MacOS menu behaviour.
function handleScroll(event: Event) {
const viewport = event.currentTarget as HTMLElement;
const shouldExpand = alignedCtx?.shouldExpandOnScrollRef;
const contentWrapper = alignedCtx?.contentWrapper;
if (shouldExpand?.value && contentWrapper?.value) {
const scrolledBy = Math.abs(prevScrollTopRef.value - viewport.scrollTop);
if (scrolledBy > 0) {
const availableHeight = window.innerHeight - CONTENT_MARGIN * 2;
const cssMinHeight = Number.parseFloat(contentWrapper.value.style.minHeight);
const cssHeight = Number.parseFloat(contentWrapper.value.style.height);
const prevHeight = Math.max(cssMinHeight || 0, cssHeight || 0);
if (prevHeight < availableHeight) {
const nextHeight = prevHeight + scrolledBy;
const clampedNextHeight = Math.min(availableHeight, nextHeight);
const heightDiff = nextHeight - clampedNextHeight;
contentWrapper.value.style.height = `${clampedNextHeight}px`;
if (contentWrapper.value.style.bottom === '0px') {
viewport.scrollTop = heightDiff > 0 ? heightDiff : 0;
contentWrapper.value.style.justifyContent = 'flex-end';
}
}
}
}
prevScrollTopRef.value = viewport.scrollTop;
}
</script>
<template>
<Primitive
:ref="forwardRef"
:as="as"
role="presentation"
data-primitives-select-viewport
style="position: relative; flex: 1; overflow: hidden auto; max-height: var(--primitives-select-content-available-height, 300px)"
@scroll="handleScroll"
>
<slot />
</Primitive>
<Primitive as="style" :nonce="nonce">
[data-primitives-select-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}
[data-primitives-select-viewport]::-webkit-scrollbar{display:none;}
</Primitive>
</template>