svelte-p5-components 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CanvasFrame.svelte +129 -0
- package/dist/CanvasFrame.svelte.d.ts +52 -0
- package/dist/CanvasFrame.test.svelte.d.ts +1 -0
- package/dist/CanvasFrame.test.svelte.js +60 -0
- package/dist/EntityToggleList.svelte +269 -0
- package/dist/EntityToggleList.svelte.d.ts +33 -0
- package/dist/EntityToggleList.test.svelte.d.ts +1 -0
- package/dist/EntityToggleList.test.svelte.js +69 -0
- package/dist/HoverTooltip.svelte +184 -0
- package/dist/HoverTooltip.svelte.d.ts +22 -0
- package/dist/HoverTooltip.test.svelte.d.ts +1 -0
- package/dist/HoverTooltip.test.svelte.js +66 -0
- package/dist/SplitPane.svelte +280 -0
- package/dist/SplitPane.svelte.d.ts +48 -0
- package/dist/SplitPane.test.svelte.d.ts +1 -0
- package/dist/SplitPane.test.svelte.js +85 -0
- package/dist/TimelineScrubber.svelte +214 -0
- package/dist/TimelineScrubber.svelte.d.ts +32 -0
- package/dist/TimelineScrubber.test.svelte.d.ts +1 -0
- package/dist/TimelineScrubber.test.svelte.js +66 -0
- package/dist/TimelineTrack.svelte +376 -0
- package/dist/TimelineTrack.svelte.d.ts +50 -0
- package/dist/TimelineTrack.test.svelte.d.ts +1 -0
- package/dist/TimelineTrack.test.svelte.js +72 -0
- package/dist/createMediaSync.svelte.d.ts +53 -0
- package/dist/createMediaSync.svelte.js +115 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/tests/setup.d.ts +1 -0
- package/dist/tests/setup.js +26 -0
- package/package.json +12 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named region on the timeline. Rendered as a tinted band; clicking
|
|
3
|
+
* it emits `onSegmentClick` with the full segment.
|
|
4
|
+
*/
|
|
5
|
+
export interface TimelineSegment {
|
|
6
|
+
id: string;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
label?: string;
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TimelineViewWindow {
|
|
13
|
+
start: number;
|
|
14
|
+
end: number;
|
|
15
|
+
}
|
|
16
|
+
import type { Snippet } from 'svelte';
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Total timeline length in your chosen unit. */
|
|
19
|
+
duration: number;
|
|
20
|
+
/** Current playhead position. Bindable. */
|
|
21
|
+
currentTime?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Optional zoomed view window. When set, only this range is visible on
|
|
24
|
+
* the track; clicks and hover map into this window. Bindable.
|
|
25
|
+
*/
|
|
26
|
+
viewWindow?: TimelineViewWindow;
|
|
27
|
+
/** Highlighted regions rendered as labeled bands. */
|
|
28
|
+
segments?: TimelineSegment[];
|
|
29
|
+
/** Show draggable handles for the view window when it's set. Default: true. */
|
|
30
|
+
showViewWindowHandles?: boolean;
|
|
31
|
+
/** Show a vertical playhead line. Default: true. */
|
|
32
|
+
showPlayhead?: boolean;
|
|
33
|
+
/** Called when the user clicks or drags the playhead. */
|
|
34
|
+
onSeek?: (time: number) => void;
|
|
35
|
+
/** Called when the user drags a view window handle. */
|
|
36
|
+
onViewWindowChange?: (window: TimelineViewWindow) => void;
|
|
37
|
+
/** Called when the user clicks a segment band. */
|
|
38
|
+
onSegmentClick?: (segment: TimelineSegment) => void;
|
|
39
|
+
/** Called whenever the mouse moves over or leaves the track. `null` on leave. */
|
|
40
|
+
onHover?: (time: number | null) => void;
|
|
41
|
+
/** Snippet rendered above the hovered-time position. Use for preview tooltips. */
|
|
42
|
+
hoverIndicator?: Snippet<[{
|
|
43
|
+
time: number;
|
|
44
|
+
left: number;
|
|
45
|
+
}]>;
|
|
46
|
+
class?: string;
|
|
47
|
+
}
|
|
48
|
+
declare const TimelineTrack: import("svelte").Component<Props, {}, "currentTime" | "viewWindow">;
|
|
49
|
+
type TimelineTrack = ReturnType<typeof TimelineTrack>;
|
|
50
|
+
export default TimelineTrack;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/svelte';
|
|
3
|
+
import TimelineTrack, {} from './TimelineTrack.svelte';
|
|
4
|
+
describe('<TimelineTrack>', () => {
|
|
5
|
+
it('renders a playhead at the correct percent for the currentTime', () => {
|
|
6
|
+
const { container } = render(TimelineTrack, {
|
|
7
|
+
props: { duration: 100, currentTime: 25 }
|
|
8
|
+
});
|
|
9
|
+
const playhead = container.querySelector('.timeline-track__playhead');
|
|
10
|
+
expect(playhead).not.toBeNull();
|
|
11
|
+
expect(playhead.style.left).toBe('25%');
|
|
12
|
+
});
|
|
13
|
+
it('omits the playhead when showPlayhead is false', () => {
|
|
14
|
+
const { container } = render(TimelineTrack, {
|
|
15
|
+
props: { duration: 100, currentTime: 50, showPlayhead: false }
|
|
16
|
+
});
|
|
17
|
+
expect(container.querySelector('.timeline-track__playhead')).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
it('renders one band per segment with the correct width percent', () => {
|
|
20
|
+
const segments = [
|
|
21
|
+
{ id: 's1', start: 0, end: 20 },
|
|
22
|
+
{ id: 's2', start: 40, end: 60, label: 'Middle' }
|
|
23
|
+
];
|
|
24
|
+
const { container } = render(TimelineTrack, {
|
|
25
|
+
props: { duration: 100, segments }
|
|
26
|
+
});
|
|
27
|
+
const bands = container.querySelectorAll('.timeline-track__segment');
|
|
28
|
+
expect(bands).toHaveLength(2);
|
|
29
|
+
expect(bands[0]?.style.left).toBe('0%');
|
|
30
|
+
expect(bands[0]?.style.width).toBe('20%');
|
|
31
|
+
expect(bands[1]?.style.left).toBe('40%');
|
|
32
|
+
expect(bands[1]?.style.width).toBe('20%');
|
|
33
|
+
expect(bands[1]?.textContent).toContain('Middle');
|
|
34
|
+
});
|
|
35
|
+
it('onSegmentClick fires the full segment when a band is clicked', async () => {
|
|
36
|
+
const segments = [{ id: 's1', start: 0, end: 20, label: 'Intro' }];
|
|
37
|
+
const onSegmentClick = vi.fn();
|
|
38
|
+
const { container } = render(TimelineTrack, {
|
|
39
|
+
props: { duration: 100, segments, onSegmentClick }
|
|
40
|
+
});
|
|
41
|
+
const band = container.querySelector('.timeline-track__segment');
|
|
42
|
+
await fireEvent.click(band);
|
|
43
|
+
expect(onSegmentClick).toHaveBeenCalledTimes(1);
|
|
44
|
+
expect(onSegmentClick.mock.calls[0]?.[0]?.id).toBe('s1');
|
|
45
|
+
});
|
|
46
|
+
it('view window handles render when viewWindow is provided', () => {
|
|
47
|
+
const { container } = render(TimelineTrack, {
|
|
48
|
+
props: { duration: 100, currentTime: 50, viewWindow: { start: 20, end: 80 } }
|
|
49
|
+
});
|
|
50
|
+
expect(container.querySelectorAll('.timeline-track__handle')).toHaveLength(2);
|
|
51
|
+
});
|
|
52
|
+
it('view window handles hidden when showViewWindowHandles=false', () => {
|
|
53
|
+
const { container } = render(TimelineTrack, {
|
|
54
|
+
props: {
|
|
55
|
+
duration: 100,
|
|
56
|
+
viewWindow: { start: 20, end: 80 },
|
|
57
|
+
showViewWindowHandles: false
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
expect(container.querySelectorAll('.timeline-track__handle')).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
it('has role=slider with aria-valuemin/max/now reflecting view window', () => {
|
|
63
|
+
const { container } = render(TimelineTrack, {
|
|
64
|
+
props: { duration: 100, currentTime: 30, viewWindow: { start: 10, end: 90 } }
|
|
65
|
+
});
|
|
66
|
+
const track = container.querySelector('.timeline-track');
|
|
67
|
+
expect(track.getAttribute('role')).toBe('slider');
|
|
68
|
+
expect(track.getAttribute('aria-valuemin')).toBe('10');
|
|
69
|
+
expect(track.getAttribute('aria-valuemax')).toBe('90');
|
|
70
|
+
expect(track.getAttribute('aria-valuenow')).toBe('30');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync a timeline's `currentTime` with an HTMLMediaElement (video or audio).
|
|
3
|
+
*
|
|
4
|
+
* The "speed multiplier quietly becomes a no-op while video is playing" bug
|
|
5
|
+
* appears in every app that wires a timeline to a video player without
|
|
6
|
+
* thinking about it carefully. `createMediaSync` handles the branch
|
|
7
|
+
* explicitly:
|
|
8
|
+
*
|
|
9
|
+
* - If the media element is playing, `isLocked === true` — the timeline
|
|
10
|
+
* should display the video's currentTime and should *not* advance
|
|
11
|
+
* currentTime from its own animation loop. Surface `isLocked` to the user
|
|
12
|
+
* (see `<TimelineScrubber speedLocked>`) so the speed multiplier's
|
|
13
|
+
* inactivity is visible rather than silent.
|
|
14
|
+
*
|
|
15
|
+
* - If the media element is paused or unloaded, the timeline is free to run
|
|
16
|
+
* its own animation loop and the consumer's speed multiplier applies.
|
|
17
|
+
*
|
|
18
|
+
* Consumers own the animation loop (an rAF tick that advances currentTime
|
|
19
|
+
* by `speed * deltaMs / 1000`). `createMediaSync` only bridges the
|
|
20
|
+
* media→timeline direction and surfaces the lock signal.
|
|
21
|
+
*
|
|
22
|
+
* Use `.seek(time)` to write the other direction (timeline→media) — the
|
|
23
|
+
* helper clamps to the media's duration and guards against NaN.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const sync = createMediaSync();
|
|
28
|
+
*
|
|
29
|
+
* $effect(() => { sync.attach(videoElement); });
|
|
30
|
+
*
|
|
31
|
+
* // In your rAF loop:
|
|
32
|
+
* if (sync.isLocked) currentTime = sync.mediaTime;
|
|
33
|
+
* else currentTime += speed * dt / 1000;
|
|
34
|
+
*
|
|
35
|
+
* // When user scrubs the timeline:
|
|
36
|
+
* sync.seek(newTime);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export interface MediaSync {
|
|
40
|
+
/** The media element's currentTime, when attached. */
|
|
41
|
+
readonly mediaTime: number;
|
|
42
|
+
/** The media element's duration (possibly Infinity for streams). */
|
|
43
|
+
readonly mediaDuration: number;
|
|
44
|
+
/** True when the media is playing — timeline should follow, not drive. */
|
|
45
|
+
readonly isLocked: boolean;
|
|
46
|
+
/** Attach the sync to an HTMLMediaElement. Re-attaching switches to the new one. */
|
|
47
|
+
attach(el: HTMLMediaElement | null): void;
|
|
48
|
+
/** Remove listeners; idempotent. Called automatically when `attach(null)`. */
|
|
49
|
+
detach(): void;
|
|
50
|
+
/** Seek the attached media to `time`, clamped into [0, duration]. */
|
|
51
|
+
seek(time: number): void;
|
|
52
|
+
}
|
|
53
|
+
export declare function createMediaSync(): MediaSync;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync a timeline's `currentTime` with an HTMLMediaElement (video or audio).
|
|
3
|
+
*
|
|
4
|
+
* The "speed multiplier quietly becomes a no-op while video is playing" bug
|
|
5
|
+
* appears in every app that wires a timeline to a video player without
|
|
6
|
+
* thinking about it carefully. `createMediaSync` handles the branch
|
|
7
|
+
* explicitly:
|
|
8
|
+
*
|
|
9
|
+
* - If the media element is playing, `isLocked === true` — the timeline
|
|
10
|
+
* should display the video's currentTime and should *not* advance
|
|
11
|
+
* currentTime from its own animation loop. Surface `isLocked` to the user
|
|
12
|
+
* (see `<TimelineScrubber speedLocked>`) so the speed multiplier's
|
|
13
|
+
* inactivity is visible rather than silent.
|
|
14
|
+
*
|
|
15
|
+
* - If the media element is paused or unloaded, the timeline is free to run
|
|
16
|
+
* its own animation loop and the consumer's speed multiplier applies.
|
|
17
|
+
*
|
|
18
|
+
* Consumers own the animation loop (an rAF tick that advances currentTime
|
|
19
|
+
* by `speed * deltaMs / 1000`). `createMediaSync` only bridges the
|
|
20
|
+
* media→timeline direction and surfaces the lock signal.
|
|
21
|
+
*
|
|
22
|
+
* Use `.seek(time)` to write the other direction (timeline→media) — the
|
|
23
|
+
* helper clamps to the media's duration and guards against NaN.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const sync = createMediaSync();
|
|
28
|
+
*
|
|
29
|
+
* $effect(() => { sync.attach(videoElement); });
|
|
30
|
+
*
|
|
31
|
+
* // In your rAF loop:
|
|
32
|
+
* if (sync.isLocked) currentTime = sync.mediaTime;
|
|
33
|
+
* else currentTime += speed * dt / 1000;
|
|
34
|
+
*
|
|
35
|
+
* // When user scrubs the timeline:
|
|
36
|
+
* sync.seek(newTime);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function createMediaSync() {
|
|
40
|
+
let el = null;
|
|
41
|
+
let mediaTime = $state(0);
|
|
42
|
+
let mediaDuration = $state(0);
|
|
43
|
+
let isLocked = $state(false);
|
|
44
|
+
function onTimeUpdate() {
|
|
45
|
+
if (el)
|
|
46
|
+
mediaTime = el.currentTime;
|
|
47
|
+
}
|
|
48
|
+
function onDurationChange() {
|
|
49
|
+
if (el)
|
|
50
|
+
mediaDuration = isFinite(el.duration) ? el.duration : 0;
|
|
51
|
+
}
|
|
52
|
+
function onPlay() {
|
|
53
|
+
isLocked = true;
|
|
54
|
+
}
|
|
55
|
+
function onPause() {
|
|
56
|
+
isLocked = false;
|
|
57
|
+
}
|
|
58
|
+
function onEnded() {
|
|
59
|
+
isLocked = false;
|
|
60
|
+
}
|
|
61
|
+
function detachInner() {
|
|
62
|
+
if (!el)
|
|
63
|
+
return;
|
|
64
|
+
el.removeEventListener('timeupdate', onTimeUpdate);
|
|
65
|
+
el.removeEventListener('durationchange', onDurationChange);
|
|
66
|
+
el.removeEventListener('play', onPlay);
|
|
67
|
+
el.removeEventListener('pause', onPause);
|
|
68
|
+
el.removeEventListener('ended', onEnded);
|
|
69
|
+
el = null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
get mediaTime() {
|
|
73
|
+
return mediaTime;
|
|
74
|
+
},
|
|
75
|
+
get mediaDuration() {
|
|
76
|
+
return mediaDuration;
|
|
77
|
+
},
|
|
78
|
+
get isLocked() {
|
|
79
|
+
return isLocked;
|
|
80
|
+
},
|
|
81
|
+
attach(next) {
|
|
82
|
+
detachInner();
|
|
83
|
+
el = next;
|
|
84
|
+
if (!el) {
|
|
85
|
+
mediaTime = 0;
|
|
86
|
+
mediaDuration = 0;
|
|
87
|
+
isLocked = false;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
mediaTime = el.currentTime;
|
|
91
|
+
mediaDuration = isFinite(el.duration) ? el.duration : 0;
|
|
92
|
+
isLocked = !el.paused && !el.ended;
|
|
93
|
+
el.addEventListener('timeupdate', onTimeUpdate);
|
|
94
|
+
el.addEventListener('durationchange', onDurationChange);
|
|
95
|
+
el.addEventListener('play', onPlay);
|
|
96
|
+
el.addEventListener('pause', onPause);
|
|
97
|
+
el.addEventListener('ended', onEnded);
|
|
98
|
+
},
|
|
99
|
+
detach() {
|
|
100
|
+
detachInner();
|
|
101
|
+
mediaTime = 0;
|
|
102
|
+
mediaDuration = 0;
|
|
103
|
+
isLocked = false;
|
|
104
|
+
},
|
|
105
|
+
seek(time) {
|
|
106
|
+
if (!el)
|
|
107
|
+
return;
|
|
108
|
+
const dur = isFinite(el.duration) ? el.duration : Number.MAX_SAFE_INTEGER;
|
|
109
|
+
const clamped = Math.max(0, Math.min(dur, time));
|
|
110
|
+
if (!isFinite(clamped))
|
|
111
|
+
return;
|
|
112
|
+
el.currentTime = clamped;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,3 +3,10 @@ export { default as FPSMonitor } from './FPSMonitor.svelte';
|
|
|
3
3
|
export { default as SketchDebug } from './SketchDebug.svelte';
|
|
4
4
|
export { default as DraggableWindow } from './DraggableWindow.svelte';
|
|
5
5
|
export { default as DraggableSketch } from './DraggableSketch.svelte';
|
|
6
|
+
export { default as CanvasFrame } from './CanvasFrame.svelte';
|
|
7
|
+
export { default as SplitPane } from './SplitPane.svelte';
|
|
8
|
+
export { default as HoverTooltip } from './HoverTooltip.svelte';
|
|
9
|
+
export { default as EntityToggleList, type Entity } from './EntityToggleList.svelte';
|
|
10
|
+
export { default as TimelineTrack, type TimelineSegment, type TimelineViewWindow } from './TimelineTrack.svelte';
|
|
11
|
+
export { default as TimelineScrubber } from './TimelineScrubber.svelte';
|
|
12
|
+
export { createMediaSync, type MediaSync } from './createMediaSync.svelte.js';
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,10 @@ export { default as FPSMonitor } from './FPSMonitor.svelte';
|
|
|
3
3
|
export { default as SketchDebug } from './SketchDebug.svelte';
|
|
4
4
|
export { default as DraggableWindow } from './DraggableWindow.svelte';
|
|
5
5
|
export { default as DraggableSketch } from './DraggableSketch.svelte';
|
|
6
|
+
export { default as CanvasFrame } from './CanvasFrame.svelte';
|
|
7
|
+
export { default as SplitPane } from './SplitPane.svelte';
|
|
8
|
+
export { default as HoverTooltip } from './HoverTooltip.svelte';
|
|
9
|
+
export { default as EntityToggleList } from './EntityToggleList.svelte';
|
|
10
|
+
export { default as TimelineTrack } from './TimelineTrack.svelte';
|
|
11
|
+
export { default as TimelineScrubber } from './TimelineScrubber.svelte';
|
|
12
|
+
export { createMediaSync } from './createMediaSync.svelte.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
2
|
+
// happy-dom doesn't ship a ResizeObserver stub out of the box — components
|
|
3
|
+
// that use it (Sketch, HoverTooltip) expect the constructor to exist.
|
|
4
|
+
// Provide a no-op implementation so mounts don't throw; tests that care
|
|
5
|
+
// about resize behavior replace it per-case.
|
|
6
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
7
|
+
globalThis.ResizeObserver = class {
|
|
8
|
+
observe() { }
|
|
9
|
+
unobserve() { }
|
|
10
|
+
disconnect() { }
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
// Same story for PointerEvent — happy-dom implements it partially in some
|
|
14
|
+
// releases. Ensure constructor works so pointer-driven tests (SplitPane drag,
|
|
15
|
+
// TimelineTrack seek) can dispatch events.
|
|
16
|
+
if (typeof globalThis.PointerEvent === 'undefined') {
|
|
17
|
+
globalThis.PointerEvent = class extends MouseEvent {
|
|
18
|
+
pointerId;
|
|
19
|
+
pointerType;
|
|
20
|
+
constructor(type, init = {}) {
|
|
21
|
+
super(type, init);
|
|
22
|
+
this.pointerId = init.pointerId ?? 0;
|
|
23
|
+
this.pointerType = init.pointerType ?? 'mouse';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-p5-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Higher-level Svelte 5 components built on svelte-p5: responsive canvas, FPS monitor, sketch debug overlay, draggable windows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "svelte-package -o dist && publint",
|
|
48
48
|
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
49
|
-
"test": "
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest",
|
|
51
|
+
"test:coverage": "vitest run --coverage"
|
|
50
52
|
},
|
|
51
53
|
"dependencies": {
|
|
52
54
|
"@neodrag/svelte": "3.0.0-next.8"
|
|
@@ -60,11 +62,18 @@
|
|
|
60
62
|
"svelte-p5": "workspace:*",
|
|
61
63
|
"@sveltejs/package": "catalog:",
|
|
62
64
|
"@sveltejs/vite-plugin-svelte": "catalog:",
|
|
65
|
+
"@testing-library/jest-dom": "catalog:",
|
|
66
|
+
"@testing-library/svelte": "catalog:",
|
|
67
|
+
"@testing-library/user-event": "catalog:",
|
|
63
68
|
"@types/p5": "catalog:",
|
|
69
|
+
"@vitest/coverage-v8": "catalog:",
|
|
70
|
+
"happy-dom": "catalog:",
|
|
64
71
|
"p5": "catalog:",
|
|
65
72
|
"publint": "catalog:",
|
|
66
73
|
"svelte": "catalog:",
|
|
67
74
|
"svelte-check": "catalog:",
|
|
68
|
-
"typescript": "catalog:"
|
|
75
|
+
"typescript": "catalog:",
|
|
76
|
+
"vite": "catalog:",
|
|
77
|
+
"vitest": "catalog:"
|
|
69
78
|
}
|
|
70
79
|
}
|