egaki 0.7.1 → 0.8.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/CHANGELOG.md +32 -0
- package/dist/cli/bpm-detect.d.ts +7 -0
- package/dist/cli/bpm-detect.d.ts.map +1 -0
- package/dist/cli/bpm-detect.js +123 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +115 -0
- package/dist/cli/loudness-detect.d.ts +13 -0
- package/dist/cli/loudness-detect.d.ts.map +1 -0
- package/dist/cli/loudness-detect.js +96 -0
- package/dist/vite/code-block.d.ts.map +1 -1
- package/dist/vite/code-block.js +26 -5
- package/dist/vite/layout-editor.js +1 -1
- package/dist/vite/mdx-video.test.js +0 -79
- package/dist/vite/player-page.d.ts.map +1 -1
- package/dist/vite/player-page.js +6 -94
- package/dist/vite/store-hooks.d.ts +2 -0
- package/dist/vite/store-hooks.d.ts.map +1 -1
- package/dist/vite/store-hooks.js +8 -0
- package/dist/vite/store.d.ts +5 -0
- package/dist/vite/store.d.ts.map +1 -1
- package/dist/vite/store.js +5 -0
- package/dist/vite/tweakpane-hook.d.ts +6 -3
- package/dist/vite/tweakpane-hook.d.ts.map +1 -1
- package/dist/vite/tweakpane-hook.js +27 -16
- package/dist/vite/vite-plugin.d.ts +0 -6
- package/dist/vite/vite-plugin.d.ts.map +1 -1
- package/dist/vite/vite-plugin.js +1 -92
- package/package.json +9 -9
- package/skills/egaki/SKILL.md +26 -224
- package/src/cli/bpm-detect.ts +165 -0
- package/src/cli/cli.ts +149 -0
- package/src/cli/loudness-detect.ts +129 -0
- package/src/vite/code-block.tsx +28 -6
- package/src/vite/layout-editor.tsx +1 -1
- package/src/vite/mdx-video.test.tsx +0 -94
- package/src/vite/player-page.tsx +28 -117
- package/src/vite/store-hooks.ts +11 -0
- package/src/vite/store.ts +10 -0
- package/src/vite/tweakpane-hook.tsx +71 -24
- package/src/vite/vite-plugin.ts +1 -91
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
import { useCallback, useEffect, useRef, useState, useSyncExternalStore, createContext, useContext } from 'react'
|
|
24
24
|
import type { PlayerRef } from '@remotion/player'
|
|
25
25
|
import { LayoutContainerContext, useIsPremounting } from './mdx-video.tsx'
|
|
26
|
+
import { setSidebarOpen } from './store.ts'
|
|
27
|
+
import { useSidebarOpen } from './store-hooks.ts'
|
|
26
28
|
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
28
30
|
// Types
|
|
@@ -530,10 +532,32 @@ export function useTweakpane<T extends ParamSchema>(
|
|
|
530
532
|
/** Sidebar width constant shared with player-page layout. */
|
|
531
533
|
export const SIDEBAR_WIDTH = 280
|
|
532
534
|
|
|
535
|
+
/** Lucide-style panel-right icon used for both opening and closing the sidebar. */
|
|
536
|
+
function PanelRightIcon() {
|
|
537
|
+
return (
|
|
538
|
+
<svg
|
|
539
|
+
width='16'
|
|
540
|
+
height='16'
|
|
541
|
+
viewBox='0 0 24 24'
|
|
542
|
+
fill='none'
|
|
543
|
+
stroke='currentColor'
|
|
544
|
+
strokeWidth='1.5'
|
|
545
|
+
strokeLinecap='round'
|
|
546
|
+
strokeLinejoin='round'
|
|
547
|
+
>
|
|
548
|
+
<rect x='3' y='3' width='18' height='18' rx='2' />
|
|
549
|
+
<line x1='15' y1='3' x2='15' y2='21' />
|
|
550
|
+
</svg>
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
|
|
533
554
|
/**
|
|
534
|
-
* Right sidebar that hosts the tweakpane Pane.
|
|
535
|
-
*
|
|
536
|
-
*
|
|
555
|
+
* Right sidebar that hosts the tweakpane Pane. Collapsed by default so the
|
|
556
|
+
* video takes the full width; a fixed top-right button opens it. The sidebar
|
|
557
|
+
* container stays mounted while collapsed (hidden via display: none) because
|
|
558
|
+
* useTweakpane registers controls into it — unmounting would lose all
|
|
559
|
+
* registrations until components remount. Also provides player metadata
|
|
560
|
+
* (frame, fps, sections) to the copy prompt button.
|
|
537
561
|
*/
|
|
538
562
|
export function TweakpaneRoot({
|
|
539
563
|
playerRef,
|
|
@@ -566,28 +590,51 @@ export function TweakpaneRoot({
|
|
|
566
590
|
}
|
|
567
591
|
}, [playerRef, fps, sections, entryPath])
|
|
568
592
|
|
|
593
|
+
const sidebarOpen = useSidebarOpen()
|
|
594
|
+
|
|
569
595
|
return (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
background: '#111',
|
|
581
|
-
borderLeft: '1px solid rgba(255,255,255,0.08)',
|
|
582
|
-
flexShrink: 0,
|
|
583
|
-
}}
|
|
584
|
-
>
|
|
585
|
-
<CopyChangesButton />
|
|
596
|
+
<>
|
|
597
|
+
{!sidebarOpen && (
|
|
598
|
+
<button
|
|
599
|
+
onClick={() => setSidebarOpen(true)}
|
|
600
|
+
title='Open controls sidebar'
|
|
601
|
+
className='fixed top-4 right-4 z-10 flex items-center justify-center rounded-lg w-8 h-8 text-zinc-400 hover:text-zinc-200 hover:bg-white/5 transition-colors cursor-pointer'
|
|
602
|
+
>
|
|
603
|
+
<PanelRightIcon />
|
|
604
|
+
</button>
|
|
605
|
+
)}
|
|
586
606
|
<div
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
607
|
+
className='egaki-sidebar'
|
|
608
|
+
style={{
|
|
609
|
+
width: SIDEBAR_WIDTH,
|
|
610
|
+
minWidth: SIDEBAR_WIDTH,
|
|
611
|
+
height: '100vh',
|
|
612
|
+
position: 'sticky',
|
|
613
|
+
top: 0,
|
|
614
|
+
overflowY: 'auto',
|
|
615
|
+
overflowX: 'hidden',
|
|
616
|
+
background: '#111',
|
|
617
|
+
borderLeft: '1px solid rgba(255,255,255,0.08)',
|
|
618
|
+
flexShrink: 0,
|
|
619
|
+
display: sidebarOpen ? undefined : 'none',
|
|
620
|
+
}}
|
|
621
|
+
>
|
|
622
|
+
<div className='flex items-center justify-end px-2 pt-2'>
|
|
623
|
+
<button
|
|
624
|
+
onClick={() => setSidebarOpen(false)}
|
|
625
|
+
title='Close controls sidebar'
|
|
626
|
+
className='flex items-center justify-center rounded-lg w-8 h-8 text-zinc-400 hover:text-zinc-200 hover:bg-white/5 transition-colors cursor-pointer'
|
|
627
|
+
>
|
|
628
|
+
<PanelRightIcon />
|
|
629
|
+
</button>
|
|
630
|
+
</div>
|
|
631
|
+
<CopyChangesButton />
|
|
632
|
+
<div
|
|
633
|
+
ref={containerRefCallback}
|
|
634
|
+
id='egaki-tweakpane'
|
|
635
|
+
style={{ width: '100%' }}
|
|
636
|
+
/>
|
|
637
|
+
</div>
|
|
638
|
+
</>
|
|
592
639
|
)
|
|
593
640
|
}
|
package/src/vite/vite-plugin.ts
CHANGED
|
@@ -64,60 +64,6 @@ function resolveSourceToFile(root: string, source: string): string | undefined {
|
|
|
64
64
|
return undefined
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* Extract raw source text ranges per section directly from the mdast.
|
|
69
|
-
* Works with heading nodes' positions to capture the FULL raw heading
|
|
70
|
-
* text including props like `duration=3s` and `transition=20` that
|
|
71
|
-
* `splitIntoSections` strips during parsing. Each section spans from
|
|
72
|
-
* its heading's start offset to the next heading's start (or EOF).
|
|
73
|
-
*/
|
|
74
|
-
function sectionRawTexts(source: string, mdast: any): string[] {
|
|
75
|
-
// Collect the start offsets of each heading node
|
|
76
|
-
const headingStarts: number[] = []
|
|
77
|
-
for (const node of mdast.children) {
|
|
78
|
-
if (node.type === 'heading' && node.position?.start?.offset != null) {
|
|
79
|
-
headingStarts.push(node.position.start.offset)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Slice source between consecutive headings, trimming trailing whitespace
|
|
83
|
-
// so boundary changes (adding/removing a following section) don't cause
|
|
84
|
-
// false positives on the preceding section.
|
|
85
|
-
return headingStarts.map((start, i) => {
|
|
86
|
-
const end = i + 1 < headingStarts.length ? headingStarts[i + 1]! : source.length
|
|
87
|
-
return source.slice(start, end).trimEnd()
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Compare old and new MDX sources section-by-section. Returns the index
|
|
93
|
-
* of the first added or edited section, or null if nothing changed (or
|
|
94
|
-
* only deletions occurred, which we skip per design).
|
|
95
|
-
*/
|
|
96
|
-
export function findChangedSectionIndex(oldSource: string, newSource: string): number | null {
|
|
97
|
-
try {
|
|
98
|
-
const oldTexts = sectionRawTexts(oldSource, mdxParse(oldSource))
|
|
99
|
-
const newTexts = sectionRawTexts(newSource, mdxParse(newSource))
|
|
100
|
-
|
|
101
|
-
const maxLen = Math.max(oldTexts.length, newTexts.length)
|
|
102
|
-
for (let i = 0; i < maxLen; i++) {
|
|
103
|
-
if (i >= oldTexts.length) return i // new section added
|
|
104
|
-
if (i >= newTexts.length) return null // section deleted, skip
|
|
105
|
-
if (oldTexts[i] !== newTexts[i]) {
|
|
106
|
-
// Distinguish edit from deletion: if the next old section matches
|
|
107
|
-
// the current new section, a section was deleted (shifted down).
|
|
108
|
-
// Look ahead to detect deletions at any position, not just the tail.
|
|
109
|
-
if (newTexts.length < oldTexts.length && oldTexts[i + 1] === newTexts[i]) {
|
|
110
|
-
return null // deletion, skip
|
|
111
|
-
}
|
|
112
|
-
return i // content changed
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return null // no scene-level change (frontmatter/preamble only)
|
|
116
|
-
} catch {
|
|
117
|
-
return null // parse error, don't seek
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
67
|
/** Discover .mdx files in the project root directory (non-recursive).
|
|
122
68
|
* Only root-level files become entries to avoid import resolution issues
|
|
123
69
|
* with nested paths. Returns a map of routePath → absolutePath. */
|
|
@@ -160,8 +106,6 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
|
|
|
160
106
|
let defaultRoute: string = ''
|
|
161
107
|
/** Whether the user's project has `motion` (framer-motion) installed. */
|
|
162
108
|
let hasMotion = false
|
|
163
|
-
/** Cached previous MDX sources for section-level diff detection, keyed by abs path. */
|
|
164
|
-
const previousMdxSources: Map<string, string> = new Map()
|
|
165
109
|
/** Set of absolute paths of all entry MDX files for quick lookup. */
|
|
166
110
|
let entryPathSet: Set<string> = new Set()
|
|
167
111
|
|
|
@@ -217,13 +161,6 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
|
|
|
217
161
|
defaultRoute = resolveDefaultRoute(mdxEntries, options?.entry, root)
|
|
218
162
|
entryPathSet = new Set(mdxEntries.values())
|
|
219
163
|
|
|
220
|
-
// Seed previous sources for section-level diff detection.
|
|
221
|
-
for (const [, absPath] of mdxEntries) {
|
|
222
|
-
try {
|
|
223
|
-
previousMdxSources.set(absPath, fs.readFileSync(absPath, 'utf-8'))
|
|
224
|
-
} catch { /* ignore read errors */ }
|
|
225
|
-
}
|
|
226
|
-
|
|
227
164
|
// Auto-generate egaki-env.d.ts so MDX LSP knows about built-in
|
|
228
165
|
// components via the global MDXProvidedComponents type. Same
|
|
229
166
|
// pattern Vite uses for vite-env.d.ts.
|
|
@@ -432,19 +369,7 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
|
|
|
432
369
|
mdxEntries.set(relPath.replace(/\.mdx$/, ''), absEntry)
|
|
433
370
|
}
|
|
434
371
|
defaultRoute = resolveDefaultRoute(mdxEntries, options?.entry, root)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// Sync previousMdxSources: seed new entries, prune removed ones.
|
|
438
|
-
for (const file of previousMdxSources.keys()) {
|
|
439
|
-
if (!nextEntryPathSet.has(file)) previousMdxSources.delete(file)
|
|
440
|
-
}
|
|
441
|
-
for (const file of nextEntryPathSet) {
|
|
442
|
-
if (!previousMdxSources.has(file)) {
|
|
443
|
-
try { previousMdxSources.set(file, fs.readFileSync(file, 'utf-8')) }
|
|
444
|
-
catch { /* file may be mid-write */ }
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
entryPathSet = nextEntryPathSet
|
|
372
|
+
entryPathSet = new Set(mdxEntries.values())
|
|
448
373
|
|
|
449
374
|
invalidateVirtual([RESOLVED_APP, RESOLVED_MDX, RESOLVED_MODULES])
|
|
450
375
|
if (this.environment.name === 'client') {
|
|
@@ -457,26 +382,11 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
|
|
|
457
382
|
invalidateVirtual([RESOLVED_APP, RESOLVED_MDX])
|
|
458
383
|
|
|
459
384
|
if (this.environment.name === 'client') {
|
|
460
|
-
// Section-level diff detection for the changed entry.
|
|
461
|
-
const newMdxSource = fs.readFileSync(normalizedFile, 'utf-8')
|
|
462
|
-
const prevSource = previousMdxSources.get(normalizedFile)
|
|
463
|
-
const changedSection = prevSource != null
|
|
464
|
-
? findChangedSectionIndex(prevSource, newMdxSource)
|
|
465
|
-
: null
|
|
466
|
-
previousMdxSources.set(normalizedFile, newMdxSource)
|
|
467
|
-
|
|
468
385
|
ctx.server.environments.client?.hot.send({
|
|
469
386
|
type: 'custom',
|
|
470
387
|
event: 'rsc:update',
|
|
471
388
|
data: { file: ctx.file },
|
|
472
389
|
})
|
|
473
|
-
if (changedSection != null) {
|
|
474
|
-
ctx.server.environments.client?.hot.send({
|
|
475
|
-
type: 'custom',
|
|
476
|
-
event: 'egaki:scene-changed',
|
|
477
|
-
data: { sectionIndex: changedSection, file: normalizedFile },
|
|
478
|
-
})
|
|
479
|
-
}
|
|
480
390
|
}
|
|
481
391
|
return []
|
|
482
392
|
}
|