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,71 @@
import { mount } from '@vue/test-utils';
import type { VueWrapper } from '@vue/test-utils';
import { nextTick } from 'vue';
import { afterEach, describe, expect, it } from 'vitest';
// Smoke test: every new media-editor `demo.vue` must MOUNT without throwing
// (catches context-consumed-outside-its-Root, null-ref api calls at setup, and
// other runtime breakage that type-checking / SFC-parsing do not surface).
import AlphaSliderDemo from '../color/alpha-slider/demo.vue';
import AngleDialDemo from '../canvas/angle-dial/demo.vue';
import CanvasStageDemo from '../canvas/canvas-stage/demo.vue';
import ColorAreaDemo from '../color/color-area/demo.vue';
import ColorFieldDemo from '../color/color-field/demo.vue';
import CompareSliderDemo from '../canvas/compare-slider/demo.vue';
import CropDemo from '../canvas/crop/demo.vue';
import CurveEditorDemo from '../canvas/curve-editor/demo.vue';
import GradientEditorDemo from '../canvas/gradient-editor/demo.vue';
import HistogramDemo from '../canvas/histogram/demo.vue';
import HueSliderDemo from '../color/hue-slider/demo.vue';
import KeyframeTrackDemo from '../canvas/keyframe-track/demo.vue';
import LevelsDemo from '../canvas/levels/demo.vue';
import TimeRulerDemo from '../canvas/time-ruler/demo.vue';
import TimelineDemo from '../canvas/timeline/demo.vue';
import TransformBoxDemo from '../canvas/transform-box/demo.vue';
import WaveformDemo from '../canvas/waveform/demo.vue';
import ZoomPanDemo from '../canvas/zoom-pan/demo.vue';
const demos: Record<string, unknown> = {
'alpha-slider': AlphaSliderDemo,
'angle-dial': AngleDialDemo,
'canvas-stage': CanvasStageDemo,
'color-area': ColorAreaDemo,
'color-field': ColorFieldDemo,
'compare-slider': CompareSliderDemo,
crop: CropDemo,
'curve-editor': CurveEditorDemo,
'gradient-editor': GradientEditorDemo,
histogram: HistogramDemo,
'hue-slider': HueSliderDemo,
'keyframe-track': KeyframeTrackDemo,
levels: LevelsDemo,
'time-ruler': TimeRulerDemo,
timeline: TimelineDemo,
'transform-box': TransformBoxDemo,
waveform: WaveformDemo,
'zoom-pan': ZoomPanDemo,
};
const wrappers: Array<VueWrapper<any>> = [];
afterEach(() => {
while (wrappers.length) wrappers.pop()!.unmount();
document.body.innerHTML = '';
});
describe('media-editor demos mount without error or Vue warnings', () => {
for (const [name, Comp] of Object.entries(demos)) {
it(`${name}/demo.vue mounts cleanly`, async () => {
// Collect Vue warnings (e.g. slot-prop-out-of-scope, missing context,
// unknown template refs) instead of letting them slip to console.
const warnings: string[] = [];
const wrapper = mount(Comp as any, {
attachTo: document.body,
global: { config: { warnHandler: (msg: string) => { warnings.push(msg); } } },
});
wrappers.push(wrapper);
await nextTick();
expect(wrapper.html().length).toBeGreaterThan(0);
expect(warnings, `Vue warnings in ${name}/demo.vue:\n${warnings.join('\n')}`).toEqual([]);
});
}
});