diff --git a/apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte b/apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte index f822d34fd..c3fd52a55 100644 --- a/apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte +++ b/apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte @@ -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 { return { id: b.id, @@ -77,7 +92,34 @@

{spec.category}

{spec.label}

- +
+ + + +
@@ -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; diff --git a/apps/mana/apps/web/src/lib/modules/website/stores/blocks.svelte.ts b/apps/mana/apps/web/src/lib/modules/website/stores/blocks.svelte.ts index 4902607ae..d6333f2a4 100644 --- a/apps/mana/apps/web/src/lib/modules/website/stores/blocks.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/website/stores/blocks.svelte.ts @@ -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 }; +} diff --git a/apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte b/apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte index 7381fa3ca..b16282155 100644 --- a/apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte +++ b/apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte @@ -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 @@