@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bycrux/editor",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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-gray-800 border border-gray-700 rounded-lg">
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-gray-900 border border-gray-700 rounded px-2 py-1.5 text-xs text-white placeholder-gray-500 resize-none focus:outline-none focus:border-gray-500"
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-gray-800 bg-gray-950 overflow-y-auto">
74
- <div className="px-3 py-2 border-b border-gray-800">
75
- <span className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Slides</span>
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-blue-500'
89
+ ? 'border-[var(--editor-accent)]'
90
90
  : dragOverIdx === idx
91
- ? 'border-blue-400 opacity-70'
92
- : 'border-gray-700 hover:border-gray-500'
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-gray-800">
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-gray-950">
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-gray-200 border-gray-700 bg-gray-900/80 hover:text-white hover:border-gray-500 hover:bg-gray-800'
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-blue-500/50 bg-blue-600/80 text-white hover:bg-blue-600 hover:border-blue-400 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
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-white text-lg font-semibold">Message your agent to start</p>
420
- <p className="text-gray-400 text-sm">Nothing will happen automatically. Copy this and send it to your agent.</p>
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-blue-400/50 bg-gray-900 p-5 flex flex-col gap-3 text-left shadow-lg shadow-blue-400/10">
425
- <p className="text-blue-400 text-xs font-bold uppercase tracking-widest">Send this to your agent</p>
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-gray-200 text-[12px] leading-relaxed break-all">
429
+ <span className="text-[var(--editor-text)] text-[12px] leading-relaxed break-all">
428
430
  There is a new project pending: &quot;{project.name ?? project.id}&quot;. 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-gray-300 hover:bg-white/20 hover:text-white'
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-gray-600 text-xs font-mono">project id: {project.id}</p>
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-gray-500 text-center max-w-md">
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-gray-600 text-sm">No slides yet. Add one in the left panel.</div>
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
- <div className="flex-shrink-0 flex flex-col overflow-hidden">
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-l border-b border-gray-800 bg-gray-950">
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
- <SlidePropertyPanel
505
- project={project}
506
- slide={selectedSlide}
507
- element={selectedElement}
508
- adapter={adapter}
509
- onSlideChange={handleSlideChange}
510
- onElementChange={handlePanelElementChange}
511
- onDeleteSlide={handleDeleteSlide}
512
- onDuplicateSlide={handleDuplicateSlide}
513
- onDeleteElement={handleDeleteElement}
514
- onDuplicateElement={handleDuplicateElement}
515
- onReorderElement={handleReorderElement}
516
- onEnterCrop={(_slideId, elementId) => { setSelectedElementId(elementId); setCropElementId(elementId) }}
517
- updateOverlayProp={state.updateOverlayProp}
518
- hiddenElementIds={hiddenElementIds}
519
- onToggleElementVisibility={onToggleElementVisibility}
520
- />
521
- {slots?.assetsPanel && (
522
- // Bound the assets slot to the sidebar width (matches SlidePropertyPanel's
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-gray-400'
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-gray-600">[render] </span>
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-gray-950 border border-gray-800 rounded-2xl shadow-2xl flex overflow-hidden">
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-gray-800 hover:border-gray-600 transition-colors bg-gray-900"
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-gray-300 font-mono flex justify-between">
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-gray-500">{file}</span>
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-gray-800">
150
- <div className="flex items-center justify-between px-5 py-4 border-b border-gray-800">
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-white">Render complete</p>
155
- <p className="text-xs text-gray-400">
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-gray-500 hover:text-white transition-colors text-lg leading-none">×</button>
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-gray-500 break-all leading-relaxed">{outputDir}</p>
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-gray-800 border border-gray-700 text-gray-300 hover:bg-gray-700 transition-colors"
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-gray-900 border border-gray-700 rounded-xl shadow-2xl flex flex-col overflow-hidden">
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-gray-800">
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-white">
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-gray-500 hover:text-white transition-colors text-lg leading-none">×</button>
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-gray-800 border border-gray-700 text-gray-400 hover:text-white hover:border-gray-500 transition-colors"
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-gray-300 bg-gray-950 flex flex-col gap-0.5"
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-gray-600 italic">Starting render engine…</span>
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-gray-800">
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-gray-800 border border-gray-700 text-gray-300 hover:bg-red-900/40 hover:border-red-700 hover:text-red-300 transition-colors"
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-gray-800 border border-gray-700 text-white hover:bg-gray-700 transition-colors"
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-gray-900 border border-gray-700 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-gray-800">
103
- <h2 className="text-sm font-semibold text-white">Add Overlay</h2>
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-gray-500 hover:text-white transition-colors text-lg leading-none"
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-gray-500 text-sm py-8">Loading overlays…</div>
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-gray-500 text-sm py-8">No overlays available</div>
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-gray-800 hover:bg-gray-700 border border-gray-700 hover:border-gray-500 rounded-lg transition-colors"
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-white truncate">{overlay.name}</div>
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-blue-400 mt-0.5 truncate">{overlay.group}</div>
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-gray-400 mt-1 line-clamp-2">{overlay.description}</div>
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 #3b82f6' : 'none',
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
- <OverlayErrorBoundary
446
- label={element.overlay.template.split('/').pop() ?? element.overlay.template}
447
- watchPath={element.overlay.template}
448
- watchFile={watchFile}
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
- <OverlayElementView element={element} compileOverlay={compileOverlay} />
451
- </OverlayErrorBoundary>
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: '#3b82f6',
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: '#3b82f6',
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: '#3b82f6',
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: '#93c5fd',
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-gray-500 hover:text-white px-1"
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-gray-500">{label}</span>
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-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-gray-500 w-full"
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-blue-500"
113
+ className="accent-[var(--editor-accent)]"
114
114
  />
115
- <span className="text-xs text-gray-300">{name}</span>
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-gray-500">{name}</span>
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-gray-800 border border-gray-700 rounded cursor-pointer"
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-gray-500">{name}</span>
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-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-gray-500 w-full"
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-gray-500">{name}</span>
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-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-gray-500 w-full"
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-gray-600 text-xs p-4">
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-gray-800 flex flex-col overflow-y-auto bg-gray-950">
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-gray-800">
222
- <div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Slide</div>
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-gray-500">Background color</span>
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-gray-800 border border-gray-700 rounded cursor-pointer"
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-gray-400 uppercase tracking-wider">
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-gray-500 hover:text-white px-1"
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-gray-500 hover:text-white px-1"
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-gray-500">Source</span>
298
- <span className="text-xs text-gray-300 truncate" title={element.src}>
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-gray-500">Loading overlay props…</div>
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-gray-500 font-medium">Props</span>
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
- // Regression: the assets slot must be width-bounded to the sidebar (w-80) so a
101
- // wide host panel can't blow out the right column and crush the flex-1 canvas.
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('w-80')
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
@@ -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-gray-600 bg-gray-800 px-2 py-1 text-sm text-gray-100 hover:bg-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50'
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-gray-600 bg-gray-800 shadow-xl ring-1 ring-black/20"
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-gray-100 hover:bg-gray-700 focus:bg-gray-700 focus:outline-none ${
129
- isActive ? 'bg-gray-700 font-medium' : ''
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-gray-400"
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-gray-600 bg-gray-800 px-2 py-1 text-sm text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50'
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
- // Montaj dark-palette button styles (gray-800 surface, gray-700 hover, blue-500 ring)
100
- const toolbarBtnBase = 'flex items-center justify-center rounded px-1.5 py-1 text-sm transition-colors hover:bg-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500'
101
- const toolbarBtnActive = 'bg-gray-700 text-gray-100'
102
- const toolbarBtnInactive = 'text-gray-400'
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-gray-600 bg-gray-800 text-gray-100 shadow-md"
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-gray-500"
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-gray-600" aria-hidden />
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-gray-600" aria-hidden />
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-gray-400">
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-gray-600" aria-hidden />}
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-gray-400 hover:bg-red-900/30 hover:text-red-400`}
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-text, --editor-border, --editor-selection
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`. Editor styles
16
- * reference these vars exclusively, which is what lets a host re-theme the same
17
- * component without forking its CSS.
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 mirror Montaj's existing Tailwind /
23
- * `index.css` palette, which is dark-first:
24
- * background gray-900 (#111827, matches `surface.DEFAULT`)
25
- * surface gray-800 (#1f2937, matches `surface.raised`)
26
- * border gray-700 (#374151, matches `surface.overlay`)
27
- * text gray-100 (#f3f4f6, matches `html.dark` text)
28
- * accent indigo-500 (#6366f1) — Montaj's interactive accent
29
- * selection indigo-400 (#818cf8) — element-selection highlight
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: '#111827',
35
- surface: '#1f2937',
46
+ background: '#030712',
47
+ surface: '#111827',
36
48
  accent: '#6366f1',
49
+ accentForeground: '#ffffff',
37
50
  text: '#f3f4f6',
38
- border: '#374151',
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-gray-700 dark:text-gray-300',
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-blue-500 disabled:pointer-events-none disabled:opacity-50',
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-blue-600 text-white hover:bg-blue-700',
10
- secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600',
11
- ghost: 'text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100',
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-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800',
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-blue-500 disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-500',
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-gray-400 leading-none', className)}
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-blue-500 disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100',
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-blue-500 disabled:opacity-50',
19
- checked ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-700',
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
  >
@@ -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-blue-500 disabled:opacity-50 resize-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-500',
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}