@bycrux/editor 0.4.1 → 0.4.2
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/__tests__/adapter.test.ts +15 -0
- package/src/carousel/AddElementMenu.tsx +2 -2
- package/src/carousel/CarouselEditor.tsx +54 -46
- package/src/carousel/CarouselRenderModal.tsx +23 -23
- package/src/carousel/OverlayPicker.tsx +10 -10
- package/src/carousel/SlideCanvas.tsx +26 -11
- package/src/carousel/SlidePropertyPanel.tsx +24 -24
- package/src/carousel/__tests__/CarouselEditor.test.tsx +5 -3
- package/src/text/FontPicker.tsx +6 -6
- package/src/text/TextFormattingToolbar.tsx +11 -11
- package/src/theme.ts +31 -15
- package/src/types.ts +5 -0
- package/src/ui/badge.tsx +1 -1
- package/src/ui/button.tsx +5 -5
- package/src/ui/input.tsx +1 -1
- package/src/ui/label.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +2 -2
- package/src/ui/textarea.tsx +1 -1
package/package.json
CHANGED
|
@@ -148,6 +148,9 @@ describe('applyTheme', () => {
|
|
|
148
148
|
expect(el.style.getPropertyValue('--editor-accent')).toBe(
|
|
149
149
|
defaultMontajTheme.colors.accent,
|
|
150
150
|
)
|
|
151
|
+
expect(el.style.getPropertyValue('--editor-accent-foreground')).toBe(
|
|
152
|
+
defaultMontajTheme.colors.accentForeground,
|
|
153
|
+
)
|
|
151
154
|
expect(el.style.getPropertyValue('--editor-text')).toBe(
|
|
152
155
|
defaultMontajTheme.colors.text,
|
|
153
156
|
)
|
|
@@ -162,6 +165,18 @@ describe('applyTheme', () => {
|
|
|
162
165
|
)
|
|
163
166
|
})
|
|
164
167
|
|
|
168
|
+
it('falls back accent-foreground to text when the theme omits it', () => {
|
|
169
|
+
const el = document.createElement('div')
|
|
170
|
+
const { accentForeground: _omit, ...colorsWithoutAccentFg } = defaultMontajTheme.colors
|
|
171
|
+
applyTheme(el, {
|
|
172
|
+
...defaultMontajTheme,
|
|
173
|
+
colors: colorsWithoutAccentFg,
|
|
174
|
+
})
|
|
175
|
+
expect(el.style.getPropertyValue('--editor-accent-foreground')).toBe(
|
|
176
|
+
defaultMontajTheme.colors.text,
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
|
|
165
180
|
it('writes serif/display font vars only when present', () => {
|
|
166
181
|
const el = document.createElement('div')
|
|
167
182
|
applyTheme(el, {
|
|
@@ -175,9 +175,9 @@ export default function AddElementMenu({ project, selectedSlideId, adapter, onAd
|
|
|
175
175
|
{textError && <div className="text-xs text-red-400">{textError}</div>}
|
|
176
176
|
|
|
177
177
|
{showPrompt && !disabled && (
|
|
178
|
-
<div className="flex flex-col gap-2 p-3 bg-
|
|
178
|
+
<div className="flex flex-col gap-2 p-3 bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded-lg">
|
|
179
179
|
<textarea
|
|
180
|
-
className="w-full bg-
|
|
180
|
+
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)]/60 resize-none focus:outline-none focus:border-[var(--editor-accent)]"
|
|
181
181
|
rows={3}
|
|
182
182
|
placeholder="Describe the image to generate…"
|
|
183
183
|
value={prompt}
|
|
@@ -70,9 +70,9 @@ function SlideGrid({
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
|
-
<div className="w-56 flex-shrink-0 flex flex-col border-r border-
|
|
74
|
-
<div className="px-3 py-2 border-b border-
|
|
75
|
-
<span className="text-xs font-semibold text-
|
|
73
|
+
<div className="w-56 flex-shrink-0 flex flex-col border-r border-[var(--editor-border)] bg-[var(--editor-bg)] overflow-y-auto">
|
|
74
|
+
<div className="px-3 py-2 border-b border-[var(--editor-border)]">
|
|
75
|
+
<span className="text-xs font-semibold text-[var(--editor-text)]/60 uppercase tracking-wider">Slides</span>
|
|
76
76
|
</div>
|
|
77
77
|
<div className="flex-1 overflow-y-auto py-2 flex flex-col gap-2 px-2">
|
|
78
78
|
{slides.map((slide, idx) => (
|
|
@@ -86,10 +86,10 @@ function SlideGrid({
|
|
|
86
86
|
onClick={() => onSelect(slide.id)}
|
|
87
87
|
className={`group relative cursor-pointer rounded overflow-hidden border transition-colors ${
|
|
88
88
|
selectedSlideId === slide.id
|
|
89
|
-
? 'border-
|
|
89
|
+
? 'border-[var(--editor-accent)]'
|
|
90
90
|
: dragOverIdx === idx
|
|
91
|
-
? 'border-
|
|
92
|
-
: 'border-
|
|
91
|
+
? 'border-[var(--editor-accent)] opacity-70'
|
|
92
|
+
: 'border-[var(--editor-border)] hover:border-[var(--editor-accent)]'
|
|
93
93
|
}`}
|
|
94
94
|
style={{ width: THUMB_W, height: thumbH }}
|
|
95
95
|
>
|
|
@@ -116,7 +116,7 @@ function SlideGrid({
|
|
|
116
116
|
</div>
|
|
117
117
|
))}
|
|
118
118
|
</div>
|
|
119
|
-
<div className="p-2 border-t border-
|
|
119
|
+
<div className="p-2 border-t border-[var(--editor-border)]">
|
|
120
120
|
<Button size="sm" variant="outline" onClick={onAdd} className="w-full text-xs">
|
|
121
121
|
+ Add Slide
|
|
122
122
|
</Button>
|
|
@@ -364,7 +364,9 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
364
364
|
const canvasScale = Math.min(availW / w, availH / h, 1)
|
|
365
365
|
|
|
366
366
|
return (
|
|
367
|
-
<div ref={containerRef} className="flex h-full overflow-hidden bg-
|
|
367
|
+
<div ref={containerRef} className="flex flex-col h-full overflow-hidden bg-[var(--editor-bg)]">
|
|
368
|
+
{/* TOP: slide rail + canvas, fills remaining height */}
|
|
369
|
+
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
368
370
|
<SlideGrid
|
|
369
371
|
project={project}
|
|
370
372
|
slides={slides}
|
|
@@ -385,7 +387,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
385
387
|
className={`absolute top-3 left-3 z-30 flex items-center gap-2 px-3 py-2 rounded-md border transition-colors ${
|
|
386
388
|
refreshState === 'err'
|
|
387
389
|
? 'text-red-300 border-red-500/40 bg-red-950/60 hover:bg-red-900/70'
|
|
388
|
-
: 'text-
|
|
390
|
+
: 'text-[var(--editor-text)] border-[var(--editor-border)] bg-[var(--editor-surface)]/80 hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)] hover:bg-[var(--editor-surface)]'
|
|
389
391
|
}`}
|
|
390
392
|
title={refreshState === 'err' ? 'Refresh failed — check connection' : 'Refresh project'}
|
|
391
393
|
>
|
|
@@ -398,7 +400,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
398
400
|
<button
|
|
399
401
|
onClick={handleRender}
|
|
400
402
|
disabled={rendering || project.status === 'pending' || slides.length === 0}
|
|
401
|
-
className="flex items-center gap-2 px-3 py-2 rounded-md border border-
|
|
403
|
+
className="flex items-center gap-2 px-3 py-2 rounded-md border border-[var(--editor-accent)] bg-[var(--editor-accent)] text-[var(--editor-accent-foreground)] hover:opacity-90 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
402
404
|
title={
|
|
403
405
|
project.status === 'pending'
|
|
404
406
|
? 'Wait for the agent to finish before rendering'
|
|
@@ -416,15 +418,15 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
416
418
|
<div className="flex flex-col items-center gap-6 text-center max-w-lg w-full">
|
|
417
419
|
{slots?.pendingStatus ?? (
|
|
418
420
|
<div className="flex flex-col items-center gap-2">
|
|
419
|
-
<p className="text-
|
|
420
|
-
<p className="text-
|
|
421
|
+
<p className="text-[var(--editor-text)] text-lg font-semibold">Message your agent to start</p>
|
|
422
|
+
<p className="text-[var(--editor-text)]/60 text-sm">Nothing will happen automatically. Copy this and send it to your agent.</p>
|
|
421
423
|
</div>
|
|
422
424
|
)}
|
|
423
425
|
{!slots?.pendingStatus && skillPath && (
|
|
424
|
-
<div className="w-full rounded-xl border-2 border-
|
|
425
|
-
<p className="text-
|
|
426
|
+
<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)]/10">
|
|
427
|
+
<p className="text-[var(--editor-accent)] text-xs font-bold uppercase tracking-widest">Send this to your agent</p>
|
|
426
428
|
<div className="flex items-start justify-between bg-black/60 border border-transparent rounded-lg px-3 py-3 font-mono gap-3">
|
|
427
|
-
<span className="text-
|
|
429
|
+
<span className="text-[var(--editor-text)] text-[12px] leading-relaxed break-all">
|
|
428
430
|
There is a new project pending: "{project.name ?? project.id}". Please see @{skillPath} and start. Talk to me if you run into questions.
|
|
429
431
|
</span>
|
|
430
432
|
<button
|
|
@@ -436,7 +438,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
436
438
|
setTimeout(() => setCopied(false), 2000)
|
|
437
439
|
}}
|
|
438
440
|
className={`shrink-0 flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-md transition-colors ${
|
|
439
|
-
copied ? 'bg-green-700 text-green-200' : 'bg-white/10 text-
|
|
441
|
+
copied ? 'bg-green-700 text-green-200' : 'bg-white/10 text-[var(--editor-text)] hover:bg-white/20 hover:text-[var(--editor-text)]'
|
|
440
442
|
}`}
|
|
441
443
|
title="Copy prompt"
|
|
442
444
|
>
|
|
@@ -445,7 +447,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
445
447
|
</div>
|
|
446
448
|
</div>
|
|
447
449
|
)}
|
|
448
|
-
<p className="text-
|
|
450
|
+
<p className="text-[var(--editor-text)]/40 text-xs font-mono">project id: {project.id}</p>
|
|
449
451
|
</div>
|
|
450
452
|
) : selectedSlide ? (
|
|
451
453
|
<>
|
|
@@ -473,12 +475,12 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
473
475
|
hiddenElementIds={hiddenElementIds}
|
|
474
476
|
/>
|
|
475
477
|
</div>
|
|
476
|
-
<p className="flex-shrink-0 text-xs text-
|
|
478
|
+
<p className="flex-shrink-0 text-xs text-[var(--editor-text)]/60 text-center max-w-md">
|
|
477
479
|
Drag to reposition, resize/rotate via handles, double-click text to edit. Cmd/Ctrl+Z to undo.
|
|
478
480
|
</p>
|
|
479
481
|
</>
|
|
480
482
|
) : (
|
|
481
|
-
<div className="text-
|
|
483
|
+
<div className="text-[var(--editor-text)]/40 text-sm">No slides yet. Add one in the left panel.</div>
|
|
482
484
|
)}
|
|
483
485
|
|
|
484
486
|
{state.lastError && (
|
|
@@ -488,11 +490,14 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
488
490
|
<button onClick={state.clearError} className="ml-2 underline">dismiss</button>
|
|
489
491
|
</div>
|
|
490
492
|
)}
|
|
493
|
+
</div>
|
|
491
494
|
</div>
|
|
492
495
|
|
|
493
|
-
|
|
496
|
+
{/* BELOW: the panels, full width under the canvas. Bounded height with
|
|
497
|
+
internal scrolling so it never crushes the canvas above. */}
|
|
498
|
+
<div className="flex-shrink-0 border-t border-[var(--editor-border)] bg-[var(--editor-bg)] overflow-hidden flex flex-col" style={{ maxHeight: '40%' }}>
|
|
494
499
|
{selectedSlide && project.status !== 'pending' && (
|
|
495
|
-
<div className="px-4 py-2 border-
|
|
500
|
+
<div className="px-4 py-2 border-b border-[var(--editor-border)] bg-[var(--editor-bg)]">
|
|
496
501
|
<AddElementMenu
|
|
497
502
|
project={project}
|
|
498
503
|
selectedSlideId={selectedSlideId}
|
|
@@ -501,32 +506,35 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
501
506
|
/>
|
|
502
507
|
</div>
|
|
503
508
|
)}
|
|
504
|
-
<
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
// w-80) and scroll vertically. Without a width cap, a wide host panel
|
|
524
|
-
// (e.g. a full media-library card) blows out this column and crushes the
|
|
525
|
-
// flex-1 canvas. Host content lays out within the 320px sidebar.
|
|
526
|
-
<div className="w-80 flex-shrink-0 border-t border-gray-800 flex flex-col overflow-y-auto overflow-x-hidden" style={{ minHeight: 180 }}>
|
|
527
|
-
{slots.assetsPanel}
|
|
509
|
+
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
510
|
+
<div className="w-80 flex-shrink-0 overflow-y-auto border-r border-[var(--editor-border)]">
|
|
511
|
+
<SlidePropertyPanel
|
|
512
|
+
project={project}
|
|
513
|
+
slide={selectedSlide}
|
|
514
|
+
element={selectedElement}
|
|
515
|
+
adapter={adapter}
|
|
516
|
+
onSlideChange={handleSlideChange}
|
|
517
|
+
onElementChange={handlePanelElementChange}
|
|
518
|
+
onDeleteSlide={handleDeleteSlide}
|
|
519
|
+
onDuplicateSlide={handleDuplicateSlide}
|
|
520
|
+
onDeleteElement={handleDeleteElement}
|
|
521
|
+
onDuplicateElement={handleDuplicateElement}
|
|
522
|
+
onReorderElement={handleReorderElement}
|
|
523
|
+
onEnterCrop={(_slideId, elementId) => { setSelectedElementId(elementId); setCropElementId(elementId) }}
|
|
524
|
+
updateOverlayProp={state.updateOverlayProp}
|
|
525
|
+
hiddenElementIds={hiddenElementIds}
|
|
526
|
+
onToggleElementVisibility={onToggleElementVisibility}
|
|
527
|
+
/>
|
|
528
528
|
</div>
|
|
529
|
-
|
|
529
|
+
{slots?.assetsPanel && (
|
|
530
|
+
// Below the canvas the assets slot fills the remaining width (no longer
|
|
531
|
+
// capped to a 320px sidebar) and scrolls vertically within the bounded
|
|
532
|
+
// below-canvas region.
|
|
533
|
+
<div className="flex-1 min-w-0 flex flex-col overflow-y-auto overflow-x-hidden">
|
|
534
|
+
{slots.assetsPanel}
|
|
535
|
+
</div>
|
|
536
|
+
)}
|
|
537
|
+
</div>
|
|
530
538
|
</div>
|
|
531
539
|
|
|
532
540
|
{renderOpen && (
|
|
@@ -26,13 +26,13 @@ function slideFile(index: number): string {
|
|
|
26
26
|
|
|
27
27
|
function LogLine({ text }: { text: string }) {
|
|
28
28
|
const t = text.replace(/^\[render\]\s*/, '')
|
|
29
|
-
let color = 'text-
|
|
29
|
+
let color = 'text-[var(--editor-text)]/60'
|
|
30
30
|
if (/done|complete|→/i.test(t)) color = 'text-green-400'
|
|
31
31
|
else if (/rendering|launching|bundling/i.test(t)) color = 'text-sky-400'
|
|
32
32
|
else if (/error|fail/i.test(t)) color = 'text-red-400'
|
|
33
33
|
|
|
34
34
|
const prefix = text.startsWith('[render]')
|
|
35
|
-
? <span className="text-
|
|
35
|
+
? <span className="text-[var(--editor-text)]/40">[render] </span>
|
|
36
36
|
: null
|
|
37
37
|
|
|
38
38
|
return (
|
|
@@ -110,7 +110,7 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
110
110
|
if (status === 'done' && outputDir) {
|
|
111
111
|
return (
|
|
112
112
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-md">
|
|
113
|
-
<div className="w-[96vw] h-[96vh] bg-
|
|
113
|
+
<div className="w-[96vw] h-[96vh] bg-[var(--editor-bg)] border border-[var(--editor-border)] rounded-2xl shadow-2xl flex overflow-hidden">
|
|
114
114
|
|
|
115
115
|
{/* Left — slide gallery */}
|
|
116
116
|
<div className="flex-1 bg-black flex items-center justify-center overflow-auto p-8">
|
|
@@ -127,7 +127,7 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
127
127
|
href={url}
|
|
128
128
|
target="_blank"
|
|
129
129
|
rel="noreferrer"
|
|
130
|
-
className="group relative block rounded-lg overflow-hidden border border-
|
|
130
|
+
className="group relative block rounded-lg overflow-hidden border border-[var(--editor-border)] hover:border-[var(--editor-accent)] transition-colors bg-[var(--editor-surface)]"
|
|
131
131
|
>
|
|
132
132
|
<img
|
|
133
133
|
src={url}
|
|
@@ -135,9 +135,9 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
135
135
|
className="block w-full h-auto"
|
|
136
136
|
style={{ aspectRatio: `${resolution[0]} / ${resolution[1]}` }}
|
|
137
137
|
/>
|
|
138
|
-
<div className="absolute bottom-0 left-0 right-0 px-2 py-1.5 bg-black/70 backdrop-blur-sm text-[11px] text-
|
|
138
|
+
<div className="absolute bottom-0 left-0 right-0 px-2 py-1.5 bg-black/70 backdrop-blur-sm text-[11px] text-[var(--editor-text)] font-mono flex justify-between">
|
|
139
139
|
<span>#{String(i + 1).padStart(2, '0')}</span>
|
|
140
|
-
<span className="text-
|
|
140
|
+
<span className="text-[var(--editor-text)]/60">{file}</span>
|
|
141
141
|
</div>
|
|
142
142
|
</a>
|
|
143
143
|
)
|
|
@@ -146,26 +146,26 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
146
146
|
</div>
|
|
147
147
|
|
|
148
148
|
{/* Right — info panel */}
|
|
149
|
-
<div className="w-72 shrink-0 flex flex-col border-l border-
|
|
150
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-
|
|
149
|
+
<div className="w-72 shrink-0 flex flex-col border-l border-[var(--editor-border)]">
|
|
150
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--editor-border)]">
|
|
151
151
|
<div className="flex items-center gap-2.5">
|
|
152
152
|
<span className="w-2 h-2 rounded-full bg-green-400" />
|
|
153
153
|
<div>
|
|
154
|
-
<p className="text-sm font-semibold text-
|
|
155
|
-
<p className="text-xs text-
|
|
154
|
+
<p className="text-sm font-semibold text-[var(--editor-text)]">Render complete</p>
|
|
155
|
+
<p className="text-xs text-[var(--editor-text)]/60">
|
|
156
156
|
{slidesCount} slide{slidesCount === 1 ? '' : 's'} ready.
|
|
157
157
|
</p>
|
|
158
158
|
</div>
|
|
159
159
|
</div>
|
|
160
|
-
<button onClick={onClose} className="text-
|
|
160
|
+
<button onClick={onClose} className="text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
|
|
161
161
|
</div>
|
|
162
162
|
|
|
163
163
|
<div className="flex flex-col gap-3 p-5 flex-1">
|
|
164
|
-
<p className="text-xs font-mono text-
|
|
164
|
+
<p className="text-xs font-mono text-[var(--editor-text)]/60 break-all leading-relaxed">{outputDir}</p>
|
|
165
165
|
{exportActions}
|
|
166
166
|
<button
|
|
167
167
|
onClick={onClose}
|
|
168
|
-
className="w-full text-center text-sm px-4 py-2.5 rounded-lg bg-
|
|
168
|
+
className="w-full text-center text-sm px-4 py-2.5 rounded-lg bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)] hover:opacity-90 transition-colors"
|
|
169
169
|
>
|
|
170
170
|
Close
|
|
171
171
|
</button>
|
|
@@ -179,37 +179,37 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
179
179
|
// ── Running / error state — log readout ─────────────────────────────────
|
|
180
180
|
return (
|
|
181
181
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
|
|
182
|
-
<div className="w-full max-w-3xl bg-
|
|
182
|
+
<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">
|
|
183
183
|
|
|
184
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-
|
|
184
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--editor-border)]">
|
|
185
185
|
<div className="flex items-center gap-2.5">
|
|
186
186
|
{status === 'running' && <span className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" />}
|
|
187
187
|
{status === 'error' && <span className="w-2 h-2 rounded-full bg-red-400" />}
|
|
188
188
|
<div className="flex flex-col gap-0.5">
|
|
189
|
-
<h2 className="text-sm font-semibold text-
|
|
189
|
+
<h2 className="text-sm font-semibold text-[var(--editor-text)]">
|
|
190
190
|
{status === 'running' ? 'Rendering slides…' : 'Render failed'}
|
|
191
191
|
</h2>
|
|
192
192
|
</div>
|
|
193
193
|
</div>
|
|
194
194
|
{status !== 'running' && (
|
|
195
|
-
<button onClick={onClose} className="text-
|
|
195
|
+
<button onClick={onClose} className="text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] transition-colors text-lg leading-none">×</button>
|
|
196
196
|
)}
|
|
197
197
|
</div>
|
|
198
198
|
|
|
199
199
|
<div className="relative">
|
|
200
200
|
<button
|
|
201
201
|
onClick={() => navigator.clipboard.writeText(logs.join('\n') + (errorMsg ? '\n' + errorMsg : ''))}
|
|
202
|
-
className="absolute top-2 right-2 z-10 text-[10px] px-2 py-0.5 rounded bg-
|
|
202
|
+
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)]/60 hover:text-[var(--editor-text)] hover:border-[var(--editor-accent)] transition-colors"
|
|
203
203
|
title="Copy logs"
|
|
204
204
|
>
|
|
205
205
|
Copy
|
|
206
206
|
</button>
|
|
207
207
|
<div
|
|
208
208
|
ref={logRef}
|
|
209
|
-
className="h-96 overflow-y-auto px-4 py-3 font-mono text-[11px] text-
|
|
209
|
+
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"
|
|
210
210
|
>
|
|
211
211
|
{logs.length === 0 && status === 'running' && (
|
|
212
|
-
<span className="text-
|
|
212
|
+
<span className="text-[var(--editor-text)]/40 italic">Starting render engine…</span>
|
|
213
213
|
)}
|
|
214
214
|
{logs.map((line, i) => (
|
|
215
215
|
<LogLine key={i} text={line} />
|
|
@@ -220,18 +220,18 @@ export default function CarouselRenderModal({ projectId, adapter, slidesCount, r
|
|
|
220
220
|
</div>
|
|
221
221
|
</div>
|
|
222
222
|
|
|
223
|
-
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-
|
|
223
|
+
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-[var(--editor-border)]">
|
|
224
224
|
{status === 'running' ? (
|
|
225
225
|
<button
|
|
226
226
|
onClick={handleCancel}
|
|
227
|
-
className="text-sm px-4 py-1.5 rounded-md bg-
|
|
227
|
+
className="text-sm px-4 py-1.5 rounded-md bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)] hover:bg-red-900/40 hover:border-red-700 hover:text-red-300 transition-colors"
|
|
228
228
|
>
|
|
229
229
|
Cancel
|
|
230
230
|
</button>
|
|
231
231
|
) : (
|
|
232
232
|
<button
|
|
233
233
|
onClick={onClose}
|
|
234
|
-
className="text-sm px-4 py-1.5 rounded-md bg-
|
|
234
|
+
className="text-sm px-4 py-1.5 rounded-md bg-[var(--editor-surface)] border border-[var(--editor-border)] text-[var(--editor-text)] hover:opacity-90 transition-colors"
|
|
235
235
|
>
|
|
236
236
|
Close
|
|
237
237
|
</button>
|
|
@@ -98,12 +98,12 @@ export default function OverlayPicker({ open, onClose, project, adapter, onPick
|
|
|
98
98
|
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
99
99
|
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
|
100
100
|
>
|
|
101
|
-
<div className="bg-
|
|
102
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-
|
|
103
|
-
<h2 className="text-sm font-semibold text-
|
|
101
|
+
<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">
|
|
102
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--editor-border)]">
|
|
103
|
+
<h2 className="text-sm font-semibold text-[var(--editor-text)]">Add Overlay</h2>
|
|
104
104
|
<button
|
|
105
105
|
onClick={onClose}
|
|
106
|
-
className="text-
|
|
106
|
+
className="text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] transition-colors text-lg leading-none"
|
|
107
107
|
>
|
|
108
108
|
×
|
|
109
109
|
</button>
|
|
@@ -111,13 +111,13 @@ export default function OverlayPicker({ open, onClose, project, adapter, onPick
|
|
|
111
111
|
|
|
112
112
|
<div className="flex-1 overflow-y-auto p-4">
|
|
113
113
|
{loading && (
|
|
114
|
-
<div className="text-center text-
|
|
114
|
+
<div className="text-center text-[var(--editor-text)]/60 text-sm py-8">Loading overlays…</div>
|
|
115
115
|
)}
|
|
116
116
|
{error && (
|
|
117
117
|
<div className="text-center text-red-400 text-sm py-8">{error}</div>
|
|
118
118
|
)}
|
|
119
119
|
{!loading && !error && overlays.length === 0 && (
|
|
120
|
-
<div className="text-center text-
|
|
120
|
+
<div className="text-center text-[var(--editor-text)]/60 text-sm py-8">No overlays available</div>
|
|
121
121
|
)}
|
|
122
122
|
{!loading && !error && overlays.length > 0 && (
|
|
123
123
|
<div className="grid grid-cols-3 gap-3">
|
|
@@ -125,14 +125,14 @@ export default function OverlayPicker({ open, onClose, project, adapter, onPick
|
|
|
125
125
|
<button
|
|
126
126
|
key={overlay.jsxPath}
|
|
127
127
|
onClick={() => handlePick(overlay)}
|
|
128
|
-
className="text-left p-3 bg-
|
|
128
|
+
className="text-left p-3 bg-[var(--editor-surface)] hover:opacity-90 border border-[var(--editor-border)] hover:border-[var(--editor-accent)] rounded-lg transition-colors"
|
|
129
129
|
>
|
|
130
|
-
<div className="text-sm font-medium text-
|
|
130
|
+
<div className="text-sm font-medium text-[var(--editor-text)] truncate">{overlay.name}</div>
|
|
131
131
|
{overlay.group && (
|
|
132
|
-
<div className="text-xs text-
|
|
132
|
+
<div className="text-xs text-[var(--editor-accent)] mt-0.5 truncate">{overlay.group}</div>
|
|
133
133
|
)}
|
|
134
134
|
{overlay.description && (
|
|
135
|
-
<div className="text-xs text-
|
|
135
|
+
<div className="text-xs text-[var(--editor-text)]/60 mt-1 line-clamp-2">{overlay.description}</div>
|
|
136
136
|
)}
|
|
137
137
|
</button>
|
|
138
138
|
))}
|
|
@@ -386,7 +386,7 @@ export default function SlideCanvas({
|
|
|
386
386
|
transformOrigin: 'center center',
|
|
387
387
|
pointerEvents: interactive ? 'auto' : 'none',
|
|
388
388
|
userSelect: 'none',
|
|
389
|
-
outline: isSelected ? '1px solid
|
|
389
|
+
outline: isSelected ? '1px solid var(--editor-selection)' : 'none',
|
|
390
390
|
cursor: interactive ? 'grab' : 'default',
|
|
391
391
|
}
|
|
392
392
|
|
|
@@ -442,13 +442,28 @@ export default function SlideCanvas({
|
|
|
442
442
|
/>
|
|
443
443
|
)
|
|
444
444
|
) : (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
445
|
+
// Overlays are authored in NATIVE slide pixels (fixed font sizes sized
|
|
446
|
+
// for element.w×element.h at full resolution). The wrapper is already
|
|
447
|
+
// shrunk to element.w*scale, so render the overlay at native size and
|
|
448
|
+
// CSS-scale it to fit — mirroring the renderer. Without this the
|
|
449
|
+
// native-size text overflows the shrunk box and overlaps (the box
|
|
450
|
+
// sizing only auto-fits resolution-independent content like <img>).
|
|
451
|
+
<div
|
|
452
|
+
style={{
|
|
453
|
+
width: element.w,
|
|
454
|
+
height: element.h,
|
|
455
|
+
transform: `scale(${scale})`,
|
|
456
|
+
transformOrigin: 'top left',
|
|
457
|
+
}}
|
|
449
458
|
>
|
|
450
|
-
<
|
|
451
|
-
|
|
459
|
+
<OverlayErrorBoundary
|
|
460
|
+
label={element.overlay.template.split('/').pop() ?? element.overlay.template}
|
|
461
|
+
watchPath={element.overlay.template}
|
|
462
|
+
watchFile={watchFile}
|
|
463
|
+
>
|
|
464
|
+
<OverlayElementView element={element} compileOverlay={compileOverlay} />
|
|
465
|
+
</OverlayErrorBoundary>
|
|
466
|
+
</div>
|
|
452
467
|
)
|
|
453
468
|
|
|
454
469
|
return (
|
|
@@ -510,7 +525,7 @@ export default function SlideCanvas({
|
|
|
510
525
|
top,
|
|
511
526
|
width: HANDLE_SIZE,
|
|
512
527
|
height: HANDLE_SIZE,
|
|
513
|
-
background: '
|
|
528
|
+
background: 'var(--editor-selection)',
|
|
514
529
|
border: '1px solid #fff',
|
|
515
530
|
borderRadius: 1,
|
|
516
531
|
cursor: h.cursor,
|
|
@@ -529,7 +544,7 @@ export default function SlideCanvas({
|
|
|
529
544
|
top: -ROTATE_OFFSET,
|
|
530
545
|
width: 1,
|
|
531
546
|
height: ROTATE_OFFSET,
|
|
532
|
-
background: '
|
|
547
|
+
background: 'var(--editor-selection)',
|
|
533
548
|
pointerEvents: 'none',
|
|
534
549
|
zIndex: 9,
|
|
535
550
|
}}
|
|
@@ -559,7 +574,7 @@ export default function SlideCanvas({
|
|
|
559
574
|
top: -ROTATE_OFFSET - 14,
|
|
560
575
|
width: 14,
|
|
561
576
|
height: 14,
|
|
562
|
-
background: '
|
|
577
|
+
background: 'var(--editor-selection)',
|
|
563
578
|
border: '2px solid #fff',
|
|
564
579
|
borderRadius: '50%',
|
|
565
580
|
cursor: 'crosshair',
|
|
@@ -575,7 +590,7 @@ export default function SlideCanvas({
|
|
|
575
590
|
bottom: -20,
|
|
576
591
|
left: 0,
|
|
577
592
|
fontSize: 10,
|
|
578
|
-
color: '
|
|
593
|
+
color: 'var(--editor-selection)',
|
|
579
594
|
pointerEvents: 'none',
|
|
580
595
|
whiteSpace: 'nowrap',
|
|
581
596
|
}}
|
|
@@ -60,7 +60,7 @@ function HideToggle({
|
|
|
60
60
|
title={isHidden ? 'Show in editor' : 'Hide from editor'}
|
|
61
61
|
aria-label={isHidden ? 'Show in editor' : 'Hide from editor'}
|
|
62
62
|
aria-pressed={isHidden}
|
|
63
|
-
className="text-
|
|
63
|
+
className="text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] px-1"
|
|
64
64
|
>
|
|
65
65
|
{isHidden ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
66
66
|
</button>
|
|
@@ -75,7 +75,7 @@ function numInput(
|
|
|
75
75
|
) {
|
|
76
76
|
return (
|
|
77
77
|
<label className="flex flex-col gap-0.5">
|
|
78
|
-
<span className="text-xs text-
|
|
78
|
+
<span className="text-xs text-[var(--editor-text)]/60">{label}</span>
|
|
79
79
|
<input
|
|
80
80
|
type="number"
|
|
81
81
|
value={value}
|
|
@@ -86,7 +86,7 @@ function numInput(
|
|
|
86
86
|
const parsed = parseNumber(e.target.value)
|
|
87
87
|
if (parsed !== null) onChange(parsed)
|
|
88
88
|
}}
|
|
89
|
-
className="bg-
|
|
89
|
+
className="bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded px-2 py-1 text-xs text-[var(--editor-text)] focus:outline-none focus:border-[var(--editor-accent)] w-full"
|
|
90
90
|
/>
|
|
91
91
|
</label>
|
|
92
92
|
)
|
|
@@ -110,9 +110,9 @@ function PropEditor({
|
|
|
110
110
|
type="checkbox"
|
|
111
111
|
checked={Boolean(value)}
|
|
112
112
|
onChange={e => onChange(e.target.checked)}
|
|
113
|
-
className="accent-
|
|
113
|
+
className="accent-[var(--editor-accent)]"
|
|
114
114
|
/>
|
|
115
|
-
<span className="text-xs text-
|
|
115
|
+
<span className="text-xs text-[var(--editor-text)]">{name}</span>
|
|
116
116
|
</label>
|
|
117
117
|
)
|
|
118
118
|
}
|
|
@@ -120,12 +120,12 @@ function PropEditor({
|
|
|
120
120
|
if (type === 'color') {
|
|
121
121
|
return (
|
|
122
122
|
<label className="flex flex-col gap-0.5" title={description}>
|
|
123
|
-
<span className="text-xs text-
|
|
123
|
+
<span className="text-xs text-[var(--editor-text)]/60">{name}</span>
|
|
124
124
|
<input
|
|
125
125
|
type="color"
|
|
126
126
|
value={String(value ?? '#000000')}
|
|
127
127
|
onChange={e => onChange(e.target.value)}
|
|
128
|
-
className="w-full h-7 bg-
|
|
128
|
+
className="w-full h-7 bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded cursor-pointer"
|
|
129
129
|
/>
|
|
130
130
|
</label>
|
|
131
131
|
)
|
|
@@ -134,7 +134,7 @@ function PropEditor({
|
|
|
134
134
|
if (type === 'int' || type === 'float') {
|
|
135
135
|
return (
|
|
136
136
|
<label className="flex flex-col gap-0.5" title={description}>
|
|
137
|
-
<span className="text-xs text-
|
|
137
|
+
<span className="text-xs text-[var(--editor-text)]/60">{name}</span>
|
|
138
138
|
<input
|
|
139
139
|
type="number"
|
|
140
140
|
value={Number(value ?? 0)}
|
|
@@ -143,7 +143,7 @@ function PropEditor({
|
|
|
143
143
|
const parsed = parseNumber(e.target.value)
|
|
144
144
|
if (parsed !== null) onChange(parsed)
|
|
145
145
|
}}
|
|
146
|
-
className="bg-
|
|
146
|
+
className="bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded px-2 py-1 text-xs text-[var(--editor-text)] focus:outline-none focus:border-[var(--editor-accent)] w-full"
|
|
147
147
|
/>
|
|
148
148
|
</label>
|
|
149
149
|
)
|
|
@@ -152,12 +152,12 @@ function PropEditor({
|
|
|
152
152
|
// string fallback
|
|
153
153
|
return (
|
|
154
154
|
<label className="flex flex-col gap-0.5" title={description}>
|
|
155
|
-
<span className="text-xs text-
|
|
155
|
+
<span className="text-xs text-[var(--editor-text)]/60">{name}</span>
|
|
156
156
|
<input
|
|
157
157
|
type="text"
|
|
158
158
|
value={String(value ?? '')}
|
|
159
159
|
onChange={e => onChange(e.target.value)}
|
|
160
|
-
className="bg-
|
|
160
|
+
className="bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded px-2 py-1 text-xs text-[var(--editor-text)] focus:outline-none focus:border-[var(--editor-accent)] w-full"
|
|
161
161
|
/>
|
|
162
162
|
</label>
|
|
163
163
|
)
|
|
@@ -206,7 +206,7 @@ export default function SlidePropertyPanel({
|
|
|
206
206
|
|
|
207
207
|
if (!slide) {
|
|
208
208
|
return (
|
|
209
|
-
<div className="w-80 flex-shrink-0 flex items-center justify-center text-
|
|
209
|
+
<div className="w-80 flex-shrink-0 flex items-center justify-center text-[var(--editor-text)]/40 text-xs p-4">
|
|
210
210
|
Select a slide
|
|
211
211
|
</div>
|
|
212
212
|
)
|
|
@@ -216,18 +216,18 @@ export default function SlidePropertyPanel({
|
|
|
216
216
|
const overlaySchema = overlayEl ? overlaySchemas.get(overlayEl.overlay.template) : null
|
|
217
217
|
|
|
218
218
|
return (
|
|
219
|
-
<div className="w-80 flex-shrink-0 border-l border-
|
|
219
|
+
<div className="w-80 flex-shrink-0 border-l border-[var(--editor-border)] flex flex-col overflow-y-auto bg-[var(--editor-bg)]">
|
|
220
220
|
{/* Slide header */}
|
|
221
|
-
<div className="px-4 py-3 border-b border-
|
|
222
|
-
<div className="text-xs font-semibold text-
|
|
221
|
+
<div className="px-4 py-3 border-b border-[var(--editor-border)]">
|
|
222
|
+
<div className="text-xs font-semibold text-[var(--editor-text)]/60 uppercase tracking-wider mb-2">Slide</div>
|
|
223
223
|
<div className="flex flex-col gap-2">
|
|
224
224
|
<label className="flex flex-col gap-0.5">
|
|
225
|
-
<span className="text-xs text-
|
|
225
|
+
<span className="text-xs text-[var(--editor-text)]/60">Background color</span>
|
|
226
226
|
<input
|
|
227
227
|
type="color"
|
|
228
228
|
value={slide.base_color || '#ffffff'}
|
|
229
229
|
onChange={e => onSlideChange({ base_color: e.target.value })}
|
|
230
|
-
className="w-full h-7 bg-
|
|
230
|
+
className="w-full h-7 bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded cursor-pointer"
|
|
231
231
|
/>
|
|
232
232
|
</label>
|
|
233
233
|
<div className="flex gap-2">
|
|
@@ -255,7 +255,7 @@ export default function SlidePropertyPanel({
|
|
|
255
255
|
{element && (
|
|
256
256
|
<div className="px-4 py-3 flex flex-col gap-3">
|
|
257
257
|
<div className="flex items-center justify-between">
|
|
258
|
-
<span className="text-xs font-semibold text-
|
|
258
|
+
<span className="text-xs font-semibold text-[var(--editor-text)]/60 uppercase tracking-wider">
|
|
259
259
|
{element.type === 'image' ? 'Image' : 'Overlay'}
|
|
260
260
|
</span>
|
|
261
261
|
<div className="flex items-center gap-1">
|
|
@@ -266,14 +266,14 @@ export default function SlidePropertyPanel({
|
|
|
266
266
|
/>
|
|
267
267
|
<button
|
|
268
268
|
onClick={() => onReorderElement(slide.id, element.id, 'forward')}
|
|
269
|
-
className="text-xs text-
|
|
269
|
+
className="text-xs text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] px-1"
|
|
270
270
|
title="Bring forward"
|
|
271
271
|
>
|
|
272
272
|
↑
|
|
273
273
|
</button>
|
|
274
274
|
<button
|
|
275
275
|
onClick={() => onReorderElement(slide.id, element.id, 'backward')}
|
|
276
|
-
className="text-xs text-
|
|
276
|
+
className="text-xs text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] px-1"
|
|
277
277
|
title="Send backward"
|
|
278
278
|
>
|
|
279
279
|
↓
|
|
@@ -294,8 +294,8 @@ export default function SlidePropertyPanel({
|
|
|
294
294
|
{element.type === 'image' && (
|
|
295
295
|
<div className="flex flex-col gap-1.5">
|
|
296
296
|
<div className="flex flex-col gap-0.5">
|
|
297
|
-
<span className="text-xs text-
|
|
298
|
-
<span className="text-xs text-
|
|
297
|
+
<span className="text-xs text-[var(--editor-text)]/60">Source</span>
|
|
298
|
+
<span className="text-xs text-[var(--editor-text)] truncate" title={element.src}>
|
|
299
299
|
{element.src.split('/').pop() || element.src}
|
|
300
300
|
</span>
|
|
301
301
|
</div>
|
|
@@ -335,12 +335,12 @@ export default function SlidePropertyPanel({
|
|
|
335
335
|
</div>
|
|
336
336
|
|
|
337
337
|
{schemasLoading && (
|
|
338
|
-
<div className="text-xs text-
|
|
338
|
+
<div className="text-xs text-[var(--editor-text)]/60">Loading overlay props…</div>
|
|
339
339
|
)}
|
|
340
340
|
|
|
341
341
|
{!schemasLoading && overlaySchema && overlaySchema.props.length > 0 && (
|
|
342
342
|
<div className="flex flex-col gap-2">
|
|
343
|
-
<span className="text-xs text-
|
|
343
|
+
<span className="text-xs text-[var(--editor-text)]/60 font-medium">Props</span>
|
|
344
344
|
{overlaySchema.props.map(prop => (
|
|
345
345
|
<PropEditor
|
|
346
346
|
key={prop.name}
|
|
@@ -97,10 +97,12 @@ describe('CarouselEditor — editor-core integration', () => {
|
|
|
97
97
|
/>,
|
|
98
98
|
)
|
|
99
99
|
await waitFor(() => getByTestId('assets'))
|
|
100
|
-
//
|
|
101
|
-
//
|
|
100
|
+
// The assets slot now lives in the below-canvas region and fills the remaining
|
|
101
|
+
// width (flex-1) rather than being capped to a 320px sidebar — so a wide host
|
|
102
|
+
// panel uses its full share of the row without crushing the canvas above.
|
|
102
103
|
const wrapper = getByTestId('assets').parentElement
|
|
103
|
-
expect(wrapper?.className).toContain('
|
|
104
|
+
expect(wrapper?.className).toContain('flex-1')
|
|
105
|
+
expect(wrapper?.className).not.toContain('w-80')
|
|
104
106
|
})
|
|
105
107
|
|
|
106
108
|
// Regression: SlideGrid thumbnails must receive `compileOverlay` so overlay
|
package/src/text/FontPicker.tsx
CHANGED
|
@@ -95,7 +95,7 @@ export function FontFamilyPicker({ value, onChange, disabled, className, buttonC
|
|
|
95
95
|
onClick={() => setOpen((o) => !o)}
|
|
96
96
|
className={
|
|
97
97
|
buttonClassName ??
|
|
98
|
-
'flex w-full items-center gap-1 rounded-md border border-
|
|
98
|
+
'flex w-full items-center gap-1 rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] px-2 py-1 text-sm text-[var(--editor-text)] hover:opacity-90 focus:outline-none focus:ring-1 focus:ring-[var(--editor-accent)] disabled:opacity-50'
|
|
99
99
|
}
|
|
100
100
|
style={displayStyle}
|
|
101
101
|
aria-haspopup="listbox"
|
|
@@ -109,7 +109,7 @@ export function FontFamilyPicker({ value, onChange, disabled, className, buttonC
|
|
|
109
109
|
{open && (
|
|
110
110
|
<div
|
|
111
111
|
role="listbox"
|
|
112
|
-
className="absolute right-0 z-50 mt-1 max-h-80 w-60 overflow-y-auto rounded-md border border-
|
|
112
|
+
className="absolute right-0 z-50 mt-1 max-h-80 w-60 overflow-y-auto rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] shadow-xl ring-1 ring-black/20"
|
|
113
113
|
>
|
|
114
114
|
<ul className="py-1">
|
|
115
115
|
{FONT_OPTIONS.map((opt) => {
|
|
@@ -125,14 +125,14 @@ export function FontFamilyPicker({ value, onChange, disabled, className, buttonC
|
|
|
125
125
|
setOpen(false)
|
|
126
126
|
}}
|
|
127
127
|
style={{ fontFamily: opt.value }}
|
|
128
|
-
className={`flex w-full items-center justify-between px-3 py-2 text-left text-[15px] leading-tight text-
|
|
129
|
-
isActive ? 'bg-
|
|
128
|
+
className={`flex w-full items-center justify-between px-3 py-2 text-left text-[15px] leading-tight text-[var(--editor-text)] hover:bg-[var(--editor-accent)]/20 focus:bg-[var(--editor-accent)]/20 focus:outline-none ${
|
|
129
|
+
isActive ? 'bg-[var(--editor-accent)]/20 font-medium' : ''
|
|
130
130
|
}`}
|
|
131
131
|
>
|
|
132
132
|
<span className="truncate">{opt.label}</span>
|
|
133
133
|
{isActive && (
|
|
134
134
|
<span
|
|
135
|
-
className="ml-2 shrink-0 text-xs text-
|
|
135
|
+
className="ml-2 shrink-0 text-xs text-[var(--editor-text)]/60"
|
|
136
136
|
style={{ fontFamily: 'system-ui, sans-serif' }}
|
|
137
137
|
>
|
|
138
138
|
✓
|
|
@@ -210,7 +210,7 @@ export function FontSizePicker({ value, onChange, disabled, min = 8, max = 9999,
|
|
|
210
210
|
}}
|
|
211
211
|
className={
|
|
212
212
|
className ??
|
|
213
|
-
'w-14 rounded-md border border-
|
|
213
|
+
'w-14 rounded-md border border-[var(--editor-border)] bg-[var(--editor-surface)] px-2 py-1 text-sm text-[var(--editor-text)] focus:outline-none focus:ring-1 focus:ring-[var(--editor-accent)] disabled:opacity-50'
|
|
214
214
|
}
|
|
215
215
|
aria-label="Font size"
|
|
216
216
|
/>
|
|
@@ -96,10 +96,10 @@ export function TextFormattingToolbar({
|
|
|
96
96
|
const rawColor = readPropAsString(element, 'color')
|
|
97
97
|
const colorValue = HEX_PATTERN.test(rawColor) && rawColor.length === 7 ? rawColor : '#111111'
|
|
98
98
|
|
|
99
|
-
//
|
|
100
|
-
const toolbarBtnBase = 'flex items-center justify-center rounded px-1.5 py-1 text-sm transition-colors hover:bg-
|
|
101
|
-
const toolbarBtnActive = 'bg-
|
|
102
|
-
const toolbarBtnInactive = 'text-
|
|
99
|
+
// Themed button styles: surface base, accent-tinted hover/active, accent ring.
|
|
100
|
+
const toolbarBtnBase = 'flex items-center justify-center rounded px-1.5 py-1 text-sm transition-colors hover:bg-[var(--editor-accent)]/20 focus:outline-none focus:ring-1 focus:ring-[var(--editor-accent)]'
|
|
101
|
+
const toolbarBtnActive = 'bg-[var(--editor-accent)]/20 text-[var(--editor-text)]'
|
|
102
|
+
const toolbarBtnInactive = 'text-[var(--editor-text)]/60'
|
|
103
103
|
|
|
104
104
|
// Divider visibility helpers
|
|
105
105
|
const hasLeftGroup = supported.has('fontWeight') || supported.has('fontStyle') || supported.has('textTransform') || supported.has('color')
|
|
@@ -108,7 +108,7 @@ export function TextFormattingToolbar({
|
|
|
108
108
|
|
|
109
109
|
return (
|
|
110
110
|
<div
|
|
111
|
-
className="rounded-lg border border-
|
|
111
|
+
className="rounded-lg border border-[var(--editor-border)] bg-[var(--editor-surface)] text-[var(--editor-text)] shadow-md"
|
|
112
112
|
onPointerDown={(e) => e.stopPropagation()}
|
|
113
113
|
onPointerMove={(e) => e.stopPropagation()}
|
|
114
114
|
onClick={(e) => e.stopPropagation()}
|
|
@@ -159,7 +159,7 @@ export function TextFormattingToolbar({
|
|
|
159
159
|
title="Text color"
|
|
160
160
|
>
|
|
161
161
|
<span
|
|
162
|
-
className="block h-3.5 w-3.5 rounded-sm border border-
|
|
162
|
+
className="block h-3.5 w-3.5 rounded-sm border border-[var(--editor-border)]"
|
|
163
163
|
style={{ backgroundColor: colorValue }}
|
|
164
164
|
aria-hidden
|
|
165
165
|
/>
|
|
@@ -174,7 +174,7 @@ export function TextFormattingToolbar({
|
|
|
174
174
|
)}
|
|
175
175
|
|
|
176
176
|
{hasLeftGroup && hasMidGroup && (
|
|
177
|
-
<div className="mx-1 h-4 w-px bg-
|
|
177
|
+
<div className="mx-1 h-4 w-px bg-[var(--editor-border)]" aria-hidden />
|
|
178
178
|
)}
|
|
179
179
|
|
|
180
180
|
{supported.has('fontSize') && (
|
|
@@ -193,7 +193,7 @@ export function TextFormattingToolbar({
|
|
|
193
193
|
)}
|
|
194
194
|
|
|
195
195
|
{hasMidGroup && hasRightGroup && (
|
|
196
|
-
<div className="mx-1 h-4 w-px bg-
|
|
196
|
+
<div className="mx-1 h-4 w-px bg-[var(--editor-border)]" aria-hidden />
|
|
197
197
|
)}
|
|
198
198
|
|
|
199
199
|
{supported.has('textAlign') && (
|
|
@@ -224,19 +224,19 @@ export function TextFormattingToolbar({
|
|
|
224
224
|
)}
|
|
225
225
|
|
|
226
226
|
{!hasAnyControlProp && (
|
|
227
|
-
<span className="px-2 py-1 text-xs text-
|
|
227
|
+
<span className="px-2 py-1 text-xs text-[var(--editor-text)]/60">
|
|
228
228
|
Edit via property panel
|
|
229
229
|
</span>
|
|
230
230
|
)}
|
|
231
231
|
|
|
232
232
|
{onDelete && (
|
|
233
233
|
<>
|
|
234
|
-
{hasAnyControlProp && <div className="mx-1 h-4 w-px bg-
|
|
234
|
+
{hasAnyControlProp && <div className="mx-1 h-4 w-px bg-[var(--editor-border)]" aria-hidden />}
|
|
235
235
|
<button
|
|
236
236
|
type="button"
|
|
237
237
|
aria-label="Delete text overlay"
|
|
238
238
|
onClick={onDelete}
|
|
239
|
-
className={`${toolbarBtnBase} text-
|
|
239
|
+
className={`${toolbarBtnBase} text-[var(--editor-text)]/60 hover:bg-red-900/30 hover:text-red-400`}
|
|
240
240
|
>
|
|
241
241
|
<Trash2 className="h-3.5 w-3.5" />
|
|
242
242
|
</button>
|
package/src/theme.ts
CHANGED
|
@@ -6,36 +6,49 @@
|
|
|
6
6
|
* ── CSS variable naming convention ───────────────────────────────────────────
|
|
7
7
|
* Every token is written as a custom property prefixed `--editor-`:
|
|
8
8
|
* colors → --editor-bg, --editor-surface, --editor-accent,
|
|
9
|
-
* --editor-
|
|
9
|
+
* --editor-accent-foreground, --editor-text, --editor-border,
|
|
10
|
+
* --editor-selection
|
|
10
11
|
* fonts → --editor-font-sans, --editor-font-serif, --editor-font-display
|
|
11
12
|
* radii → --editor-radius-{sm|md|lg}
|
|
12
13
|
* spacing → --editor-space-{n} (n = scale step)
|
|
13
14
|
*
|
|
14
15
|
* Optional tokens (serif/display fonts) are only written when present, so a
|
|
15
|
-
* host can detect their absence via an empty `getPropertyValue`.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* host can detect their absence via an empty `getPropertyValue`. The carousel
|
|
17
|
+
* editor chrome (shell, panels, toolbars, buttons, selection handles)
|
|
18
|
+
* references these vars via Tailwind arbitrary values (`bg-[var(--editor-bg)]`,
|
|
19
|
+
* etc.) and inline styles — so passing a host theme actually re-skins the
|
|
20
|
+
* editor, rather than only setting CSS vars nothing reads.
|
|
18
21
|
*/
|
|
19
22
|
import type { EditorTheme } from './types'
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
* Montaj's default editor theme. Values
|
|
23
|
-
*
|
|
24
|
-
* background gray-
|
|
25
|
-
* surface gray-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
25
|
+
* Montaj's default editor theme. Values are chosen to preserve the look the
|
|
26
|
+
* chrome rendered before it was var-driven, which used three distinct grays:
|
|
27
|
+
* background gray-950 (#030712) — the editor shell (`bg-gray-950`)
|
|
28
|
+
* surface gray-900 (#111827) — raised panels/inputs/buttons (the
|
|
29
|
+
* dominant of the former gray-900/gray-800 surfaces)
|
|
30
|
+
* border gray-800 (#1f2937) — the dominant hairline (former
|
|
31
|
+
* gray-800/700 borders)
|
|
32
|
+
* text gray-100 (#f3f4f6) — primary text
|
|
33
|
+
* accent indigo-500 (#6366f1) — Montaj's interactive accent (Render
|
|
34
|
+
* button, focus rings, accent borders)
|
|
35
|
+
* accentForeground white (#ffffff) — readable on the indigo accent
|
|
36
|
+
* selection indigo-400 (#818cf8) — element-selection outline/handles
|
|
37
|
+
* (the former hardcoded #3b82f6 blue)
|
|
30
38
|
* font Inter (the configured `fontFamily.sans`)
|
|
39
|
+
*
|
|
40
|
+
* Collapsing the former 3-gray surface/border set into bg/surface/border keeps
|
|
41
|
+
* Montaj's chrome visually stable; muted text is rendered as `text` at reduced
|
|
42
|
+
* opacity by the chrome rather than as a separate token.
|
|
31
43
|
*/
|
|
32
44
|
export const defaultMontajTheme: EditorTheme = {
|
|
33
45
|
colors: {
|
|
34
|
-
background: '#
|
|
35
|
-
surface: '#
|
|
46
|
+
background: '#030712',
|
|
47
|
+
surface: '#111827',
|
|
36
48
|
accent: '#6366f1',
|
|
49
|
+
accentForeground: '#ffffff',
|
|
37
50
|
text: '#f3f4f6',
|
|
38
|
-
border: '#
|
|
51
|
+
border: '#1f2937',
|
|
39
52
|
selection: '#818cf8',
|
|
40
53
|
},
|
|
41
54
|
fonts: {
|
|
@@ -68,6 +81,9 @@ export function applyTheme(el: HTMLElement, theme: EditorTheme): void {
|
|
|
68
81
|
style.setProperty('--editor-bg', theme.colors.background)
|
|
69
82
|
style.setProperty('--editor-surface', theme.colors.surface)
|
|
70
83
|
style.setProperty('--editor-accent', theme.colors.accent)
|
|
84
|
+
// Accent-foreground falls back to `text` so it's never empty (e.g. a host
|
|
85
|
+
// theme that omits it still gets a readable foreground for accent controls).
|
|
86
|
+
style.setProperty('--editor-accent-foreground', theme.colors.accentForeground ?? theme.colors.text)
|
|
71
87
|
style.setProperty('--editor-text', theme.colors.text)
|
|
72
88
|
style.setProperty('--editor-border', theme.colors.border)
|
|
73
89
|
style.setProperty('--editor-selection', theme.colors.selection)
|
package/src/types.ts
CHANGED
|
@@ -337,6 +337,11 @@ export interface EditorTheme {
|
|
|
337
337
|
surface: string
|
|
338
338
|
/** Primary interactive/brand accent. */
|
|
339
339
|
accent: string
|
|
340
|
+
/**
|
|
341
|
+
* Readable foreground to pair with `accent` — e.g. dark text on a yellow
|
|
342
|
+
* accent button. Optional; when absent, `applyTheme` falls back to `text`.
|
|
343
|
+
*/
|
|
344
|
+
accentForeground?: string
|
|
340
345
|
/** Default text color. */
|
|
341
346
|
text: string
|
|
342
347
|
/** Hairline/divider color. */
|
package/src/ui/badge.tsx
CHANGED
|
@@ -9,7 +9,7 @@ const badgeVariants = cva(
|
|
|
9
9
|
pending: 'bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300',
|
|
10
10
|
draft: 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300',
|
|
11
11
|
final: 'bg-emerald-100 text-emerald-700 dark:bg-green-900/50 dark:text-green-300',
|
|
12
|
-
default: 'bg-gray-100 text-gray-600 dark:bg-
|
|
12
|
+
default: 'bg-gray-100 text-gray-600 dark:bg-[var(--editor-surface)] dark:text-[var(--editor-text)]',
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
defaultVariants: { variant: 'default' },
|
package/src/ui/button.tsx
CHANGED
|
@@ -2,15 +2,15 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
|
|
2
2
|
import { cn } from './utils'
|
|
3
3
|
|
|
4
4
|
const buttonVariants = cva(
|
|
5
|
-
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-
|
|
5
|
+
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--editor-accent)] disabled:pointer-events-none disabled:opacity-50',
|
|
6
6
|
{
|
|
7
7
|
variants: {
|
|
8
8
|
variant: {
|
|
9
|
-
default: 'bg-
|
|
10
|
-
secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-
|
|
11
|
-
ghost: 'text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-
|
|
9
|
+
default: 'bg-[var(--editor-accent)] text-[var(--editor-accent-foreground)] hover:opacity-90',
|
|
10
|
+
secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-[var(--editor-surface)] dark:text-[var(--editor-text)] dark:hover:opacity-90',
|
|
11
|
+
ghost: 'text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-[var(--editor-text)]/60 dark:hover:bg-[var(--editor-surface)] dark:hover:text-[var(--editor-text)]',
|
|
12
12
|
danger: 'bg-red-600 text-white hover:bg-red-700',
|
|
13
|
-
outline: 'border border-gray-300 dark:border-
|
|
13
|
+
outline: 'border border-gray-300 dark:border-[var(--editor-border)] text-gray-700 dark:text-[var(--editor-text)] hover:bg-gray-100 dark:hover:bg-[var(--editor-surface)]',
|
|
14
14
|
},
|
|
15
15
|
size: {
|
|
16
16
|
default: 'h-9 px-4 py-2',
|
package/src/ui/input.tsx
CHANGED
|
@@ -6,7 +6,7 @@ export function Input({ className, ...props }: InputProps) {
|
|
|
6
6
|
return (
|
|
7
7
|
<input
|
|
8
8
|
className={cn(
|
|
9
|
-
'flex h-9 w-full rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-
|
|
9
|
+
'flex h-9 w-full rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-[var(--editor-accent)] disabled:opacity-50 dark:border-[var(--editor-border)] dark:bg-[var(--editor-surface)] dark:text-[var(--editor-text)] dark:placeholder:text-[var(--editor-text)]/60',
|
|
10
10
|
className,
|
|
11
11
|
)}
|
|
12
12
|
{...props}
|
package/src/ui/label.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { cn } from './utils'
|
|
|
3
3
|
export function Label({ className, ...props }: React.LabelHTMLAttributes<HTMLLabelElement>) {
|
|
4
4
|
return (
|
|
5
5
|
<label
|
|
6
|
-
className={cn('text-xs font-medium text-
|
|
6
|
+
className={cn('text-xs font-medium text-[var(--editor-text)]/60 leading-none', className)}
|
|
7
7
|
{...props}
|
|
8
8
|
/>
|
|
9
9
|
)
|
package/src/ui/select.tsx
CHANGED
|
@@ -8,7 +8,7 @@ export function Select({ className, options, ...props }: SelectProps) {
|
|
|
8
8
|
return (
|
|
9
9
|
<select
|
|
10
10
|
className={cn(
|
|
11
|
-
'flex h-9 w-full rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-
|
|
11
|
+
'flex h-9 w-full rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-[var(--editor-accent)] disabled:opacity-50 dark:border-[var(--editor-border)] dark:bg-[var(--editor-surface)] dark:text-[var(--editor-text)]',
|
|
12
12
|
className,
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
package/src/ui/switch.tsx
CHANGED
|
@@ -15,8 +15,8 @@ export function Switch({ checked, onCheckedChange, className, disabled }: Switch
|
|
|
15
15
|
disabled={disabled}
|
|
16
16
|
onClick={() => onCheckedChange(!checked)}
|
|
17
17
|
className={cn(
|
|
18
|
-
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-
|
|
19
|
-
checked ? 'bg-
|
|
18
|
+
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--editor-accent)] disabled:opacity-50',
|
|
19
|
+
checked ? 'bg-[var(--editor-accent)]' : 'bg-gray-300 dark:bg-[var(--editor-border)]',
|
|
20
20
|
className,
|
|
21
21
|
)}
|
|
22
22
|
>
|
package/src/ui/textarea.tsx
CHANGED
|
@@ -6,7 +6,7 @@ export function Textarea({ className, ...props }: TextareaProps) {
|
|
|
6
6
|
return (
|
|
7
7
|
<textarea
|
|
8
8
|
className={cn(
|
|
9
|
-
'flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-
|
|
9
|
+
'flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-[var(--editor-accent)] disabled:opacity-50 resize-none dark:border-[var(--editor-border)] dark:bg-[var(--editor-surface)] dark:text-[var(--editor-text)] dark:placeholder:text-[var(--editor-text)]/60',
|
|
10
10
|
className,
|
|
11
11
|
)}
|
|
12
12
|
{...props}
|