mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 02:21:10 +02:00
feat(website): reorder blocks via up/down arrows in inspector
blocksStore.reorderBlock already existed but was never wired into the editor, so sections could only be created or deleted — not moved. Adds two arrows in the BlockInspector header next to the delete button. - blocksStore: moveBlockUp / moveBlockDown helpers that look up the block's siblings (same page + parent), compute the fractional index that swaps with the neighbour, and delegate to reorderBlock - BlockInspector: up / down buttons with disabled state at the bounds, plus siblings prop driving canMoveUp / canMoveDown - EditorView: derives selectedSiblings (same-parent blocks on the current page) and passes them down Works for top-level sections and for children inside containers, since siblings are scoped to (pageId, parentBlockId). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
25314200b2
commit
bdd4e05446
3 changed files with 123 additions and 8 deletions
|
|
@ -8,10 +8,15 @@
|
|||
|
||||
interface Props {
|
||||
block: WebsiteBlock;
|
||||
siblings: WebsiteBlock[];
|
||||
onDeleted?: () => void;
|
||||
}
|
||||
|
||||
let { block, onDeleted }: Props = $props();
|
||||
let { block, siblings, onDeleted }: Props = $props();
|
||||
|
||||
const siblingIndex = $derived(siblings.findIndex((b) => b.id === block.id));
|
||||
const canMoveUp = $derived(siblingIndex > 0);
|
||||
const canMoveDown = $derived(siblingIndex >= 0 && siblingIndex < siblings.length - 1);
|
||||
|
||||
const spec = $derived(getBlockSpec(block.type));
|
||||
|
||||
|
|
@ -57,6 +62,16 @@
|
|||
onDeleted?.();
|
||||
}
|
||||
|
||||
async function onMoveUp() {
|
||||
if (!canMoveUp) return;
|
||||
await blocksStore.moveBlockUp(block.id);
|
||||
}
|
||||
|
||||
async function onMoveDown() {
|
||||
if (!canMoveDown) return;
|
||||
await blocksStore.moveBlockDown(block.id);
|
||||
}
|
||||
|
||||
function asRegistryBlock(b: WebsiteBlock): Block<unknown> {
|
||||
return {
|
||||
id: b.id,
|
||||
|
|
@ -77,7 +92,34 @@
|
|||
<p class="wb-inspector__kind">{spec.category}</p>
|
||||
<h3>{spec.label}</h3>
|
||||
</div>
|
||||
<button class="wb-inspector__delete" onclick={onDelete} title="Block löschen"> × </button>
|
||||
<div class="wb-inspector__actions">
|
||||
<button
|
||||
class="wb-inspector__action"
|
||||
onclick={onMoveUp}
|
||||
disabled={!canMoveUp}
|
||||
title="Nach oben verschieben"
|
||||
aria-label="Nach oben verschieben"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
class="wb-inspector__action"
|
||||
onclick={onMoveDown}
|
||||
disabled={!canMoveDown}
|
||||
title="Nach unten verschieben"
|
||||
aria-label="Nach unten verschieben"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
class="wb-inspector__action wb-inspector__action--delete"
|
||||
onclick={onDelete}
|
||||
title="Block löschen"
|
||||
aria-label="Block löschen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wb-inspector__body">
|
||||
|
|
@ -121,22 +163,43 @@
|
|||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.wb-inspector__delete {
|
||||
.wb-inspector__actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.wb-inspector__action {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: inherit;
|
||||
padding: 0.1rem 0.5rem;
|
||||
font-size: 1.15rem;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.wb-inspector__delete:hover {
|
||||
.wb-inspector__action:hover:not(:disabled) {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
opacity: 1;
|
||||
}
|
||||
.wb-inspector__action:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.wb-inspector__action--delete {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
.wb-inspector__action--delete:hover:not(:disabled) {
|
||||
background: rgba(248, 113, 113, 0.15);
|
||||
border-color: rgba(248, 113, 113, 0.5);
|
||||
color: rgb(248, 113, 113);
|
||||
opacity: 1;
|
||||
}
|
||||
.wb-inspector__body {
|
||||
flex: 1 1 auto;
|
||||
|
|
|
|||
|
|
@ -160,4 +160,46 @@ export const blocksStore = {
|
|||
});
|
||||
await touchSiteForPage(existing.pageId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Swap the block with its previous sibling (same page + parent). No-op
|
||||
* if the block is already first or not found.
|
||||
*/
|
||||
async moveBlockUp(id: string) {
|
||||
const siblings = await orderedSiblings(id);
|
||||
if (!siblings) return;
|
||||
const { ordered, idx } = siblings;
|
||||
if (idx <= 0) return;
|
||||
const prev = ordered[idx - 1];
|
||||
const prevPrev = idx >= 2 ? ordered[idx - 2] : null;
|
||||
await this.reorderBlock(id, prevPrev?.order ?? null, prev.order);
|
||||
},
|
||||
|
||||
/**
|
||||
* Swap the block with its next sibling (same page + parent). No-op if
|
||||
* the block is already last or not found.
|
||||
*/
|
||||
async moveBlockDown(id: string) {
|
||||
const siblings = await orderedSiblings(id);
|
||||
if (!siblings) return;
|
||||
const { ordered, idx } = siblings;
|
||||
if (idx < 0 || idx >= ordered.length - 1) return;
|
||||
const next = ordered[idx + 1];
|
||||
const nextNext = idx + 2 < ordered.length ? ordered[idx + 2] : null;
|
||||
await this.reorderBlock(id, next.order, nextNext?.order ?? null);
|
||||
},
|
||||
};
|
||||
|
||||
async function orderedSiblings(
|
||||
id: string
|
||||
): Promise<{ ordered: LocalWebsiteBlock[]; idx: number } | null> {
|
||||
const block = await websiteBlocksTable.get(id);
|
||||
if (!block) return null;
|
||||
const parentId = block.parentBlockId ?? null;
|
||||
const all = await websiteBlocksTable.where('pageId').equals(block.pageId).toArray();
|
||||
const ordered = all
|
||||
.filter((b) => !b.deletedAt && (b.parentBlockId ?? null) === parentId)
|
||||
.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
|
||||
const idx = ordered.findIndex((b) => b.id === id);
|
||||
return { ordered, idx };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@
|
|||
selectedBlockId ? (pageBlocks.find((b) => b.id === selectedBlockId) ?? null) : null
|
||||
);
|
||||
|
||||
const selectedSiblings = $derived.by(() => {
|
||||
if (!selectedBlock) return [];
|
||||
const parentId = selectedBlock.parentBlockId ?? null;
|
||||
return pageBlocks.filter((b) => (b.parentBlockId ?? null) === parentId);
|
||||
});
|
||||
|
||||
// Clear selection when switching page.
|
||||
$effect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
|
|
@ -102,7 +108,11 @@
|
|||
|
||||
<aside class="wb-editor__right">
|
||||
{#if selectedBlock}
|
||||
<BlockInspector block={selectedBlock} onDeleted={() => (selectedBlockId = null)} />
|
||||
<BlockInspector
|
||||
block={selectedBlock}
|
||||
siblings={selectedSiblings}
|
||||
onDeleted={() => (selectedBlockId = null)}
|
||||
/>
|
||||
{:else}
|
||||
<p class="wb-editor__inspector-empty">
|
||||
Wähle einen Block in der Vorschau, um ihn zu bearbeiten.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue