@bycrux/editor 0.4.7 → 0.5.1
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
|
@@ -197,6 +197,24 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
197
197
|
return () => window.removeEventListener('keydown', onKey)
|
|
198
198
|
}, [state])
|
|
199
199
|
|
|
200
|
+
// ── Keyboard shortcut: Delete / Backspace removes the selected element. ──
|
|
201
|
+
// Guarded against text inputs (so editing an overlay's text or a panel field
|
|
202
|
+
// never deletes the element) and against crop mode (Backspace there belongs to
|
|
203
|
+
// the crop UI). Deleting mutates project state, so the canvas + thumbnails
|
|
204
|
+
// re-render without the element immediately.
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const onKey = (e: KeyboardEvent) => {
|
|
207
|
+
if (isTypingTarget(e.target)) return
|
|
208
|
+
if (e.key !== 'Delete' && e.key !== 'Backspace') return
|
|
209
|
+
if (!selectedSlideId || !selectedElementId || cropElementId) return
|
|
210
|
+
e.preventDefault()
|
|
211
|
+
void state.removeElement(selectedSlideId, selectedElementId)
|
|
212
|
+
setSelectedElementId(null)
|
|
213
|
+
}
|
|
214
|
+
window.addEventListener('keydown', onKey)
|
|
215
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
216
|
+
}, [state, selectedSlideId, selectedElementId, cropElementId])
|
|
217
|
+
|
|
200
218
|
async function handleRender() {
|
|
201
219
|
setRendering(true)
|
|
202
220
|
try {
|
|
@@ -439,7 +439,12 @@ export default function SlideCanvas({
|
|
|
439
439
|
<img
|
|
440
440
|
src={resolveSrc(element)}
|
|
441
441
|
draggable={false}
|
|
442
|
-
|
|
442
|
+
// Uncropped images use `contain` to mirror the renderer
|
|
443
|
+
// (render/templates/slide.jsx: no-crop branch → objectFit
|
|
444
|
+
// 'contain'). Logos/escudos placed in a fixed box must keep
|
|
445
|
+
// their aspect (letterboxed) in BOTH paths; `cover` here would
|
|
446
|
+
// crop them in the preview and diverge from the final PNG.
|
|
447
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }}
|
|
443
448
|
alt=""
|
|
444
449
|
/>
|
|
445
450
|
)
|
|
@@ -290,4 +290,43 @@ describe('CarouselEditor — editor-core integration', () => {
|
|
|
290
290
|
expect(lastSelection()).toBeNull()
|
|
291
291
|
})
|
|
292
292
|
})
|
|
293
|
+
|
|
294
|
+
// Delete / Backspace removes the selected element and persists the removal.
|
|
295
|
+
it('deletes the selected element on Delete keypress', async () => {
|
|
296
|
+
const adapter = makeFakeAdapter()
|
|
297
|
+
const initial = makeProject()
|
|
298
|
+
render(<CarouselEditor project={initial} adapter={adapter} onProjectChange={vi.fn()} />)
|
|
299
|
+
|
|
300
|
+
const wrapper = await waitFor(() => findInteractiveWrapper('el-img'))
|
|
301
|
+
await act(async () => { fireEvent.click(wrapper) })
|
|
302
|
+
|
|
303
|
+
await act(async () => { fireEvent.keyDown(window, { key: 'Delete' }) })
|
|
304
|
+
|
|
305
|
+
// The removal is persisted: the latest saved project has no elements.
|
|
306
|
+
await waitFor(() => {
|
|
307
|
+
expect(adapter.saveCalls.length).toBeGreaterThan(0)
|
|
308
|
+
const saved = adapter.saveCalls[adapter.saveCalls.length - 1].project
|
|
309
|
+
expect(saved.slides![0].elements.find(e => e.id === 'el-img')).toBeUndefined()
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Guard: Delete/Backspace must not delete while typing (e.g. editing text or a
|
|
314
|
+
// panel field), or Backspace in a field would wipe the element.
|
|
315
|
+
it('does not delete the selected element while typing in an input', async () => {
|
|
316
|
+
const adapter = makeFakeAdapter()
|
|
317
|
+
const initial = makeProject()
|
|
318
|
+
render(<CarouselEditor project={initial} adapter={adapter} onProjectChange={vi.fn()} />)
|
|
319
|
+
|
|
320
|
+
const wrapper = await waitFor(() => findInteractiveWrapper('el-img'))
|
|
321
|
+
await act(async () => { fireEvent.click(wrapper) })
|
|
322
|
+
|
|
323
|
+
const input = document.createElement('input')
|
|
324
|
+
document.body.appendChild(input)
|
|
325
|
+
input.focus()
|
|
326
|
+
|
|
327
|
+
const before = adapter.saveCalls.length
|
|
328
|
+
await act(async () => { fireEvent.keyDown(input, { key: 'Backspace' }) })
|
|
329
|
+
expect(adapter.saveCalls.length).toBe(before)
|
|
330
|
+
document.body.removeChild(input)
|
|
331
|
+
})
|
|
293
332
|
})
|
|
@@ -61,7 +61,7 @@ describe('SlideCanvas crop display', () => {
|
|
|
61
61
|
expect(wrapper.style.overflow).toBe('hidden')
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
it('renders plain
|
|
64
|
+
it('renders plain contain (no crop wrapper) when element.crop is absent', async () => {
|
|
65
65
|
const el: ImageElement = {
|
|
66
66
|
id: 'el-img',
|
|
67
67
|
type: 'image',
|
|
@@ -88,7 +88,9 @@ describe('SlideCanvas crop display', () => {
|
|
|
88
88
|
if (!found) throw new Error('img not rendered')
|
|
89
89
|
return found
|
|
90
90
|
})
|
|
91
|
-
|
|
91
|
+
// Uncropped images use `contain` to mirror the renderer (no-crop branch in
|
|
92
|
+
// render/templates/slide.jsx) so logos/escudos keep aspect in both paths.
|
|
93
|
+
expect(img.style.objectFit).toBe('contain')
|
|
92
94
|
expect(img.style.width).toBe('100%')
|
|
93
95
|
expect(img.style.height).toBe('100%')
|
|
94
96
|
})
|
|
@@ -388,7 +388,9 @@ function ReviewSurface<P extends Project>({
|
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
return (
|
|
391
|
-
<div className="flex flex-1 overflow-hidden">
|
|
391
|
+
<div className="flex flex-col flex-1 overflow-hidden">
|
|
392
|
+
{/* Work area — editor body + version rail, side by side */}
|
|
393
|
+
<div className="flex flex-1 overflow-hidden min-h-0">
|
|
392
394
|
{/* Main: preview + timeline */}
|
|
393
395
|
<div className="flex flex-col flex-1 overflow-hidden">
|
|
394
396
|
<div className="flex-1 flex items-center justify-center bg-black overflow-hidden p-2">
|
|
@@ -473,8 +475,8 @@ function ReviewSurface<P extends Project>({
|
|
|
473
475
|
</div>
|
|
474
476
|
</div>
|
|
475
477
|
|
|
476
|
-
{/* Right
|
|
477
|
-
{(adapter.listVersionHistory || slots?.
|
|
478
|
+
{/* Right rail — version history + run history slot */}
|
|
479
|
+
{(adapter.listVersionHistory || slots?.runHistory) && (
|
|
478
480
|
<div className="w-48 shrink-0 border-l border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-950 flex flex-col overflow-hidden">
|
|
479
481
|
{adapter.listVersionHistory && (
|
|
480
482
|
<VersionPanel versions={versions} restoring={restoring} onRestore={handleRestoreVersion} />
|
|
@@ -483,7 +485,16 @@ function ReviewSurface<P extends Project>({
|
|
|
483
485
|
RunSnapshot / project.history are host-only types — the package never
|
|
484
486
|
reads them. When absent nothing is rendered. */}
|
|
485
487
|
{slots?.runHistory}
|
|
486
|
-
|
|
488
|
+
</div>
|
|
489
|
+
)}
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
{/* Project media / assets — full-width region stacked BELOW the editor,
|
|
493
|
+
mirroring CarouselEditor's layout (was previously crammed into the
|
|
494
|
+
narrow right rail). The host's panel manages its own scroll. */}
|
|
495
|
+
{slots?.assetsPanel && (
|
|
496
|
+
<div className="shrink-0 border-t border-gray-200 dark:border-gray-800 w-full flex flex-col max-h-[45%] overflow-hidden">
|
|
497
|
+
{slots.assetsPanel}
|
|
487
498
|
</div>
|
|
488
499
|
)}
|
|
489
500
|
|