@bycrux/editor 1.2.2 → 1.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/package.json +1 -1
- package/src/ControlsInfoModal.tsx +5 -5
- package/src/__tests__/no-opacity-modifier-on-editor-vars.test.ts +84 -0
- package/src/__tests__/node-shims.d.ts +19 -0
- package/src/__tests__/portal-roots-set-editor-text.test.ts +55 -0
- package/src/carousel/AddElementMenu.tsx +1 -1
- package/src/carousel/CarouselEditor.tsx +11 -11
- package/src/carousel/CarouselRenderModal.tsx +14 -14
- package/src/carousel/OverlayPicker.tsx +4 -4
- package/src/carousel/SlidePropertyPanel.tsx +6 -6
- package/src/carousel/__tests__/CarouselEditor.test.tsx +12 -0
- package/src/components/FilmstripScrubber.tsx +1 -1
- package/src/crop/VideoSourceCropModal.tsx +5 -5
- package/src/state/__tests__/use-project-sync.test.tsx +76 -0
- package/src/state/use-project-sync.ts +21 -1
- package/src/text/FontPicker.tsx +3 -3
- package/src/text/TextFormattingToolbar.tsx +3 -3
- package/src/types.ts +18 -0
- package/src/ui/Loader.tsx +1 -1
- package/src/ui/NumberField.tsx +3 -3
- package/src/ui/SwatchInput.tsx +1 -1
- package/src/ui/Tooltip.tsx +1 -1
- package/src/ui/input.tsx +1 -1
- package/src/ui/label.tsx +1 -1
- package/src/ui/textarea.tsx +1 -1
- package/src/video/AudioPolishModal.tsx +22 -22
- package/src/video/CaptionListPanel.test.tsx +8 -7
- package/src/video/CaptionListPanel.tsx +17 -17
- package/src/video/CaptionRegenModal.tsx +8 -8
- package/src/video/CaptionStyleGallery.tsx +1 -1
- package/src/video/CommandPalette.tsx +8 -8
- package/src/video/ImageToneMenu.tsx +13 -14
- package/src/video/OverlayInspector.tsx +7 -7
- package/src/video/RenderModal.tsx +76 -78
- package/src/video/VersionCompare.tsx +10 -10
- package/src/video/VersionPanel.tsx +2 -2
- package/src/video/VideoEditor.tsx +48 -23
- package/src/video/__tests__/RenderModal.hostTheme.test.tsx +85 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +6 -0
- package/src/video/__tests__/VideoEditor.pendingSurface.test.tsx +233 -0
- package/src/video/__tests__/VideoEditor.saveError.test.tsx +105 -0
- package/src/video/__tests__/export-limits.test.ts +27 -2
- package/src/video/__tests__/use-report-context.test.tsx +22 -1
- package/src/video/export-limits.ts +22 -1
- package/src/video/panels/ClipPropertiesPanel.tsx +4 -4
- package/src/video/panels/OverlayContentPanel.tsx +5 -5
- package/src/video/preview/OverlayItemsLayer.tsx +7 -2
- package/src/video/preview/PreviewPlayer.tsx +14 -0
- package/src/video/preview/SocialPreviewMenu.tsx +6 -6
- package/src/video/preview/__tests__/PreviewPlayer.playing.test.tsx +62 -0
- package/src/video/preview/__tests__/resolveOverlayPropPaths.test.ts +21 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +2 -2
- package/src/video/timeline/SpeedControl.tsx +3 -3
- package/src/video/timeline/Timeline.tsx +3 -3
- package/src/video/timeline/TrackGutter.tsx +6 -6
- package/src/video/timeline/TrackSettingsPopover.tsx +4 -4
- package/src/video/timeline/VolumeControl.tsx +3 -3
- package/src/video/use-report-context.ts +10 -7
package/package.json
CHANGED
|
@@ -110,7 +110,7 @@ export default function ControlsInfoModal({ title, sections, onClose }: Controls
|
|
|
110
110
|
role="dialog"
|
|
111
111
|
aria-modal="true"
|
|
112
112
|
aria-label={title}
|
|
113
|
-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
|
113
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm text-[var(--editor-text)]"
|
|
114
114
|
onClick={onClose}
|
|
115
115
|
>
|
|
116
116
|
<div
|
|
@@ -140,7 +140,7 @@ export default function ControlsInfoModal({ title, sections, onClose }: Controls
|
|
|
140
140
|
<div className="relative flex items-start justify-between gap-4">
|
|
141
141
|
<div className="flex items-center gap-3">
|
|
142
142
|
<span
|
|
143
|
-
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border bg-[var(--editor-text)]
|
|
143
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border bg-[color-mix(in_srgb,var(--editor-text)_6%,transparent)]"
|
|
144
144
|
style={{ borderColor: 'var(--editor-border)' }}
|
|
145
145
|
>
|
|
146
146
|
<Command size={17} style={{ color: 'var(--editor-accent)' }} />
|
|
@@ -156,7 +156,7 @@ export default function ControlsInfoModal({ title, sections, onClose }: Controls
|
|
|
156
156
|
type="button"
|
|
157
157
|
onClick={onClose}
|
|
158
158
|
aria-label="Close"
|
|
159
|
-
className="-mr-1 -mt-1 shrink-0 cursor-pointer rounded-md p-1.5 opacity-55 transition-all hover:bg-[var(--editor-text)]
|
|
159
|
+
className="-mr-1 -mt-1 shrink-0 cursor-pointer rounded-md p-1.5 opacity-55 transition-all hover:bg-[color-mix(in_srgb,var(--editor-text)_10%,transparent)] hover:opacity-100"
|
|
160
160
|
>
|
|
161
161
|
<X size={18} />
|
|
162
162
|
</button>
|
|
@@ -182,7 +182,7 @@ export default function ControlsInfoModal({ title, sections, onClose }: Controls
|
|
|
182
182
|
style={{ borderColor: 'var(--editor-border)' }}
|
|
183
183
|
>
|
|
184
184
|
<span
|
|
185
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-[var(--editor-text)]
|
|
185
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-[color-mix(in_srgb,var(--editor-text)_6%,transparent)]"
|
|
186
186
|
>
|
|
187
187
|
<Icon size={14} style={{ color: 'var(--editor-accent)' }} />
|
|
188
188
|
</span>
|
|
@@ -194,7 +194,7 @@ export default function ControlsInfoModal({ title, sections, onClose }: Controls
|
|
|
194
194
|
{section.entries.map((entry, i) => (
|
|
195
195
|
<li
|
|
196
196
|
key={i}
|
|
197
|
-
className="flex items-center justify-between gap-3 rounded-md px-2 py-2 text-sm transition-colors hover:bg-[var(--editor-text)]
|
|
197
|
+
className="flex items-center justify-between gap-3 rounded-md px-2 py-2 text-sm transition-colors hover:bg-[color-mix(in_srgb,var(--editor-text)_6%,transparent)]"
|
|
198
198
|
>
|
|
199
199
|
<span className="flex items-start gap-2.5 opacity-90">
|
|
200
200
|
{/* A toolbar row shows that button's real glyph;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
// See ./node-shims.d.ts: this package has no @types/node, so the `node:*`
|
|
6
|
+
// specifiers above have no type declarations under this package's tsconfig.
|
|
7
|
+
// Vitest itself runs on Node and provides the real modules at runtime.
|
|
8
|
+
|
|
9
|
+
// Tailwind cannot generate a rule for a `/NN` opacity modifier on an
|
|
10
|
+
// arbitrary CSS-variable color — `text-[var(--editor-text)]/60` and friends
|
|
11
|
+
// — because it can't parse an opaque `var(...)` reference to inject an alpha
|
|
12
|
+
// channel. It silently emits NO rule (measured under a host app's Tailwind
|
|
13
|
+
// v3.4), so the class falls back to an inherited/default color, which is how
|
|
14
|
+
// toolbar and timeline icons went invisible against the editor's dark
|
|
15
|
+
// surface. The fix is `color-mix(in_srgb,var(--editor-X)_NN%,transparent)`
|
|
16
|
+
// baked directly into the arbitrary value, so Tailwind never has to parse
|
|
17
|
+
// the color itself (see CHANGELOG.md "Unreleased", and LeftPanelTabs.tsx for
|
|
18
|
+
// the original instance of this fix).
|
|
19
|
+
//
|
|
20
|
+
// This guard fails the build if that dead pattern comes back anywhere in
|
|
21
|
+
// this package's source, from a revert, a bad merge, or a new component
|
|
22
|
+
// copy-pasting an old snippet.
|
|
23
|
+
|
|
24
|
+
const SRC_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
25
|
+
|
|
26
|
+
// The one deliberate exception: this file quotes the OLD broken class with a
|
|
27
|
+
// real digit (`text-[var(--editor-text)]/40`) inside a comment, as a
|
|
28
|
+
// historical example for a narrower regression test that predates this
|
|
29
|
+
// repo-wide fix. It is not live code and this guard would otherwise treat
|
|
30
|
+
// that quote as a violation.
|
|
31
|
+
const ALLOWLIST = new Set(['video/CaptionListPanel.test.tsx'])
|
|
32
|
+
|
|
33
|
+
// Requires a digit or `[` right after the slash, matching how the modifier
|
|
34
|
+
// actually looks in real Tailwind classes (`/60`, `/[0.06]`) — this keeps
|
|
35
|
+
// prose like "`text-[var(--editor-text)]/N`" (an `N` placeholder, not a real
|
|
36
|
+
// modifier) out of the check without needing it on the allowlist below.
|
|
37
|
+
const DEAD_OPACITY_MODIFIER = /var\(--editor-[a-z-]+\)\]\/[\d[]/
|
|
38
|
+
|
|
39
|
+
function collectSourceFiles(dir: string, out: string[] = []): string[] {
|
|
40
|
+
for (const entry of readdirSync(dir)) {
|
|
41
|
+
if (entry === 'node_modules') continue
|
|
42
|
+
const full = join(dir, entry)
|
|
43
|
+
const stat = statSync(full)
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
collectSourceFiles(full, out)
|
|
46
|
+
} else if (/\.(ts|tsx)$/.test(entry)) {
|
|
47
|
+
out.push(full)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('no dead Tailwind opacity modifier on --editor-* vars', () => {
|
|
54
|
+
it('finds zero `[var(--editor-*)]/NN`-style classes anywhere in src', () => {
|
|
55
|
+
const thisFile = fileURLToPath(import.meta.url)
|
|
56
|
+
const offenders: { file: string; line: number; text: string }[] = []
|
|
57
|
+
|
|
58
|
+
for (const file of collectSourceFiles(SRC_ROOT)) {
|
|
59
|
+
if (file === thisFile) continue
|
|
60
|
+
const rel = relative(SRC_ROOT, file)
|
|
61
|
+
if (ALLOWLIST.has(rel)) continue
|
|
62
|
+
|
|
63
|
+
const lines = readFileSync(file, 'utf8').split('\n')
|
|
64
|
+
lines.forEach((line, i) => {
|
|
65
|
+
if (DEAD_OPACITY_MODIFIER.test(line)) {
|
|
66
|
+
offenders.push({ file: rel, line: i + 1, text: line.trim() })
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (offenders.length > 0) {
|
|
72
|
+
const report = offenders
|
|
73
|
+
.map((o) => ` ${o.file}:${o.line}: ${o.text}`)
|
|
74
|
+
.join('\n')
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Found ${offenders.length} dead Tailwind opacity-modifier class(es) on an --editor-* var ` +
|
|
77
|
+
`(Tailwind emits no rule for these — see this test's header comment).\n` +
|
|
78
|
+
`Replace each with color-mix(in_srgb,var(--editor-X)_NN%,transparent):\n${report}`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
expect(offenders).toEqual([])
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// This package has no @types/node (it's a browser-facing library with no
|
|
2
|
+
// Node-touching runtime code otherwise), so plain TypeScript can't resolve
|
|
3
|
+
// `node:*` specifiers on its own. Vitest runs on Node and provides the real
|
|
4
|
+
// modules at runtime regardless — these ambient shapes cover only the
|
|
5
|
+
// handful of calls `no-opacity-modifier-on-editor-vars.test.ts` makes,
|
|
6
|
+
// purely so `tsc --noEmit` can typecheck that file.
|
|
7
|
+
declare module 'node:fs' {
|
|
8
|
+
export function readFileSync(path: string, encoding: 'utf8'): string
|
|
9
|
+
export function readdirSync(path: string): string[]
|
|
10
|
+
export function statSync(path: string): { isDirectory(): boolean }
|
|
11
|
+
}
|
|
12
|
+
declare module 'node:path' {
|
|
13
|
+
export function dirname(path: string): string
|
|
14
|
+
export function join(...parts: string[]): string
|
|
15
|
+
export function relative(from: string, to: string): string
|
|
16
|
+
}
|
|
17
|
+
declare module 'node:url' {
|
|
18
|
+
export function fileURLToPath(url: string): string
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
// See ./node-shims.d.ts for why the `node:*` imports typecheck here.
|
|
6
|
+
|
|
7
|
+
// Anything this package portals to `document.body` lands OUTSIDE the editor
|
|
8
|
+
// root, so it no longer inherits the editor's `text-[var(--editor-text)]`.
|
|
9
|
+
// It inherits the HOST page's text colour instead: in the Montaj desktop app
|
|
10
|
+
// that is `text-gray-900` (#111827), which is exactly the dark theme's
|
|
11
|
+
// `--editor-surface`. Any child without its own text class (or whose class the
|
|
12
|
+
// host's Tailwind failed to emit) then renders dark on dark, which is how the
|
|
13
|
+
// Export dialog's Cancel label and tier dims went invisible.
|
|
14
|
+
//
|
|
15
|
+
// Guard: in every file that calls `createPortal`, every `fixed` overlay root
|
|
16
|
+
// must set its own text colour, so portalled content inherits the editor's
|
|
17
|
+
// text, never the host's.
|
|
18
|
+
|
|
19
|
+
const SRC_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
20
|
+
const TEXT_COLOUR = /(^|\s)text-(\[var\(--editor-text\)\]|white)(\s|$)/
|
|
21
|
+
|
|
22
|
+
function collectSourceFiles(dir: string, out: string[] = []): string[] {
|
|
23
|
+
for (const entry of readdirSync(dir)) {
|
|
24
|
+
if (entry === 'node_modules' || entry === '__tests__') continue
|
|
25
|
+
const full = join(dir, entry)
|
|
26
|
+
if (statSync(full).isDirectory()) collectSourceFiles(full, out)
|
|
27
|
+
else if (/\.tsx$/.test(entry) && !/\.test\.tsx$/.test(entry)) out.push(full)
|
|
28
|
+
}
|
|
29
|
+
return out
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('portal roots set the editor text colour', () => {
|
|
33
|
+
it('every `fixed` root in a portalling component carries a text colour', () => {
|
|
34
|
+
const offenders: string[] = []
|
|
35
|
+
let checked = 0
|
|
36
|
+
for (const file of collectSourceFiles(SRC_ROOT)) {
|
|
37
|
+
const src = readFileSync(file, 'utf8')
|
|
38
|
+
if (!src.includes('createPortal(')) continue
|
|
39
|
+
// Static className strings and the static head of template literals.
|
|
40
|
+
const re = /className=(?:"([^"]*)"|\{`([^`]*)`\})/g
|
|
41
|
+
let m: RegExpExecArray | null
|
|
42
|
+
while ((m = re.exec(src)) !== null) {
|
|
43
|
+
const cls = (m[1] ?? m[2]).replace(/\$\{[^}]*\}/g, ' ')
|
|
44
|
+
if (!/(^|\s)fixed(\s|$)/.test(cls)) continue
|
|
45
|
+
checked += 1
|
|
46
|
+
if (!TEXT_COLOUR.test(cls)) {
|
|
47
|
+
const line = src.slice(0, m.index).split('\n').length
|
|
48
|
+
offenders.push(`${relative(SRC_ROOT, file)}:${line}: ${cls.trim()}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
expect(checked).toBeGreaterThan(5)
|
|
53
|
+
expect(offenders).toEqual([])
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -182,7 +182,7 @@ export default function AddElementMenu({ project, selectedSlideId, adapter, onAd
|
|
|
182
182
|
{showPrompt && !disabled && (
|
|
183
183
|
<div className="flex flex-col gap-2 p-3 bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded-lg">
|
|
184
184
|
<textarea
|
|
185
|
-
className="w-full bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded px-2 py-1.5 text-xs text-[var(--editor-text)] placeholder-[var(--editor-text)]
|
|
185
|
+
className="w-full bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded px-2 py-1.5 text-xs text-[var(--editor-text)] placeholder-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] resize-none focus:outline-none focus:border-[var(--editor-accent)]"
|
|
186
186
|
rows={3}
|
|
187
187
|
placeholder="Describe the image to generate…"
|
|
188
188
|
value={prompt}
|
|
@@ -73,7 +73,7 @@ function SlideGrid({
|
|
|
73
73
|
return (
|
|
74
74
|
<div className="w-56 flex-shrink-0 flex flex-col border-r border-[var(--editor-border)] bg-[var(--editor-bg)] overflow-y-auto min-h-0 h-full">
|
|
75
75
|
<div className="px-3 py-2 border-b border-[var(--editor-border)]">
|
|
76
|
-
<span className="text-xs font-semibold text-[var(--editor-text)]
|
|
76
|
+
<span className="text-xs font-semibold text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] uppercase tracking-wider">Slides</span>
|
|
77
77
|
</div>
|
|
78
78
|
<div className="flex-1 overflow-y-auto py-2 flex flex-col gap-2 px-2">
|
|
79
79
|
{slides.map((slide, idx) => (
|
|
@@ -392,7 +392,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
392
392
|
const canvasScale = Math.min(availW / w, availH / h, 1)
|
|
393
393
|
|
|
394
394
|
return (
|
|
395
|
-
<div ref={containerRef} className="flex flex-col h-full overflow-y-auto bg-[var(--editor-bg)]">
|
|
395
|
+
<div ref={containerRef} className="flex flex-col h-full overflow-y-auto bg-[var(--editor-bg)] text-[var(--editor-text)]">
|
|
396
396
|
{/* TOP: slide rail | canvas | editing panel (right). Fixed viewport-relative
|
|
397
397
|
height with min-h-0 so each of the three columns establishes its own
|
|
398
398
|
independent scroll context; the project-media region flows beneath and
|
|
@@ -424,7 +424,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
424
424
|
className={`flex items-center gap-2 px-3 py-2 rounded-md border transition-colors ${
|
|
425
425
|
refreshState === 'err'
|
|
426
426
|
? (mode === 'light' ? 'text-red-700 border-red-300 bg-red-50 hover:bg-red-100' : 'text-red-300 border-red-500/40 bg-red-950/60 hover:bg-red-900/70')
|
|
427
|
-
: 'text-[var(--editor-text)] border-[var(--editor-border)] bg-[var(--editor-surface)]
|
|
427
|
+
: 'text-[var(--editor-text)] border-[var(--editor-border)] bg-[color-mix(in_srgb,var(--editor-surface)_80%,transparent)] hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)] hover:bg-[var(--editor-surface)]'
|
|
428
428
|
}`}
|
|
429
429
|
title={refreshState === 'err' ? 'Refresh failed — check connection' : 'Refresh project'}
|
|
430
430
|
>
|
|
@@ -434,7 +434,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
434
434
|
<button
|
|
435
435
|
onClick={() => state.undo()}
|
|
436
436
|
disabled={!state.canUndo}
|
|
437
|
-
className="flex items-center justify-center p-2 rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)]
|
|
437
|
+
className="flex items-center justify-center p-2 rounded-md border border-[var(--editor-border)] bg-[color-mix(in_srgb,var(--editor-surface)_80%,transparent)] text-[var(--editor-text)] transition-colors hover:border-[var(--editor-accent)] hover:bg-[var(--editor-surface)] disabled:opacity-40 disabled:cursor-not-allowed"
|
|
438
438
|
title="Undo (Cmd/Ctrl+Z)"
|
|
439
439
|
aria-label="Undo"
|
|
440
440
|
>
|
|
@@ -443,7 +443,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
443
443
|
<button
|
|
444
444
|
onClick={() => state.redo()}
|
|
445
445
|
disabled={!state.canRedo}
|
|
446
|
-
className="flex items-center justify-center p-2 rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)]
|
|
446
|
+
className="flex items-center justify-center p-2 rounded-md border border-[var(--editor-border)] bg-[color-mix(in_srgb,var(--editor-surface)_80%,transparent)] text-[var(--editor-text)] transition-colors hover:border-[var(--editor-accent)] hover:bg-[var(--editor-surface)] disabled:opacity-40 disabled:cursor-not-allowed"
|
|
447
447
|
title="Redo (Cmd/Ctrl+Shift+Z)"
|
|
448
448
|
aria-label="Redo"
|
|
449
449
|
>
|
|
@@ -479,11 +479,11 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
479
479
|
{slots?.pendingStatus ?? (
|
|
480
480
|
<div className="flex flex-col items-center gap-2">
|
|
481
481
|
<p className="text-[var(--editor-text)] text-lg font-semibold">Message your agent to start</p>
|
|
482
|
-
<p className="text-[var(--editor-text)]
|
|
482
|
+
<p className="text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] text-sm">Nothing will happen automatically. Copy this and send it to your agent.</p>
|
|
483
483
|
</div>
|
|
484
484
|
)}
|
|
485
485
|
{!slots?.pendingStatus && skillPath && (
|
|
486
|
-
<div className="w-full rounded-xl border-2 border-[var(--editor-accent)] bg-[var(--editor-surface)] p-5 flex flex-col gap-3 text-left shadow-lg shadow-[var(--editor-accent)]
|
|
486
|
+
<div className="w-full rounded-xl border-2 border-[var(--editor-accent)] bg-[var(--editor-surface)] p-5 flex flex-col gap-3 text-left shadow-lg shadow-[color-mix(in_srgb,var(--editor-accent)_10%,transparent)]">
|
|
487
487
|
{/* indigo-600 in light mode: the accent (indigo-500) is ~4.06:1 on
|
|
488
488
|
`--editor-surface`, and 12px bold is still NORMAL text under WCAG
|
|
489
489
|
(large starts at 18.66px bold), so it needs the 4.5:1 floor. The
|
|
@@ -527,7 +527,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
527
527
|
</div>
|
|
528
528
|
</div>
|
|
529
529
|
)}
|
|
530
|
-
<p className="text-[var(--editor-text)]
|
|
530
|
+
<p className="text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)] text-xs font-mono">project id: {project.id}</p>
|
|
531
531
|
</div>
|
|
532
532
|
) : selectedSlide ? (
|
|
533
533
|
<>
|
|
@@ -538,7 +538,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
538
538
|
instead, the same "tint with the foreground colour" idiom the rest
|
|
539
539
|
of the chrome uses, so it is a light ring on dark and a dark ring
|
|
540
540
|
on light at the same subtle strength. */}
|
|
541
|
-
<div className="flex-shrink-0 ring-1 ring-[var(--editor-text)]
|
|
541
|
+
<div className="flex-shrink-0 ring-1 ring-[color-mix(in_srgb,var(--editor-text)_10%,transparent)]">
|
|
542
542
|
<SlideCanvas
|
|
543
543
|
slide={selectedSlide}
|
|
544
544
|
slideId={selectedSlide.id}
|
|
@@ -562,7 +562,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
562
562
|
hiddenElementIds={hiddenElementIds}
|
|
563
563
|
/>
|
|
564
564
|
</div>
|
|
565
|
-
<div className="flex-shrink-0 flex items-center justify-center gap-1.5 text-xs text-[var(--editor-text)]
|
|
565
|
+
<div className="flex-shrink-0 flex items-center justify-center gap-1.5 text-xs text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] max-w-md">
|
|
566
566
|
<span className="text-center">
|
|
567
567
|
Drag to reposition, resize/rotate via handles, double-click text to edit. Cmd/Ctrl+Z to undo.
|
|
568
568
|
</span>
|
|
@@ -578,7 +578,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
578
578
|
</div>
|
|
579
579
|
</>
|
|
580
580
|
) : (
|
|
581
|
-
<div className="text-[var(--editor-text)]
|
|
581
|
+
<div className="text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)] text-sm">No slides yet. Add one in the left panel.</div>
|
|
582
582
|
)}
|
|
583
583
|
|
|
584
584
|
{state.lastError && (
|
|
@@ -64,13 +64,13 @@ function slidesFromMedia(media: RenderMedia[]): SlideView[] {
|
|
|
64
64
|
|
|
65
65
|
function LogLine({ text, mode = 'dark' }: { text: string; mode?: 'light' | 'dark' }) {
|
|
66
66
|
const t = text.replace(/^\[render\]\s*/, '')
|
|
67
|
-
let color = 'text-[var(--editor-text)]
|
|
67
|
+
let color = 'text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]'
|
|
68
68
|
if (/done|complete|→/i.test(t)) color = mode === 'light' ? 'text-green-700' : 'text-green-400'
|
|
69
69
|
else if (/rendering|launching|bundling/i.test(t)) color = mode === 'light' ? 'text-sky-700' : 'text-sky-400'
|
|
70
70
|
else if (/error|fail/i.test(t)) color = mode === 'light' ? 'text-red-600' : 'text-red-400'
|
|
71
71
|
|
|
72
72
|
const prefix = text.startsWith('[render]')
|
|
73
|
-
? <span className="text-[var(--editor-text)]
|
|
73
|
+
? <span className="text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">[render] </span>
|
|
74
74
|
: null
|
|
75
75
|
|
|
76
76
|
return (
|
|
@@ -240,7 +240,7 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
240
240
|
// Portal to document.body (see RenderModal): a transformed host ancestor
|
|
241
241
|
// would otherwise trap this `fixed` overlay and center the panel off-screen.
|
|
242
242
|
return createPortal(
|
|
243
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black">
|
|
243
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black text-[var(--editor-text)]">
|
|
244
244
|
<div className="w-[96vw] h-[96vh] bg-[var(--editor-bg)] border border-[var(--editor-border)] rounded-2xl shadow-2xl flex overflow-hidden">
|
|
245
245
|
|
|
246
246
|
{/* Left — slide gallery */}
|
|
@@ -264,15 +264,15 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
264
264
|
className="block w-full h-auto"
|
|
265
265
|
style={{ aspectRatio: `${resolution[0]} / ${resolution[1]}` }}
|
|
266
266
|
/>
|
|
267
|
-
<div className="absolute bottom-0 left-0 right-0 px-2 py-1.5 bg-black text-[11px] text-
|
|
267
|
+
<div className="absolute bottom-0 left-0 right-0 px-2 py-1.5 bg-black text-[11px] text-white/90 font-mono flex justify-between">
|
|
268
268
|
<span>#{String(i + 1).padStart(2, '0')}</span>
|
|
269
|
-
<span className="text-
|
|
269
|
+
<span className="text-white/60">{slide.filename}</span>
|
|
270
270
|
</div>
|
|
271
271
|
</a>
|
|
272
272
|
))}
|
|
273
273
|
</div>
|
|
274
274
|
) : (
|
|
275
|
-
<p className="text-sm text-
|
|
275
|
+
<p className="text-sm text-white/60">Render complete.</p>
|
|
276
276
|
)}
|
|
277
277
|
</div>
|
|
278
278
|
|
|
@@ -283,12 +283,12 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
283
283
|
<span className="w-2 h-2 rounded-full bg-green-400" />
|
|
284
284
|
<div>
|
|
285
285
|
<p className="text-sm font-semibold text-[var(--editor-text)]">Render complete</p>
|
|
286
|
-
<p className="text-xs text-[var(--editor-text)]
|
|
286
|
+
<p className="text-xs text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">
|
|
287
287
|
{slides.length || slidesCount} slide{(slides.length || slidesCount) === 1 ? '' : 's'} ready.
|
|
288
288
|
</p>
|
|
289
289
|
</div>
|
|
290
290
|
</div>
|
|
291
|
-
<button onClick={onClose} className="text-[var(--editor-text)]
|
|
291
|
+
<button onClick={onClose} className="text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
|
|
292
292
|
</div>
|
|
293
293
|
|
|
294
294
|
<div className="flex flex-col gap-3 p-5 flex-1">
|
|
@@ -311,7 +311,7 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
311
311
|
// Poll transport carries no per-line logs, so show a spinner + status text;
|
|
312
312
|
// the SSE transport (montaj-native) streams logs into the log panel.
|
|
313
313
|
return createPortal(
|
|
314
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black">
|
|
314
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black text-[var(--editor-text)]">
|
|
315
315
|
<div className="w-full max-w-3xl bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded-xl shadow-2xl flex flex-col overflow-hidden">
|
|
316
316
|
|
|
317
317
|
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--editor-border)]">
|
|
@@ -325,7 +325,7 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
325
325
|
</div>
|
|
326
326
|
</div>
|
|
327
327
|
{status !== 'running' && (
|
|
328
|
-
<button onClick={onClose} className="text-[var(--editor-text)]
|
|
328
|
+
<button onClick={onClose} className="text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
|
|
329
329
|
)}
|
|
330
330
|
</div>
|
|
331
331
|
|
|
@@ -334,8 +334,8 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
334
334
|
{status === 'running' ? (
|
|
335
335
|
<>
|
|
336
336
|
<Loader size="lg" />
|
|
337
|
-
<p className="text-sm text-[var(--editor-text)]
|
|
338
|
-
<p className="text-xs text-[var(--editor-text)]
|
|
337
|
+
<p className="text-sm text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)]">Rendering slides…</p>
|
|
338
|
+
<p className="text-xs text-[color-mix(in_srgb,var(--editor-text)_50%,transparent)]">This can take a moment.</p>
|
|
339
339
|
</>
|
|
340
340
|
) : (
|
|
341
341
|
<p className={`text-sm whitespace-pre-wrap break-words text-center ${mode === 'light' ? 'text-red-600' : 'text-red-400'}`}>
|
|
@@ -347,7 +347,7 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
347
347
|
<div className="relative">
|
|
348
348
|
<button
|
|
349
349
|
onClick={() => navigator.clipboard.writeText(logs.join('\n') + (errorMsg ? '\n' + errorMsg : ''))}
|
|
350
|
-
className="absolute top-2 right-2 z-10 text-[10px] px-2 py-0.5 rounded bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)]
|
|
350
|
+
className="absolute top-2 right-2 z-10 text-[10px] px-2 py-0.5 rounded bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)] transition-colors"
|
|
351
351
|
title="Copy logs"
|
|
352
352
|
>
|
|
353
353
|
Copy
|
|
@@ -357,7 +357,7 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
357
357
|
className="h-96 overflow-y-auto px-4 py-3 font-mono text-[11px] text-[var(--editor-text)] bg-[var(--editor-bg)] flex flex-col gap-0.5"
|
|
358
358
|
>
|
|
359
359
|
{logs.length === 0 && status === 'running' && (
|
|
360
|
-
<span className="text-[var(--editor-text)]
|
|
360
|
+
<span className="text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] italic">Starting render engine…</span>
|
|
361
361
|
)}
|
|
362
362
|
{logs.map((line, i) => (
|
|
363
363
|
<LogLine key={i} text={line} mode={mode} />
|
|
@@ -100,7 +100,7 @@ export default function OverlayPicker({ open, onClose, project, adapter, onPick,
|
|
|
100
100
|
|
|
101
101
|
return (
|
|
102
102
|
<div
|
|
103
|
-
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
103
|
+
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 text-[var(--editor-text)]"
|
|
104
104
|
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
|
105
105
|
>
|
|
106
106
|
<div className="bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded-xl shadow-2xl w-full max-w-2xl max-h-[80vh] flex flex-col overflow-hidden">
|
|
@@ -108,7 +108,7 @@ export default function OverlayPicker({ open, onClose, project, adapter, onPick,
|
|
|
108
108
|
<h2 className="text-sm font-semibold text-[var(--editor-text)]">Add Overlay</h2>
|
|
109
109
|
<button
|
|
110
110
|
onClick={onClose}
|
|
111
|
-
className="text-[var(--editor-text)]
|
|
111
|
+
className="text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:text-[var(--editor-text)] transition-colors text-lg leading-none"
|
|
112
112
|
>
|
|
113
113
|
×
|
|
114
114
|
</button>
|
|
@@ -124,7 +124,7 @@ export default function OverlayPicker({ open, onClose, project, adapter, onPick,
|
|
|
124
124
|
<div className={`text-center text-sm py-8 ${mode === 'light' ? 'text-red-600' : 'text-red-400'}`}>{error}</div>
|
|
125
125
|
)}
|
|
126
126
|
{!loading && !error && overlays.length === 0 && (
|
|
127
|
-
<div className="text-center text-[var(--editor-text)]
|
|
127
|
+
<div className="text-center text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] text-sm py-8">No overlays available</div>
|
|
128
128
|
)}
|
|
129
129
|
{!loading && !error && overlays.length > 0 && (
|
|
130
130
|
<div className="grid grid-cols-3 gap-3">
|
|
@@ -139,7 +139,7 @@ export default function OverlayPicker({ open, onClose, project, adapter, onPick,
|
|
|
139
139
|
<div className="text-xs text-[var(--editor-accent)] mt-0.5 truncate">{overlay.group}</div>
|
|
140
140
|
)}
|
|
141
141
|
{overlay.description && (
|
|
142
|
-
<div className="text-xs text-[var(--editor-text)]
|
|
142
|
+
<div className="text-xs text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] mt-1 line-clamp-2">{overlay.description}</div>
|
|
143
143
|
)}
|
|
144
144
|
</button>
|
|
145
145
|
))}
|
|
@@ -14,11 +14,11 @@ import { Loader } from '../ui/Loader'
|
|
|
14
14
|
import { TextFormattingToolbar } from '../text/TextFormattingToolbar'
|
|
15
15
|
|
|
16
16
|
// Shared small muted label that sits just above an inspector control.
|
|
17
|
-
const fieldLabelClass = 'text-[11px] uppercase tracking-wide text-[var(--editor-text)]
|
|
17
|
+
const fieldLabelClass = 'text-[11px] uppercase tracking-wide text-[color-mix(in_srgb,var(--editor-text)_55%,transparent)]'
|
|
18
18
|
|
|
19
19
|
// Section header: SLIDE / OVERLAY / IMAGE.
|
|
20
20
|
const sectionHeaderClass =
|
|
21
|
-
'text-xs font-semibold uppercase tracking-wider text-[var(--editor-text)]
|
|
21
|
+
'text-xs font-semibold uppercase tracking-wider text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)]'
|
|
22
22
|
|
|
23
23
|
interface Props {
|
|
24
24
|
project: Project
|
|
@@ -71,7 +71,7 @@ function HideToggle({
|
|
|
71
71
|
title={isHidden ? 'Show in editor' : 'Hide from editor'}
|
|
72
72
|
aria-label={isHidden ? 'Show in editor' : 'Hide from editor'}
|
|
73
73
|
aria-pressed={isHidden}
|
|
74
|
-
className="flex h-7 w-7 items-center justify-center rounded text-[var(--editor-text)]
|
|
74
|
+
className="flex h-7 w-7 items-center justify-center rounded text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:bg-[var(--editor-surface)] hover:text-[var(--editor-text)]"
|
|
75
75
|
>
|
|
76
76
|
{isHidden ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
77
77
|
</button>
|
|
@@ -238,7 +238,7 @@ export default function SlidePropertyPanel({
|
|
|
238
238
|
|
|
239
239
|
if (!slide) {
|
|
240
240
|
return (
|
|
241
|
-
<div className={cn('w-80 flex-shrink-0 flex items-center justify-center text-[var(--editor-text)]
|
|
241
|
+
<div className={cn('w-80 flex-shrink-0 flex items-center justify-center text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)] text-xs p-4', className)}>
|
|
242
242
|
Select a slide
|
|
243
243
|
</div>
|
|
244
244
|
)
|
|
@@ -295,14 +295,14 @@ export default function SlidePropertyPanel({
|
|
|
295
295
|
/>
|
|
296
296
|
<button
|
|
297
297
|
onClick={() => onReorderElement(slide.id, element.id, 'forward')}
|
|
298
|
-
className="flex h-7 w-7 items-center justify-center rounded text-[var(--editor-text)]
|
|
298
|
+
className="flex h-7 w-7 items-center justify-center rounded text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:bg-[var(--editor-surface)] hover:text-[var(--editor-text)]"
|
|
299
299
|
title="Bring forward"
|
|
300
300
|
>
|
|
301
301
|
↑
|
|
302
302
|
</button>
|
|
303
303
|
<button
|
|
304
304
|
onClick={() => onReorderElement(slide.id, element.id, 'backward')}
|
|
305
|
-
className="flex h-7 w-7 items-center justify-center rounded text-[var(--editor-text)]
|
|
305
|
+
className="flex h-7 w-7 items-center justify-center rounded text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)] hover:bg-[var(--editor-surface)] hover:text-[var(--editor-text)]"
|
|
306
306
|
title="Send backward"
|
|
307
307
|
>
|
|
308
308
|
↓
|
|
@@ -105,6 +105,18 @@ describe('CarouselEditor — editor-core integration', () => {
|
|
|
105
105
|
expect(wrapper?.className).not.toContain('w-80')
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
// The root sets the editor text colour (as VideoEditor's root does), so chrome
|
|
109
|
+
// that inherits colour reads --editor-text instead of the host page's colour.
|
|
110
|
+
it('sets the editor text colour on its root', async () => {
|
|
111
|
+
const adapter = makeFakeAdapter()
|
|
112
|
+
const { container } = render(
|
|
113
|
+
<CarouselEditor project={makeProject()} adapter={adapter} onProjectChange={vi.fn()} />,
|
|
114
|
+
)
|
|
115
|
+
const root = container.firstElementChild as HTMLElement
|
|
116
|
+
expect(root.className).toContain('bg-[var(--editor-bg)]')
|
|
117
|
+
expect(root.className).toContain('text-[var(--editor-text)]')
|
|
118
|
+
})
|
|
119
|
+
|
|
108
120
|
// Regression: SlideGrid thumbnails must receive `compileOverlay` so overlay
|
|
109
121
|
// elements render in the left rail. Before the fix the thumbnail used a
|
|
110
122
|
// noopCompiler that always rejected → a red "overlay error" badge on every
|
|
@@ -266,7 +266,7 @@ export default function FilmstripScrubber({
|
|
|
266
266
|
)}
|
|
267
267
|
{phase === 'error' && (
|
|
268
268
|
<div
|
|
269
|
-
className="pointer-events-none absolute inset-0 flex items-center justify-center bg-[var(--editor-surface)] text-[10px] text-[var(--editor-text)]
|
|
269
|
+
className="pointer-events-none absolute inset-0 flex items-center justify-center bg-[var(--editor-surface)] text-[10px] text-[color-mix(in_srgb,var(--editor-text)_40%,transparent)]"
|
|
270
270
|
role="status"
|
|
271
271
|
>
|
|
272
272
|
No preview
|
|
@@ -156,7 +156,7 @@ export function VideoSourceCropModal({
|
|
|
156
156
|
|
|
157
157
|
return (
|
|
158
158
|
<div
|
|
159
|
-
className="fixed inset-0 z-[300] flex items-center justify-center bg-black/70 p-6"
|
|
159
|
+
className="fixed inset-0 z-[300] flex items-center justify-center bg-black/70 p-6 text-[var(--editor-text)]"
|
|
160
160
|
onPointerDown={onClose}
|
|
161
161
|
>
|
|
162
162
|
<div
|
|
@@ -165,7 +165,7 @@ export function VideoSourceCropModal({
|
|
|
165
165
|
>
|
|
166
166
|
<div className="flex items-center justify-between">
|
|
167
167
|
<h2 className="text-sm font-semibold text-[var(--editor-text)]">Crop source</h2>
|
|
168
|
-
<span className="text-xs text-[var(--editor-text)]
|
|
168
|
+
<span className="text-xs text-[color-mix(in_srgb,var(--editor-text)_60%,transparent)]">Pick the part of the footage to keep. Position and zoom live on the canvas</span>
|
|
169
169
|
</div>
|
|
170
170
|
|
|
171
171
|
{/* Source frame + crop window */}
|
|
@@ -243,8 +243,8 @@ export function VideoSourceCropModal({
|
|
|
243
243
|
title={key === 'free' ? 'Free-form crop' : `Lock crop to ${label}`}
|
|
244
244
|
className={`flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
245
245
|
active
|
|
246
|
-
? 'border-[var(--editor-selection)] bg-[var(--editor-selection)]
|
|
247
|
-
: 'border-[var(--editor-border)] text-[var(--editor-text)]
|
|
246
|
+
? 'border-[var(--editor-selection)] bg-[color-mix(in_srgb,var(--editor-selection)_15%,transparent)] text-[var(--editor-text)]'
|
|
247
|
+
: 'border-[var(--editor-border)] text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] hover:text-[var(--editor-text)]'
|
|
248
248
|
}`}
|
|
249
249
|
>
|
|
250
250
|
<Icon size={14} />
|
|
@@ -258,7 +258,7 @@ export function VideoSourceCropModal({
|
|
|
258
258
|
<div className="flex items-center justify-end gap-2 pt-1">
|
|
259
259
|
<button
|
|
260
260
|
onClick={onClose}
|
|
261
|
-
className="rounded-md px-3 py-1.5 text-xs font-medium text-[var(--editor-text)]
|
|
261
|
+
className="rounded-md px-3 py-1.5 text-xs font-medium text-[color-mix(in_srgb,var(--editor-text)_70%,transparent)] hover:text-[var(--editor-text)] transition-colors"
|
|
262
262
|
>
|
|
263
263
|
Cancel
|
|
264
264
|
</button>
|