@bycrux/editor 0.4.1 → 0.4.3
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 +38 -30
- 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 +30 -25
- 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) => (
|
|
@@ -84,12 +84,12 @@ function SlideGrid({
|
|
|
84
84
|
onDrop={() => handleDrop(idx)}
|
|
85
85
|
onDragEnd={handleDragEnd}
|
|
86
86
|
onClick={() => onSelect(slide.id)}
|
|
87
|
-
className={`group relative cursor-pointer rounded overflow-hidden border transition-colors ${
|
|
87
|
+
className={`group relative flex-shrink-0 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>
|
|
@@ -357,14 +357,18 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
357
357
|
obs.observe(el)
|
|
358
358
|
return () => obs.disconnect()
|
|
359
359
|
}, [])
|
|
360
|
-
const PADDING =
|
|
361
|
-
const HINT_RESERVE =
|
|
360
|
+
const PADDING = 32
|
|
361
|
+
const HINT_RESERVE = 28
|
|
362
362
|
const availW = Math.max(0, canvasContainerSize.w - PADDING)
|
|
363
363
|
const availH = Math.max(0, canvasContainerSize.h - PADDING - HINT_RESERVE)
|
|
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-
|
|
367
|
+
<div ref={containerRef} className="flex flex-col h-full overflow-y-auto bg-[var(--editor-bg)]">
|
|
368
|
+
{/* TOP: slide rail + canvas. Given a generous viewport-relative height so
|
|
369
|
+
the slide renders large; the below-panels region flows beneath and the
|
|
370
|
+
whole editor scrolls vertically. */}
|
|
371
|
+
<div className="flex flex-shrink-0 min-h-[80vh] overflow-hidden">
|
|
368
372
|
<SlideGrid
|
|
369
373
|
project={project}
|
|
370
374
|
slides={slides}
|
|
@@ -378,14 +382,14 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
378
382
|
compileOverlay={(t) => adapter.compileOverlay(t)}
|
|
379
383
|
/>
|
|
380
384
|
|
|
381
|
-
<div ref={canvasContainerRef} className="relative flex-1 flex flex-col items-center justify-center gap-4 overflow-hidden p-
|
|
385
|
+
<div ref={canvasContainerRef} className="relative flex-1 flex flex-col items-center justify-center gap-4 overflow-hidden p-4">
|
|
382
386
|
<button
|
|
383
387
|
onClick={handleRefresh}
|
|
384
388
|
disabled={refreshing}
|
|
385
389
|
className={`absolute top-3 left-3 z-30 flex items-center gap-2 px-3 py-2 rounded-md border transition-colors ${
|
|
386
390
|
refreshState === 'err'
|
|
387
391
|
? 'text-red-300 border-red-500/40 bg-red-950/60 hover:bg-red-900/70'
|
|
388
|
-
: 'text-
|
|
392
|
+
: '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
393
|
}`}
|
|
390
394
|
title={refreshState === 'err' ? 'Refresh failed — check connection' : 'Refresh project'}
|
|
391
395
|
>
|
|
@@ -398,7 +402,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
398
402
|
<button
|
|
399
403
|
onClick={handleRender}
|
|
400
404
|
disabled={rendering || project.status === 'pending' || slides.length === 0}
|
|
401
|
-
className="flex items-center gap-2 px-3 py-2 rounded-md border border-
|
|
405
|
+
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
406
|
title={
|
|
403
407
|
project.status === 'pending'
|
|
404
408
|
? 'Wait for the agent to finish before rendering'
|
|
@@ -416,15 +420,15 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
416
420
|
<div className="flex flex-col items-center gap-6 text-center max-w-lg w-full">
|
|
417
421
|
{slots?.pendingStatus ?? (
|
|
418
422
|
<div className="flex flex-col items-center gap-2">
|
|
419
|
-
<p className="text-
|
|
420
|
-
<p className="text-
|
|
423
|
+
<p className="text-[var(--editor-text)] text-lg font-semibold">Message your agent to start</p>
|
|
424
|
+
<p className="text-[var(--editor-text)]/60 text-sm">Nothing will happen automatically. Copy this and send it to your agent.</p>
|
|
421
425
|
</div>
|
|
422
426
|
)}
|
|
423
427
|
{!slots?.pendingStatus && skillPath && (
|
|
424
|
-
<div className="w-full rounded-xl border-2 border-
|
|
425
|
-
<p className="text-
|
|
428
|
+
<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">
|
|
429
|
+
<p className="text-[var(--editor-accent)] text-xs font-bold uppercase tracking-widest">Send this to your agent</p>
|
|
426
430
|
<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-
|
|
431
|
+
<span className="text-[var(--editor-text)] text-[12px] leading-relaxed break-all">
|
|
428
432
|
There is a new project pending: "{project.name ?? project.id}". Please see @{skillPath} and start. Talk to me if you run into questions.
|
|
429
433
|
</span>
|
|
430
434
|
<button
|
|
@@ -436,7 +440,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
436
440
|
setTimeout(() => setCopied(false), 2000)
|
|
437
441
|
}}
|
|
438
442
|
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-
|
|
443
|
+
copied ? 'bg-green-700 text-green-200' : 'bg-white/10 text-[var(--editor-text)] hover:bg-white/20 hover:text-[var(--editor-text)]'
|
|
440
444
|
}`}
|
|
441
445
|
title="Copy prompt"
|
|
442
446
|
>
|
|
@@ -445,7 +449,7 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
445
449
|
</div>
|
|
446
450
|
</div>
|
|
447
451
|
)}
|
|
448
|
-
<p className="text-
|
|
452
|
+
<p className="text-[var(--editor-text)]/40 text-xs font-mono">project id: {project.id}</p>
|
|
449
453
|
</div>
|
|
450
454
|
) : selectedSlide ? (
|
|
451
455
|
<>
|
|
@@ -473,12 +477,12 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
473
477
|
hiddenElementIds={hiddenElementIds}
|
|
474
478
|
/>
|
|
475
479
|
</div>
|
|
476
|
-
<p className="flex-shrink-0 text-xs text-
|
|
480
|
+
<p className="flex-shrink-0 text-xs text-[var(--editor-text)]/60 text-center max-w-md">
|
|
477
481
|
Drag to reposition, resize/rotate via handles, double-click text to edit. Cmd/Ctrl+Z to undo.
|
|
478
482
|
</p>
|
|
479
483
|
</>
|
|
480
484
|
) : (
|
|
481
|
-
<div className="text-
|
|
485
|
+
<div className="text-[var(--editor-text)]/40 text-sm">No slides yet. Add one in the left panel.</div>
|
|
482
486
|
)}
|
|
483
487
|
|
|
484
488
|
{state.lastError && (
|
|
@@ -488,11 +492,16 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
488
492
|
<button onClick={state.clearError} className="ml-2 underline">dismiss</button>
|
|
489
493
|
</div>
|
|
490
494
|
)}
|
|
495
|
+
</div>
|
|
491
496
|
</div>
|
|
492
497
|
|
|
493
|
-
|
|
498
|
+
{/* BELOW: the slide editor, stacked vertically full-width under the canvas.
|
|
499
|
+
Flows beneath the tall canvas region and scrolls with the page (the root
|
|
500
|
+
is overflow-y-auto). Order: add-element toolbar → property panel →
|
|
501
|
+
project media (assets) at the very bottom. */}
|
|
502
|
+
<div className="flex-shrink-0 border-t border-[var(--editor-border)] bg-[var(--editor-bg)] flex flex-col">
|
|
494
503
|
{selectedSlide && project.status !== 'pending' && (
|
|
495
|
-
<div className="px-4 py-2 border-
|
|
504
|
+
<div className="px-4 py-2 border-b border-[var(--editor-border)] bg-[var(--editor-bg)]">
|
|
496
505
|
<AddElementMenu
|
|
497
506
|
project={project}
|
|
498
507
|
selectedSlideId={selectedSlideId}
|
|
@@ -517,13 +526,12 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
517
526
|
updateOverlayProp={state.updateOverlayProp}
|
|
518
527
|
hiddenElementIds={hiddenElementIds}
|
|
519
528
|
onToggleElementVisibility={onToggleElementVisibility}
|
|
529
|
+
// Stacked full-width here (drop the default w-80 sidebar + left border).
|
|
530
|
+
className="w-full border-l-0 border-b border-[var(--editor-border)]"
|
|
520
531
|
/>
|
|
521
532
|
{slots?.assetsPanel && (
|
|
522
|
-
//
|
|
523
|
-
|
|
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 }}>
|
|
533
|
+
// Project media — at the very bottom, full width.
|
|
534
|
+
<div className="w-full flex flex-col">
|
|
527
535
|
{slots.assetsPanel}
|
|
528
536
|
</div>
|
|
529
537
|
)}
|
|
@@ -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
|
}}
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
GlobalOverlayProp,
|
|
10
10
|
EditorAdapter,
|
|
11
11
|
} from '../types'
|
|
12
|
-
import { Button } from '../ui'
|
|
12
|
+
import { Button, cn } from '../ui'
|
|
13
13
|
import { TextFormattingToolbar } from '../text/TextFormattingToolbar'
|
|
14
14
|
|
|
15
15
|
function parseNumber(v: string): number | null {
|
|
@@ -39,6 +39,10 @@ interface Props {
|
|
|
39
39
|
// selected element; `hiddenElementIds` reflects the current hidden set.
|
|
40
40
|
hiddenElementIds?: string[]
|
|
41
41
|
onToggleElementVisibility?: (elementId: string) => void
|
|
42
|
+
// Override the panel's root container classes. Hosts that stack the panel
|
|
43
|
+
// full-width (e.g. below the canvas) pass this to drop the default `w-80`
|
|
44
|
+
// sidebar constraint.
|
|
45
|
+
className?: string
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
// Small eye toggle to hide/show the selected element in the editor preview only
|
|
@@ -60,7 +64,7 @@ function HideToggle({
|
|
|
60
64
|
title={isHidden ? 'Show in editor' : 'Hide from editor'}
|
|
61
65
|
aria-label={isHidden ? 'Show in editor' : 'Hide from editor'}
|
|
62
66
|
aria-pressed={isHidden}
|
|
63
|
-
className="text-
|
|
67
|
+
className="text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] px-1"
|
|
64
68
|
>
|
|
65
69
|
{isHidden ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
66
70
|
</button>
|
|
@@ -75,7 +79,7 @@ function numInput(
|
|
|
75
79
|
) {
|
|
76
80
|
return (
|
|
77
81
|
<label className="flex flex-col gap-0.5">
|
|
78
|
-
<span className="text-xs text-
|
|
82
|
+
<span className="text-xs text-[var(--editor-text)]/60">{label}</span>
|
|
79
83
|
<input
|
|
80
84
|
type="number"
|
|
81
85
|
value={value}
|
|
@@ -86,7 +90,7 @@ function numInput(
|
|
|
86
90
|
const parsed = parseNumber(e.target.value)
|
|
87
91
|
if (parsed !== null) onChange(parsed)
|
|
88
92
|
}}
|
|
89
|
-
className="bg-
|
|
93
|
+
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
94
|
/>
|
|
91
95
|
</label>
|
|
92
96
|
)
|
|
@@ -110,9 +114,9 @@ function PropEditor({
|
|
|
110
114
|
type="checkbox"
|
|
111
115
|
checked={Boolean(value)}
|
|
112
116
|
onChange={e => onChange(e.target.checked)}
|
|
113
|
-
className="accent-
|
|
117
|
+
className="accent-[var(--editor-accent)]"
|
|
114
118
|
/>
|
|
115
|
-
<span className="text-xs text-
|
|
119
|
+
<span className="text-xs text-[var(--editor-text)]">{name}</span>
|
|
116
120
|
</label>
|
|
117
121
|
)
|
|
118
122
|
}
|
|
@@ -120,12 +124,12 @@ function PropEditor({
|
|
|
120
124
|
if (type === 'color') {
|
|
121
125
|
return (
|
|
122
126
|
<label className="flex flex-col gap-0.5" title={description}>
|
|
123
|
-
<span className="text-xs text-
|
|
127
|
+
<span className="text-xs text-[var(--editor-text)]/60">{name}</span>
|
|
124
128
|
<input
|
|
125
129
|
type="color"
|
|
126
130
|
value={String(value ?? '#000000')}
|
|
127
131
|
onChange={e => onChange(e.target.value)}
|
|
128
|
-
className="w-full h-7 bg-
|
|
132
|
+
className="w-full h-7 bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded cursor-pointer"
|
|
129
133
|
/>
|
|
130
134
|
</label>
|
|
131
135
|
)
|
|
@@ -134,7 +138,7 @@ function PropEditor({
|
|
|
134
138
|
if (type === 'int' || type === 'float') {
|
|
135
139
|
return (
|
|
136
140
|
<label className="flex flex-col gap-0.5" title={description}>
|
|
137
|
-
<span className="text-xs text-
|
|
141
|
+
<span className="text-xs text-[var(--editor-text)]/60">{name}</span>
|
|
138
142
|
<input
|
|
139
143
|
type="number"
|
|
140
144
|
value={Number(value ?? 0)}
|
|
@@ -143,7 +147,7 @@ function PropEditor({
|
|
|
143
147
|
const parsed = parseNumber(e.target.value)
|
|
144
148
|
if (parsed !== null) onChange(parsed)
|
|
145
149
|
}}
|
|
146
|
-
className="bg-
|
|
150
|
+
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
151
|
/>
|
|
148
152
|
</label>
|
|
149
153
|
)
|
|
@@ -152,12 +156,12 @@ function PropEditor({
|
|
|
152
156
|
// string fallback
|
|
153
157
|
return (
|
|
154
158
|
<label className="flex flex-col gap-0.5" title={description}>
|
|
155
|
-
<span className="text-xs text-
|
|
159
|
+
<span className="text-xs text-[var(--editor-text)]/60">{name}</span>
|
|
156
160
|
<input
|
|
157
161
|
type="text"
|
|
158
162
|
value={String(value ?? '')}
|
|
159
163
|
onChange={e => onChange(e.target.value)}
|
|
160
|
-
className="bg-
|
|
164
|
+
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
165
|
/>
|
|
162
166
|
</label>
|
|
163
167
|
)
|
|
@@ -179,6 +183,7 @@ export default function SlidePropertyPanel({
|
|
|
179
183
|
adapter,
|
|
180
184
|
hiddenElementIds,
|
|
181
185
|
onToggleElementVisibility,
|
|
186
|
+
className,
|
|
182
187
|
}: Props) {
|
|
183
188
|
// Map of jsxPath → GlobalOverlay for overlay prop schemas
|
|
184
189
|
const [overlaySchemas, setOverlaySchemas] = useState<Map<string, GlobalOverlay>>(new Map())
|
|
@@ -206,7 +211,7 @@ export default function SlidePropertyPanel({
|
|
|
206
211
|
|
|
207
212
|
if (!slide) {
|
|
208
213
|
return (
|
|
209
|
-
<div className=
|
|
214
|
+
<div className={cn('w-80 flex-shrink-0 flex items-center justify-center text-[var(--editor-text)]/40 text-xs p-4', className)}>
|
|
210
215
|
Select a slide
|
|
211
216
|
</div>
|
|
212
217
|
)
|
|
@@ -216,18 +221,18 @@ export default function SlidePropertyPanel({
|
|
|
216
221
|
const overlaySchema = overlayEl ? overlaySchemas.get(overlayEl.overlay.template) : null
|
|
217
222
|
|
|
218
223
|
return (
|
|
219
|
-
<div className=
|
|
224
|
+
<div className={cn('w-80 flex-shrink-0 border-l border-[var(--editor-border)] flex flex-col overflow-y-auto bg-[var(--editor-bg)]', className)}>
|
|
220
225
|
{/* Slide header */}
|
|
221
|
-
<div className="px-4 py-3 border-b border-
|
|
222
|
-
<div className="text-xs font-semibold text-
|
|
226
|
+
<div className="px-4 py-3 border-b border-[var(--editor-border)]">
|
|
227
|
+
<div className="text-xs font-semibold text-[var(--editor-text)]/60 uppercase tracking-wider mb-2">Slide</div>
|
|
223
228
|
<div className="flex flex-col gap-2">
|
|
224
229
|
<label className="flex flex-col gap-0.5">
|
|
225
|
-
<span className="text-xs text-
|
|
230
|
+
<span className="text-xs text-[var(--editor-text)]/60">Background color</span>
|
|
226
231
|
<input
|
|
227
232
|
type="color"
|
|
228
233
|
value={slide.base_color || '#ffffff'}
|
|
229
234
|
onChange={e => onSlideChange({ base_color: e.target.value })}
|
|
230
|
-
className="w-full h-7 bg-
|
|
235
|
+
className="w-full h-7 bg-[var(--editor-surface)] border border-[var(--editor-border)] rounded cursor-pointer"
|
|
231
236
|
/>
|
|
232
237
|
</label>
|
|
233
238
|
<div className="flex gap-2">
|
|
@@ -255,7 +260,7 @@ export default function SlidePropertyPanel({
|
|
|
255
260
|
{element && (
|
|
256
261
|
<div className="px-4 py-3 flex flex-col gap-3">
|
|
257
262
|
<div className="flex items-center justify-between">
|
|
258
|
-
<span className="text-xs font-semibold text-
|
|
263
|
+
<span className="text-xs font-semibold text-[var(--editor-text)]/60 uppercase tracking-wider">
|
|
259
264
|
{element.type === 'image' ? 'Image' : 'Overlay'}
|
|
260
265
|
</span>
|
|
261
266
|
<div className="flex items-center gap-1">
|
|
@@ -266,14 +271,14 @@ export default function SlidePropertyPanel({
|
|
|
266
271
|
/>
|
|
267
272
|
<button
|
|
268
273
|
onClick={() => onReorderElement(slide.id, element.id, 'forward')}
|
|
269
|
-
className="text-xs text-
|
|
274
|
+
className="text-xs text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] px-1"
|
|
270
275
|
title="Bring forward"
|
|
271
276
|
>
|
|
272
277
|
↑
|
|
273
278
|
</button>
|
|
274
279
|
<button
|
|
275
280
|
onClick={() => onReorderElement(slide.id, element.id, 'backward')}
|
|
276
|
-
className="text-xs text-
|
|
281
|
+
className="text-xs text-[var(--editor-text)]/60 hover:text-[var(--editor-text)] px-1"
|
|
277
282
|
title="Send backward"
|
|
278
283
|
>
|
|
279
284
|
↓
|
|
@@ -294,8 +299,8 @@ export default function SlidePropertyPanel({
|
|
|
294
299
|
{element.type === 'image' && (
|
|
295
300
|
<div className="flex flex-col gap-1.5">
|
|
296
301
|
<div className="flex flex-col gap-0.5">
|
|
297
|
-
<span className="text-xs text-
|
|
298
|
-
<span className="text-xs text-
|
|
302
|
+
<span className="text-xs text-[var(--editor-text)]/60">Source</span>
|
|
303
|
+
<span className="text-xs text-[var(--editor-text)] truncate" title={element.src}>
|
|
299
304
|
{element.src.split('/').pop() || element.src}
|
|
300
305
|
</span>
|
|
301
306
|
</div>
|
|
@@ -335,12 +340,12 @@ export default function SlidePropertyPanel({
|
|
|
335
340
|
</div>
|
|
336
341
|
|
|
337
342
|
{schemasLoading && (
|
|
338
|
-
<div className="text-xs text-
|
|
343
|
+
<div className="text-xs text-[var(--editor-text)]/60">Loading overlay props…</div>
|
|
339
344
|
)}
|
|
340
345
|
|
|
341
346
|
{!schemasLoading && overlaySchema && overlaySchema.props.length > 0 && (
|
|
342
347
|
<div className="flex flex-col gap-2">
|
|
343
|
-
<span className="text-xs text-
|
|
348
|
+
<span className="text-xs text-[var(--editor-text)]/60 font-medium">Props</span>
|
|
344
349
|
{overlaySchema.props.map(prop => (
|
|
345
350
|
<PropEditor
|
|
346
351
|
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 lives in the below-canvas region, stacked full-width at the
|
|
101
|
+
// very bottom (no longer capped to a 320px sidebar) so the host panel spans
|
|
102
|
+
// the editor width beneath the property panel.
|
|
102
103
|
const wrapper = getByTestId('assets').parentElement
|
|
103
|
-
expect(wrapper?.className).toContain('w-
|
|
104
|
+
expect(wrapper?.className).toContain('w-full')
|
|
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-
|
|
11
|
-
ghost: 'text-
|
|
9
|
+
default: 'bg-[var(--editor-accent)] text-[var(--editor-accent-foreground)] hover:opacity-90',
|
|
10
|
+
secondary: 'bg-[var(--editor-surface)] text-[var(--editor-text)] border border-[var(--editor-border)] hover:border-[var(--editor-accent)]',
|
|
11
|
+
ghost: 'text-[var(--editor-text)] hover:bg-[var(--editor-surface)] hover:text-[var(--editor-text)]',
|
|
12
12
|
danger: 'bg-red-600 text-white hover:bg-red-700',
|
|
13
|
-
outline: '
|
|
13
|
+
outline: 'bg-[var(--editor-surface)] text-[var(--editor-text)] border border-[var(--editor-border)] hover:border-[var(--editor-accent)]',
|
|
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}
|