@elah/core 0.1.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.
Files changed (153) hide show
  1. package/dist/actions/index.d.ts +3 -0
  2. package/dist/actions/index.js +1 -0
  3. package/dist/actions/splitClipAtPlayhead.d.ts +8 -0
  4. package/dist/actions/splitClipAtPlayhead.js +34 -0
  5. package/dist/actions/types.d.ts +8 -0
  6. package/dist/actions/types.js +1 -0
  7. package/dist/assets/importFiles.d.ts +29 -0
  8. package/dist/assets/importFiles.js +351 -0
  9. package/dist/assets/index.d.ts +10 -0
  10. package/dist/assets/index.js +13 -0
  11. package/dist/assets/store.d.ts +13 -0
  12. package/dist/assets/store.js +20 -0
  13. package/dist/assets/types.d.ts +23 -0
  14. package/dist/assets/types.js +1 -0
  15. package/dist/debug/trace.d.ts +20 -0
  16. package/dist/debug/trace.js +98 -0
  17. package/dist/editor/TimelineEngine.d.ts +65 -0
  18. package/dist/editor/TimelineEngine.js +413 -0
  19. package/dist/editor-context.d.ts +10 -0
  20. package/dist/editor-context.js +10 -0
  21. package/dist/elements/audio.d.ts +13 -0
  22. package/dist/elements/audio.js +4 -0
  23. package/dist/elements/base.d.ts +40 -0
  24. package/dist/elements/base.js +39 -0
  25. package/dist/elements/image.d.ts +13 -0
  26. package/dist/elements/image.js +4 -0
  27. package/dist/elements/text.d.ts +12 -0
  28. package/dist/elements/text.js +8 -0
  29. package/dist/elements/video.d.ts +13 -0
  30. package/dist/elements/video.js +4 -0
  31. package/dist/export/ExportWorker.d.ts +1 -0
  32. package/dist/export/ExportWorker.js +371 -0
  33. package/dist/export/exportVideo.d.ts +3 -0
  34. package/dist/export/exportVideo.js +118 -0
  35. package/dist/export/index.d.ts +2 -0
  36. package/dist/export/index.js +1 -0
  37. package/dist/export/lazyExport.d.ts +4 -0
  38. package/dist/export/lazyExport.js +4 -0
  39. package/dist/export/types.d.ts +37 -0
  40. package/dist/export/types.js +1 -0
  41. package/dist/index.d.ts +48 -0
  42. package/dist/index.js +28 -0
  43. package/dist/media/audio/AudioPlaybackController.d.ts +26 -0
  44. package/dist/media/audio/AudioPlaybackController.js +125 -0
  45. package/dist/media/video/DecoderBackedVideoFrameProvider.d.ts +51 -0
  46. package/dist/media/video/DecoderBackedVideoFrameProvider.js +125 -0
  47. package/dist/media/video/FrameCache.d.ts +28 -0
  48. package/dist/media/video/FrameCache.js +88 -0
  49. package/dist/media/video/StreamingFrameProducer.d.ts +57 -0
  50. package/dist/media/video/StreamingFrameProducer.js +356 -0
  51. package/dist/media/video/VideoDecoderManager.d.ts +59 -0
  52. package/dist/media/video/VideoDecoderManager.js +342 -0
  53. package/dist/media/video/VideoFrameProvider.d.ts +101 -0
  54. package/dist/media/video/VideoFrameProvider.js +257 -0
  55. package/dist/media/video/demuxer/MediabunnyDemuxer.d.ts +23 -0
  56. package/dist/media/video/demuxer/MediabunnyDemuxer.js +88 -0
  57. package/dist/media/video/demuxer/createMediabunnyBackend.d.ts +32 -0
  58. package/dist/media/video/demuxer/createMediabunnyBackend.js +156 -0
  59. package/dist/media/video/index.d.ts +8 -0
  60. package/dist/media/video/index.js +5 -0
  61. package/dist/playback/PlaybackEngine.d.ts +50 -0
  62. package/dist/playback/PlaybackEngine.js +188 -0
  63. package/dist/renderer/gpu/GpuRenderer.d.ts +38 -0
  64. package/dist/renderer/gpu/GpuRenderer.js +208 -0
  65. package/dist/renderer/gpu/RenderGraph.d.ts +10 -0
  66. package/dist/renderer/gpu/RenderGraph.js +80 -0
  67. package/dist/renderer/gpu/ShaderProgram.d.ts +14 -0
  68. package/dist/renderer/gpu/ShaderProgram.js +76 -0
  69. package/dist/renderer/gpu/TexturePool.d.ts +25 -0
  70. package/dist/renderer/gpu/TexturePool.js +93 -0
  71. package/dist/renderer/gpu/VideoTexture.d.ts +13 -0
  72. package/dist/renderer/gpu/VideoTexture.js +54 -0
  73. package/dist/renderer/gpu/WebGLContext.d.ts +28 -0
  74. package/dist/renderer/gpu/WebGLContext.js +102 -0
  75. package/dist/renderer/gpu/debug/DebugGpuRenderer.d.ts +27 -0
  76. package/dist/renderer/gpu/debug/DebugGpuRenderer.js +108 -0
  77. package/dist/renderer/gpu/debug/DebugOverlay.d.ts +17 -0
  78. package/dist/renderer/gpu/debug/DebugOverlay.js +83 -0
  79. package/dist/renderer/gpu/debug/GpuDebugCounters.d.ts +38 -0
  80. package/dist/renderer/gpu/debug/GpuDebugCounters.js +72 -0
  81. package/dist/renderer/gpu/debug/GpuDebugGlobal.d.ts +16 -0
  82. package/dist/renderer/gpu/debug/GpuDebugGlobal.js +14 -0
  83. package/dist/renderer/gpu/debug/GpuRendererDebugPanel.d.ts +31 -0
  84. package/dist/renderer/gpu/debug/GpuRendererDebugPanel.js +128 -0
  85. package/dist/renderer/gpu/debug/RecordingGl.d.ts +88 -0
  86. package/dist/renderer/gpu/debug/RecordingGl.js +214 -0
  87. package/dist/renderer/gpu/debug/playground.d.ts +20 -0
  88. package/dist/renderer/gpu/debug/playground.js +64 -0
  89. package/dist/renderer/gpu/debug/scenarios.d.ts +7 -0
  90. package/dist/renderer/gpu/debug/scenarios.js +145 -0
  91. package/dist/renderer/gpu/debug/types.d.ts +16 -0
  92. package/dist/renderer/gpu/debug/types.js +1 -0
  93. package/dist/renderer/gpu/layers/FrameProbeLayer.d.ts +16 -0
  94. package/dist/renderer/gpu/layers/FrameProbeLayer.js +127 -0
  95. package/dist/renderer/gpu/layers/ImageLayer.d.ts +23 -0
  96. package/dist/renderer/gpu/layers/ImageLayer.js +124 -0
  97. package/dist/renderer/gpu/layers/TestLayer.d.ts +16 -0
  98. package/dist/renderer/gpu/layers/TestLayer.js +109 -0
  99. package/dist/renderer/gpu/layers/TextLayer.d.ts +19 -0
  100. package/dist/renderer/gpu/layers/TextLayer.js +166 -0
  101. package/dist/renderer/gpu/layers/VideoLayer.d.ts +38 -0
  102. package/dist/renderer/gpu/layers/VideoLayer.js +194 -0
  103. package/dist/renderer/gpu/layers/drawRect.d.ts +13 -0
  104. package/dist/renderer/gpu/layers/drawRect.js +55 -0
  105. package/dist/renderer/gpu/layers/objectFit.d.ts +7 -0
  106. package/dist/renderer/gpu/layers/objectFit.js +26 -0
  107. package/dist/renderer/gpu/layers/textLayout.d.ts +47 -0
  108. package/dist/renderer/gpu/layers/textLayout.js +82 -0
  109. package/dist/renderer/gpu/layers/types.d.ts +18 -0
  110. package/dist/renderer/gpu/layers/types.js +1 -0
  111. package/dist/renderer/gpu/shaders/quad.frag.d.ts +1 -0
  112. package/dist/renderer/gpu/shaders/quad.frag.js +14 -0
  113. package/dist/renderer/gpu/shaders/quad.vert.d.ts +1 -0
  114. package/dist/renderer/gpu/shaders/quad.vert.js +28 -0
  115. package/dist/renderer/gpu/types.d.ts +21 -0
  116. package/dist/renderer/gpu/types.js +1 -0
  117. package/dist/renderer/gpu/viewport.d.ts +7 -0
  118. package/dist/renderer/gpu/viewport.js +33 -0
  119. package/dist/renderer/types.d.ts +7 -0
  120. package/dist/renderer/types.js +1 -0
  121. package/dist/resolver/resolveTimeline.d.ts +3 -0
  122. package/dist/resolver/resolveTimeline.js +249 -0
  123. package/dist/resolver/scene.d.ts +54 -0
  124. package/dist/resolver/scene.js +1 -0
  125. package/dist/stores/playback.store.d.ts +50 -0
  126. package/dist/stores/playback.store.js +42 -0
  127. package/dist/stores/selection.store.d.ts +12 -0
  128. package/dist/stores/selection.store.js +19 -0
  129. package/dist/stores/tracks.store.d.ts +19 -0
  130. package/dist/stores/tracks.store.js +27 -0
  131. package/dist/stores/transitions.store.d.ts +9 -0
  132. package/dist/stores/transitions.store.js +7 -0
  133. package/dist/track/track.d.ts +8 -0
  134. package/dist/track/track.js +19 -0
  135. package/dist/types/index.d.ts +117 -0
  136. package/dist/types/index.js +1 -0
  137. package/dist/utils/frames.d.ts +20 -0
  138. package/dist/utils/frames.js +40 -0
  139. package/dist/utils/id.d.ts +1 -0
  140. package/dist/utils/id.js +3 -0
  141. package/dist/utils/snap.d.ts +5 -0
  142. package/dist/utils/snap.js +79 -0
  143. package/dist/visitor/add.d.ts +3 -0
  144. package/dist/visitor/add.js +16 -0
  145. package/dist/visitor/clone.d.ts +3 -0
  146. package/dist/visitor/clone.js +25 -0
  147. package/dist/visitor/remove.d.ts +5 -0
  148. package/dist/visitor/remove.js +29 -0
  149. package/dist/visitor/split.d.ts +3 -0
  150. package/dist/visitor/split.js +31 -0
  151. package/dist/visitor/update.d.ts +4 -0
  152. package/dist/visitor/update.js +31 -0
  153. package/package.json +31 -0
@@ -0,0 +1,40 @@
1
+ export function toFrame(value) {
2
+ if (!Number.isFinite(value))
3
+ return 0;
4
+ return Math.max(0, Math.round(value));
5
+ }
6
+ export function secondsToFrames(seconds, fps) {
7
+ return toFrame(seconds * fps);
8
+ }
9
+ export function framesToSeconds(frames, fps) {
10
+ return frames / fps;
11
+ }
12
+ export function framesToTimecode(frames, fps) {
13
+ const f = frames % fps;
14
+ const totalSeconds = Math.floor(frames / fps);
15
+ const s = totalSeconds % 60;
16
+ const m = Math.floor(totalSeconds / 60) % 60;
17
+ const h = Math.floor(totalSeconds / 3600);
18
+ return `${pad(h)}:${pad(m)}:${pad(s)}:${pad(f)}`;
19
+ }
20
+ function pad(n) {
21
+ return String(Math.floor(n)).padStart(2, '0');
22
+ }
23
+ export function getTotalFrames(clips) {
24
+ let max = 0;
25
+ for (const trackClips of Object.values(clips)) {
26
+ for (const clip of trackClips) {
27
+ const end = clip.startFrame + clip.durationFrames;
28
+ if (end > max)
29
+ max = end;
30
+ }
31
+ }
32
+ return max;
33
+ }
34
+ export function clipsOverlap(a, b) {
35
+ return (a.startFrame < b.startFrame + b.durationFrames &&
36
+ a.startFrame + a.durationFrames > b.startFrame);
37
+ }
38
+ export function findOverlaps(trackClips, candidate, excludeId) {
39
+ return trackClips.filter((c) => c.id !== excludeId && clipsOverlap(c, candidate));
40
+ }
@@ -0,0 +1 @@
1
+ export declare function generateId(): string;
@@ -0,0 +1,3 @@
1
+ export function generateId() {
2
+ return Math.random().toString(36).slice(2, 10);
3
+ }
@@ -0,0 +1,5 @@
1
+ import type { Clip } from '../types';
2
+ export declare function snapFrame(frame: number, snapPoints: number[], threshold: number): number;
3
+ export declare function buildSnapPoints(clips: Record<string, Clip[]>, excludeId?: string): number[];
4
+ export declare const DEFAULT_OVERLAP_TOLERANCE = 0.4;
5
+ export declare function resolveOverlapEdgeSnap(nextStart: number, movingClip: Clip, trackClips: Clip[], overlapTolerance?: number): number;
@@ -0,0 +1,79 @@
1
+ export function snapFrame(frame, snapPoints, threshold) {
2
+ let nearest = frame;
3
+ let minDist = threshold;
4
+ for (const point of snapPoints) {
5
+ const dist = Math.abs(frame - point);
6
+ if (dist < minDist) {
7
+ minDist = dist;
8
+ nearest = point;
9
+ }
10
+ }
11
+ return nearest;
12
+ }
13
+ export function buildSnapPoints(clips, excludeId) {
14
+ const points = new Set();
15
+ for (const trackClips of Object.values(clips)) {
16
+ for (const clip of trackClips) {
17
+ if (clip.id === excludeId)
18
+ continue;
19
+ points.add(clip.startFrame);
20
+ points.add(clip.startFrame + clip.durationFrames);
21
+ }
22
+ }
23
+ points.add(0);
24
+ return [...points].sort((a, b) => a - b);
25
+ }
26
+ export const DEFAULT_OVERLAP_TOLERANCE = 0.4;
27
+ const MAX_OVERLAP_RESOLVE_ITERATIONS = 48;
28
+ function clipEndFrame(c) {
29
+ return c.startFrame + c.durationFrames;
30
+ }
31
+ function overlapFrames(aStart, aEnd, bStart, bEnd) {
32
+ const lo = Math.max(aStart, bStart);
33
+ const hi = Math.min(aEnd, bEnd);
34
+ const n = hi - lo;
35
+ return n > 0 ? n : 0;
36
+ }
37
+ function clipsOverlappingMoving(start, durationFrames, movingId, trackClips) {
38
+ const mEnd = start + durationFrames;
39
+ return trackClips.filter((c) => {
40
+ if (c.id === movingId)
41
+ return false;
42
+ return overlapFrames(start, mEnd, c.startFrame, clipEndFrame(c)) > 0;
43
+ });
44
+ }
45
+ export function resolveOverlapEdgeSnap(nextStart, movingClip, trackClips, overlapTolerance = DEFAULT_OVERLAP_TOLERANCE) {
46
+ const D = movingClip.durationFrames;
47
+ let start = nextStart;
48
+ for (let iter = 0; iter < MAX_OVERLAP_RESOLVE_ITERATIONS; iter++) {
49
+ const overlapping = clipsOverlappingMoving(start, D, movingClip.id, trackClips);
50
+ if (overlapping.length === 0)
51
+ return start;
52
+ let dominant = null;
53
+ let maxRatio = -1;
54
+ const mEnd = start + D;
55
+ for (const c of overlapping) {
56
+ const ov = overlapFrames(start, mEnd, c.startFrame, clipEndFrame(c));
57
+ const ratio = ov / Math.max(1, c.durationFrames);
58
+ if (ratio > maxRatio) {
59
+ maxRatio = ratio;
60
+ dominant = c;
61
+ }
62
+ }
63
+ if (!dominant)
64
+ return start;
65
+ const targetStart = dominant.startFrame;
66
+ const targetEnd = clipEndFrame(dominant);
67
+ let candidate = maxRatio > overlapTolerance
68
+ ? targetEnd
69
+ : Math.max(0, targetStart - D);
70
+ if (candidate === start) {
71
+ candidate = Math.max(...overlapping.map((c) => clipEndFrame(c)));
72
+ }
73
+ start = candidate;
74
+ }
75
+ const finalOverlapping = clipsOverlappingMoving(start, D, movingClip.id, trackClips);
76
+ if (finalOverlapping.length === 0)
77
+ return start;
78
+ return Math.max(start, ...finalOverlapping.map((c) => clipEndFrame(c)));
79
+ }
@@ -0,0 +1,3 @@
1
+ import type { Draft } from 'immer';
2
+ import type { Clip, Project } from '../types';
3
+ export declare function addClip(draft: Draft<Project>, clip: Clip): void;
@@ -0,0 +1,16 @@
1
+ import { findOverlaps } from '../utils/frames';
2
+ export function addClip(draft, clip) {
3
+ const track = draft.tracks.find((t) => t.id === clip.trackId);
4
+ if (!track) {
5
+ throw new Error(`Track "${clip.trackId}" not found`);
6
+ }
7
+ if (!draft.clips[clip.trackId]) {
8
+ draft.clips[clip.trackId] = [];
9
+ }
10
+ const overlaps = findOverlaps(draft.clips[clip.trackId], clip);
11
+ if (overlaps.length > 0) {
12
+ throw new Error(`Clip overlaps with existing clip "${overlaps[0].id}" on track "${clip.trackId}"`);
13
+ }
14
+ draft.clips[clip.trackId].push(clip);
15
+ draft.clips[clip.trackId].sort((a, b) => a.startFrame - b.startFrame);
16
+ }
@@ -0,0 +1,3 @@
1
+ import type { Draft } from 'immer';
2
+ import type { Project } from '../types';
3
+ export declare function cloneClip(draft: Draft<Project>, clipId: string, trackId: string, startFrame: number): string | null;
@@ -0,0 +1,25 @@
1
+ import { generateId } from '../utils/id';
2
+ import { toFrame } from '../utils/frames';
3
+ import { findOverlaps } from '../utils/frames';
4
+ export function cloneClip(draft, clipId, trackId, startFrame) {
5
+ const trackClips = draft.clips[trackId];
6
+ if (!trackClips)
7
+ return null;
8
+ const original = trackClips.find((c) => c.id === clipId);
9
+ if (!original)
10
+ return null;
11
+ const newStartFrame = toFrame(startFrame);
12
+ const candidate = { startFrame: newStartFrame, durationFrames: original.durationFrames };
13
+ const overlaps = findOverlaps(trackClips, candidate);
14
+ if (overlaps.length > 0)
15
+ return null;
16
+ const clone = {
17
+ ...original,
18
+ id: generateId(),
19
+ startFrame: newStartFrame,
20
+ sourceStartFrame: original.sourceStartFrame,
21
+ };
22
+ trackClips.push(clone);
23
+ trackClips.sort((a, b) => a.startFrame - b.startFrame);
24
+ return clone.id;
25
+ }
@@ -0,0 +1,5 @@
1
+ import type { Draft } from 'immer';
2
+ import type { Project } from '../types';
3
+ export declare function pruneOrphanedTransitions(draft: Draft<Project>): void;
4
+ export declare function removeClip(draft: Draft<Project>, clipId: string, trackId: string): void;
5
+ export declare function removeTrack(draft: Draft<Project>, trackId: string): void;
@@ -0,0 +1,29 @@
1
+ export function pruneOrphanedTransitions(draft) {
2
+ const allClips = Object.values(draft.clips).flat();
3
+ draft.transitions = draft.transitions.filter((t) => {
4
+ const from = allClips.find((c) => c.id === t.fromClipId);
5
+ const to = allClips.find((c) => c.id === t.toClipId);
6
+ if (!from || !to)
7
+ return false;
8
+ const gap = to.startFrame - (from.startFrame + from.durationFrames);
9
+ return gap >= 0 && gap <= 2;
10
+ });
11
+ }
12
+ export function removeClip(draft, clipId, trackId) {
13
+ const trackClips = draft.clips[trackId];
14
+ if (!trackClips)
15
+ return;
16
+ const idx = trackClips.findIndex((c) => c.id === clipId);
17
+ if (idx !== -1) {
18
+ trackClips.splice(idx, 1);
19
+ }
20
+ pruneOrphanedTransitions(draft);
21
+ }
22
+ export function removeTrack(draft, trackId) {
23
+ const idx = draft.tracks.findIndex((t) => t.id === trackId);
24
+ if (idx !== -1) {
25
+ draft.tracks.splice(idx, 1);
26
+ }
27
+ delete draft.clips[trackId];
28
+ pruneOrphanedTransitions(draft);
29
+ }
@@ -0,0 +1,3 @@
1
+ import type { Draft } from 'immer';
2
+ import type { Project } from '../types';
3
+ export declare function splitClip(draft: Draft<Project>, clipId: string, trackId: string, atFrame: number): [string, string] | null;
@@ -0,0 +1,31 @@
1
+ import { generateId } from '../utils/id';
2
+ import { toFrame } from '../utils/frames';
3
+ export function splitClip(draft, clipId, trackId, atFrame) {
4
+ const trackClips = draft.clips[trackId];
5
+ if (!trackClips)
6
+ return null;
7
+ const idx = trackClips.findIndex((c) => c.id === clipId);
8
+ if (idx === -1)
9
+ return null;
10
+ const clip = trackClips[idx];
11
+ const splitFrame = toFrame(atFrame);
12
+ if (splitFrame <= clip.startFrame ||
13
+ splitFrame >= clip.startFrame + clip.durationFrames) {
14
+ return null;
15
+ }
16
+ const leftDuration = splitFrame - clip.startFrame;
17
+ const rightDuration = clip.durationFrames - leftDuration;
18
+ const rightId = generateId();
19
+ const rightClip = {
20
+ ...clip,
21
+ id: rightId,
22
+ startFrame: splitFrame,
23
+ durationFrames: rightDuration,
24
+ sourceStartFrame: clip.sourceStartFrame + leftDuration,
25
+ sourceDurationFrames: rightDuration,
26
+ };
27
+ clip.durationFrames = leftDuration;
28
+ clip.sourceDurationFrames = leftDuration;
29
+ trackClips.splice(idx + 1, 0, rightClip);
30
+ return [clip.id, rightId];
31
+ }
@@ -0,0 +1,4 @@
1
+ import type { Draft } from 'immer';
2
+ import type { Clip, Project, Track } from '../types';
3
+ export declare function updateClip(draft: Draft<Project>, clipId: string, trackId: string, updates: Partial<Clip>): void;
4
+ export declare function updateTrack(draft: Draft<Project>, trackId: string, updates: Partial<Track>): void;
@@ -0,0 +1,31 @@
1
+ import { toFrame } from '../utils/frames';
2
+ import { findOverlaps } from '../utils/frames';
3
+ export function updateClip(draft, clipId, trackId, updates) {
4
+ const trackClips = draft.clips[trackId];
5
+ if (!trackClips)
6
+ return;
7
+ const clip = trackClips.find((c) => c.id === clipId);
8
+ if (!clip)
9
+ return;
10
+ if (updates.startFrame !== undefined) {
11
+ updates.startFrame = toFrame(updates.startFrame);
12
+ }
13
+ if (updates.durationFrames !== undefined) {
14
+ updates.durationFrames = Math.max(1, toFrame(updates.durationFrames));
15
+ }
16
+ const merged = { ...clip, ...updates };
17
+ const overlaps = findOverlaps(trackClips, merged, clipId);
18
+ if (overlaps.length > 0) {
19
+ throw new Error(`Update would cause clip "${clipId}" to overlap with "${overlaps[0].id}"`);
20
+ }
21
+ Object.assign(clip, updates);
22
+ if (updates.startFrame !== undefined) {
23
+ trackClips.sort((a, b) => a.startFrame - b.startFrame);
24
+ }
25
+ }
26
+ export function updateTrack(draft, trackId, updates) {
27
+ const track = draft.tracks.find((t) => t.id === trackId);
28
+ if (!track)
29
+ return;
30
+ Object.assign(track, updates);
31
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@elah/core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic video timeline engine",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "build": "tsc --noEmit && tsc -p tsconfig.build.json"
22
+ },
23
+ "dependencies": {
24
+ "immer": "^10.1.1",
25
+ "zustand": "^5.0.3"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.7.3",
29
+ "vitest": "^3.0.6"
30
+ }
31
+ }