@pramen/cms-editor 0.0.27 → 0.0.29
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/dist/app.css +1 -1
- package/dist/index.html +1 -1
- package/dist/main.y75r3gyx.js +251 -0
- package/package.json +1 -1
- package/src/components.tsx +66 -4
- package/dist/main.negeh67g.js +0 -251
package/package.json
CHANGED
package/src/components.tsx
CHANGED
|
@@ -184,8 +184,21 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
184
184
|
|
|
185
185
|
const addBlock = async (region: string, slug: string) => {
|
|
186
186
|
try {
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
// addBlock echoes the created block + placement, so append it locally instead of a
|
|
188
|
+
// full getContentType+getPage reload — one round trip instead of three.
|
|
189
|
+
const { block, placement } = await api.call<{
|
|
190
|
+
block: { id: string; title?: string | null; fields?: Record<string, unknown> | null };
|
|
191
|
+
placement: { id: string; isShared?: boolean | number };
|
|
192
|
+
}>("addBlock", { pageId: page.id, blockTypeSlug: slug, region, fields: {} });
|
|
193
|
+
const rb: RenderedBlock = {
|
|
194
|
+
id: String(placement.id),
|
|
195
|
+
block_id: String(block.id),
|
|
196
|
+
block_type: slug,
|
|
197
|
+
title: block.title ?? null,
|
|
198
|
+
fields: block.fields ?? {},
|
|
199
|
+
is_shared: Boolean(placement.isShared),
|
|
200
|
+
};
|
|
201
|
+
setAssembled((prev) => (prev ? { ...prev, regions: { ...prev.regions, [region]: [...(prev.regions[region] ?? []), rb] } } : prev));
|
|
189
202
|
flash("block added");
|
|
190
203
|
} catch (e) {
|
|
191
204
|
setErr(errMsg(e));
|
|
@@ -219,6 +232,22 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
219
232
|
});
|
|
220
233
|
}, []);
|
|
221
234
|
|
|
235
|
+
// Drag-to-reorder within a region. `from`/`over` are indices in the region's block list;
|
|
236
|
+
// hovering updates `over` live (for the drop-target highlight), and the drop commits the
|
|
237
|
+
// new order via reorderRegion. Dragging is confined to the region it started in.
|
|
238
|
+
const [drag, setDrag] = useState<{ region: string; from: number; over: number } | null>(null);
|
|
239
|
+
const beginDrag = (region: string, i: number) => setDrag({ region, from: i, over: i });
|
|
240
|
+
const hoverDrag = (region: string, i: number) => setDrag((d) => (d && d.region === region && d.over !== i ? { ...d, over: i } : d));
|
|
241
|
+
const cancelDrag = () => setDrag(null);
|
|
242
|
+
const dropDrag = (region: string) => {
|
|
243
|
+
setDrag(null);
|
|
244
|
+
if (!drag || drag.region !== region || drag.from === drag.over) return;
|
|
245
|
+
const ids = (assembled?.regions[region] ?? []).map((b) => b.id);
|
|
246
|
+
const [moved] = ids.splice(drag.from, 1);
|
|
247
|
+
ids.splice(drag.over, 0, moved);
|
|
248
|
+
reorder(region, ids);
|
|
249
|
+
};
|
|
250
|
+
|
|
222
251
|
return (
|
|
223
252
|
<div className="grid min-h-[calc(100vh-68px)] grid-cols-[260px_1fr_400px] gap-5 px-7 pb-7 pt-2 max-[820px]:grid-cols-1">
|
|
224
253
|
<div className="overflow-auto rounded-panel bg-surface-muted p-[18px]">
|
|
@@ -259,6 +288,12 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
259
288
|
onRemove={() => removeBlock(b)}
|
|
260
289
|
onPatch={patchBlockFields}
|
|
261
290
|
onError={setErr}
|
|
291
|
+
dragging={drag?.region === r.name && drag.from === i}
|
|
292
|
+
isOver={!!drag && drag.region === r.name && drag.over === i && drag.from !== i}
|
|
293
|
+
onDragStartBlock={() => beginDrag(r.name, i)}
|
|
294
|
+
onDragOverBlock={() => hoverDrag(r.name, i)}
|
|
295
|
+
onDropBlock={() => dropDrag(r.name)}
|
|
296
|
+
onDragEndBlock={cancelDrag}
|
|
262
297
|
/>
|
|
263
298
|
))}
|
|
264
299
|
<AddBlock allowed={allowed} btBySlug={btBySlug} onAdd={(slug) => addBlock(r.name, slug)} />
|
|
@@ -288,7 +323,7 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
288
323
|
// as the WYSIWYG, media as a thumbnail picker), and edits autosave — debounced while typing,
|
|
289
324
|
// flushed on blur and on unmount — so there's no separate "save block" step and no full page
|
|
290
325
|
// reload that would drop the caret. Collapse folds it to a one-line plain-text preview.
|
|
291
|
-
function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, onPatch, onError }: {
|
|
326
|
+
function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, onPatch, onError, dragging, isOver, onDragStartBlock, onDragOverBlock, onDropBlock, onDragEndBlock }: {
|
|
292
327
|
api: Api;
|
|
293
328
|
block: RenderedBlock;
|
|
294
329
|
blockType: BlockType | undefined;
|
|
@@ -298,12 +333,19 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
298
333
|
onRemove: () => void;
|
|
299
334
|
onPatch: (placementId: string, fields: Record<string, unknown>) => void;
|
|
300
335
|
onError: (s: string) => void;
|
|
336
|
+
dragging: boolean;
|
|
337
|
+
isOver: boolean;
|
|
338
|
+
onDragStartBlock: () => void;
|
|
339
|
+
onDragOverBlock: () => void;
|
|
340
|
+
onDropBlock: () => void;
|
|
341
|
+
onDragEndBlock: () => void;
|
|
301
342
|
}) {
|
|
302
343
|
const [fields, setFields] = useState<Record<string, unknown> | null>(null);
|
|
303
344
|
const [collapsed, setCollapsed] = useState(false);
|
|
304
345
|
const [saveState, setSaveState] = useState<"idle" | "saving" | "saved">("idle");
|
|
305
346
|
const pending = useRef<Record<string, unknown> | null>(null);
|
|
306
347
|
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
348
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
307
349
|
const blockId = block.block_id;
|
|
308
350
|
const placementId = block.id;
|
|
309
351
|
|
|
@@ -345,8 +387,28 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
345
387
|
const name = blockType?.name ?? block.block_type;
|
|
346
388
|
|
|
347
389
|
return (
|
|
348
|
-
|
|
390
|
+
// The card is the drop target; the ⠿ handle is the only draggable element, so
|
|
391
|
+
// dragging never fights the inline contentEditable text selection. The handle's drag
|
|
392
|
+
// image is set to the whole card so you drag a preview of the block, not just the grip.
|
|
393
|
+
<div
|
|
394
|
+
ref={cardRef}
|
|
395
|
+
className={`mb-2.5 rounded-panel border bg-surface-card transition-colors ${isOver ? "border-brand-green" : "border-border"} ${dragging ? "opacity-40" : ""}`}
|
|
396
|
+
onDragOver={(e) => { e.preventDefault(); onDragOverBlock(); }}
|
|
397
|
+
onDrop={(e) => { e.preventDefault(); onDropBlock(); }}
|
|
398
|
+
>
|
|
349
399
|
<div className="flex items-center gap-2.5 px-3.5 py-2.5">
|
|
400
|
+
<span
|
|
401
|
+
draggable
|
|
402
|
+
onDragStart={(e) => {
|
|
403
|
+
if (cardRef.current) e.dataTransfer.setDragImage(cardRef.current, 12, 12);
|
|
404
|
+
e.dataTransfer.effectAllowed = "move";
|
|
405
|
+
e.dataTransfer.setData("text/plain", "");
|
|
406
|
+
onDragStartBlock();
|
|
407
|
+
}}
|
|
408
|
+
onDragEnd={onDragEndBlock}
|
|
409
|
+
className="shrink-0 cursor-grab select-none px-0.5 text-fg-subtle hover:text-fg active:cursor-grabbing"
|
|
410
|
+
title="Drag to reorder"
|
|
411
|
+
>⠿</span>
|
|
350
412
|
<button type="button" className="w-4 shrink-0 text-fg-subtle hover:text-fg" title={collapsed ? "Expand" : "Collapse"} onClick={() => setCollapsed((c) => !c)}>{collapsed ? "▸" : "▾"}</button>
|
|
351
413
|
<span className="font-medium text-fg">{name}</span>
|
|
352
414
|
{block.is_shared ? <span className="text-[11px] text-accent-strong">shared</span> : null}
|