diff --git a/src/components/block-kitchen.tsx b/src/components/block-kitchen.tsx
index 7a679a2..ba4532c 100644
--- a/src/components/block-kitchen.tsx
+++ b/src/components/block-kitchen.tsx
@@ -188,6 +188,7 @@ export function BlockKitchen(props: BlockKitchenProps) {
onUpdate={updateBlock}
onDuplicate={duplicateBlock}
onDelete={removeBlock}
+ onReorder={reorderBlock}
isPaletteDrag={activePaletteVariant !== null}
/>
diff --git a/src/components/block-row.tsx b/src/components/block-row.tsx
index ce21fac..07209d0 100644
--- a/src/components/block-row.tsx
+++ b/src/components/block-row.tsx
@@ -1,6 +1,6 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
-import { AlertTriangle, Copy, Pencil, Trash2 } from 'lucide-react';
+import { AlertTriangle, ArrowDown, ArrowUp, Copy, Pencil, Trash2 } from 'lucide-react';
import { useState } from 'react';
import type { RichTextBlock } from 'slack-web-api-client';
import { cn } from '../lib/cn';
@@ -59,6 +59,9 @@ export function BlockRow({
onUpdate,
onDuplicate,
onDelete,
+ index,
+ total,
+ onReorder,
isPaletteDrag = false
}: {
builderBlock: BuilderBlock;
@@ -73,6 +76,12 @@ export function BlockRow({
onUpdate: (id: string, block: SupportedBlock) => void;
onDuplicate: (id: string) => void;
onDelete: (id: string) => void;
+ /** Zero-based index of this row in the surface; powers move up/down. */
+ index?: number;
+ /** Total number of rows on the surface; powers move up/down. */
+ total?: number;
+ /** Move this row to a new index. Wires up the keyboard-accessible move buttons. */
+ onReorder?: (id: string, toIndex: number) => void;
/** True while a palette item is being dragged (vs. reordering an existing block). */
isPaletteDrag?: boolean;
}) {
@@ -143,6 +152,15 @@ export function BlockRow({
aria-label="Edit block"
{...sortableA11yAttrs}
{...listeners}
+ onKeyDown={(e) => {
+ // The div trigger is keyboard-activatable via role="button" + tabIndex=0,
+ // but Space on a non-button element scrolls the page by default. Toggle the
+ // popover ourselves on Enter/Space and swallow the event so the page stays put.
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onOpenChange?.(!isOpen);
+ }
+ }}
className={cn(
'block w-full cursor-grab rounded-sm transition-shadow hover:shadow-md focus-visible:ring-1 focus-visible:ring-ring active:cursor-grabbing',
hasErrors ? 'ring-1 ring-destructive/60 hover:ring-destructive' : 'hover:ring-1 hover:ring-border'
@@ -166,7 +184,10 @@ export function BlockRow({
{hasErrors ? (
@@ -176,7 +197,7 @@ export function BlockRow({
type="button"
aria-label={`Show ${errors!.length} validation ${errors!.length === 1 ? 'issue' : 'issues'}`}
onClick={() => onOpenChange?.(true)}
- className="flex h-6 w-6 items-center justify-center rounded text-destructive hover:bg-destructive/10"
+ className="flex h-6 w-6 items-center justify-center rounded text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
@@ -191,6 +212,38 @@ export function BlockRow({
) : null}
+ {onReorder && typeof index === 'number' && typeof total === 'number' ? (
+ <>
+
+
+
+
+ Move up
+
+
+
+
+
+ Move down
+
+ >
+ ) : null}
@@ -216,7 +269,7 @@ export function BlockRow({
type="button"
aria-label="Duplicate block"
onClick={() => onDuplicate(builderBlock.id)}
- className="flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
+ className="flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
@@ -229,7 +282,7 @@ export function BlockRow({
type="button"
aria-label="Delete block"
onClick={() => onDelete(builderBlock.id)}
- className="flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
+ className="flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
diff --git a/src/components/json-drawer.tsx b/src/components/json-drawer.tsx
index ffd7e7c..df0d3c6 100644
--- a/src/components/json-drawer.tsx
+++ b/src/components/json-drawer.tsx
@@ -127,6 +127,7 @@ export function JsonDrawer({