@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bycrux/editor",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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) => (
@@ -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-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>
@@ -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 = 48
361
- const HINT_RESERVE = 36
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-hidden bg-gray-950">
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-6">
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-gray-200 border-gray-700 bg-gray-900/80 hover:text-white hover:border-gray-500 hover:bg-gray-800'
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-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"
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-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>
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-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>
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-gray-200 text-[12px] leading-relaxed break-all">
431
+ <span className="text-[var(--editor-text)] text-[12px] leading-relaxed break-all">
428
432
  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
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-gray-300 hover:bg-white/20 hover:text-white'
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-gray-600 text-xs font-mono">project id: {project.id}</p>
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-gray-500 text-center max-w-md">
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-gray-600 text-sm">No slides yet. Add one in the left panel.</div>
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
- <div className="flex-shrink-0 flex flex-col overflow-hidden">
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-l border-b border-gray-800 bg-gray-950">
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
- // 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 }}>
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-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
  }}
@@ -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-gray-500 hover:text-white px-1"
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-gray-500">{label}</span>
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-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-gray-500 w-full"
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-blue-500"
117
+ className="accent-[var(--editor-accent)]"
114
118
  />
115
- <span className="text-xs text-gray-300">{name}</span>
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-gray-500">{name}</span>
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-gray-800 border border-gray-700 rounded cursor-pointer"
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-gray-500">{name}</span>
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-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-gray-500 w-full"
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-gray-500">{name}</span>
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-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-gray-500 w-full"
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="w-80 flex-shrink-0 flex items-center justify-center text-gray-600 text-xs p-4">
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="w-80 flex-shrink-0 border-l border-gray-800 flex flex-col overflow-y-auto bg-gray-950">
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-gray-800">
222
- <div className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Slide</div>
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-gray-500">Background color</span>
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-gray-800 border border-gray-700 rounded cursor-pointer"
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-gray-400 uppercase tracking-wider">
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-gray-500 hover:text-white px-1"
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-gray-500 hover:text-white px-1"
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-gray-500">Source</span>
298
- <span className="text-xs text-gray-300 truncate" title={element.src}>
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-gray-500">Loading overlay props…</div>
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-gray-500 font-medium">Props</span>
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
- // 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 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-80')
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
@@ -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-[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: '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: '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-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}