@tightknitai/block-kitchen 0.8.8 → 0.8.9

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, pointerWithin, closestCenter, DndContext, DragOverlay, useDroppable, useDraggable } from '@dnd-kit/core';
2
2
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
3
- import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, Plus, ChevronDown, BookOpen, ExternalLink, AlertTriangle, Trash2, Code2, MoreHorizontal, Send, Search, CheckCircle2, Check, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Pencil, Copy, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Undo2, Redo2, Link as Link$1, Smile } from 'lucide-react';
3
+ import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, Plus, ChevronDown, BookOpen, ExternalLink, AlertTriangle, Trash2, Code2, MoreHorizontal, Send, Search, CheckCircle2, Check, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Pencil, Copy, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Undo2, Redo2, Smile, Link as Link$1 } from 'lucide-react';
4
4
  import * as React from 'react';
5
5
  import { createContext, useMemo, useState, useCallback, useRef, useEffect, useId, useContext } from 'react';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -23,6 +23,34 @@ import 'slack-blocks-to-jsx/dist/style.css';
23
23
  import { Message } from 'slack-blocks-to-jsx';
24
24
 
25
25
  // src/components/block-kitchen.tsx
26
+
27
+ // src/lib/container-blocks.ts
28
+ var CONTAINER_CHILD_TYPES = /* @__PURE__ */ new Set([
29
+ "actions",
30
+ "context",
31
+ "divider",
32
+ "header",
33
+ "image",
34
+ "input",
35
+ "rich_text",
36
+ "section",
37
+ "table",
38
+ "video"
39
+ ]);
40
+ var MAX_CONTAINER_CHILDREN = 10;
41
+ function isContainerChildType(type) {
42
+ return CONTAINER_CHILD_TYPES.has(type);
43
+ }
44
+ var BODY_PREFIX = "container-body:";
45
+ function containerBodyId(containerId) {
46
+ return `${BODY_PREFIX}${containerId}`;
47
+ }
48
+ function parseContainerBodyId(id) {
49
+ if (typeof id !== "string" || !id.startsWith(BODY_PREFIX)) {
50
+ return null;
51
+ }
52
+ return id.slice(BODY_PREFIX.length);
53
+ }
26
54
  function makeEmojiHook(byName) {
27
55
  return (data, parse) => {
28
56
  const entry = byName.get(data.name);
@@ -1982,9 +2010,52 @@ function useBlockKitValidation(blocks, surface) {
1982
2010
  }, [blocks, surface]);
1983
2011
  return state;
1984
2012
  }
2013
+ function mirror(node) {
2014
+ if (!node.children) {
2015
+ return node;
2016
+ }
2017
+ return {
2018
+ ...node,
2019
+ block: { ...node.block, child_blocks: node.children.map((c) => c.block) }
2020
+ };
2021
+ }
1985
2022
  function wrap(block) {
2023
+ if (block.type === "container") {
2024
+ const children = (block.child_blocks ?? []).map(wrap);
2025
+ return mirror({ id: nanoid(8), block, children });
2026
+ }
1986
2027
  return { id: nanoid(8), block };
1987
2028
  }
2029
+ function cloneNode(node) {
2030
+ if (node.children) {
2031
+ return mirror({ id: nanoid(8), block: structuredClone(node.block), children: node.children.map(cloneNode) });
2032
+ }
2033
+ return { id: nanoid(8), block: structuredClone(node.block) };
2034
+ }
2035
+ function applyUpdate(node, block) {
2036
+ if (node.children) {
2037
+ return mirror({ ...node, block });
2038
+ }
2039
+ return { ...node, block };
2040
+ }
2041
+ function locate(blocks, id) {
2042
+ const top = blocks.findIndex((b) => b.id === id);
2043
+ if (top !== -1) {
2044
+ return { kind: "top", index: top };
2045
+ }
2046
+ for (const block of blocks) {
2047
+ if (block.children) {
2048
+ const idx = block.children.findIndex((c) => c.id === id);
2049
+ if (idx !== -1) {
2050
+ return { kind: "child", parentId: block.id, index: idx };
2051
+ }
2052
+ }
2053
+ }
2054
+ return null;
2055
+ }
2056
+ function withChildren(blocks, parentId, fn) {
2057
+ return blocks.map((b) => b.id === parentId && b.children ? mirror({ ...b, children: fn(b.children) }) : b);
2058
+ }
1988
2059
  function useBlockKitchenState({
1989
2060
  initialBlocks,
1990
2061
  onChange
@@ -2005,41 +2076,113 @@ function useBlockKitchenState({
2005
2076
  const addBlock = useCallback((block, atIndex) => {
2006
2077
  setBlocks((prev) => {
2007
2078
  const next = [...prev];
2008
- const inserted = wrap(block);
2009
2079
  const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2010
- next.splice(idx, 0, inserted);
2080
+ next.splice(idx, 0, wrap(block));
2011
2081
  return next;
2012
2082
  });
2013
2083
  }, []);
2084
+ const addChild = useCallback((parentId, block, atIndex) => {
2085
+ if (!isContainerChildType(block.type)) {
2086
+ return;
2087
+ }
2088
+ setBlocks(
2089
+ (prev) => withChildren(prev, parentId, (children) => {
2090
+ if (children.length >= MAX_CONTAINER_CHILDREN) {
2091
+ return children;
2092
+ }
2093
+ const next = [...children];
2094
+ const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2095
+ next.splice(idx, 0, wrap(block));
2096
+ return next;
2097
+ })
2098
+ );
2099
+ }, []);
2014
2100
  const updateBlock = useCallback((id, block) => {
2015
- setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, block } : b));
2101
+ setBlocks(
2102
+ (prev) => prev.map((b) => {
2103
+ if (b.id === id) {
2104
+ return applyUpdate(b, block);
2105
+ }
2106
+ if (b.children?.some((c) => c.id === id)) {
2107
+ return mirror({ ...b, children: b.children.map((c) => c.id === id ? applyUpdate(c, block) : c) });
2108
+ }
2109
+ return b;
2110
+ })
2111
+ );
2016
2112
  }, []);
2017
2113
  const removeBlock = useCallback((id) => {
2018
- setBlocks((prev) => prev.filter((b) => b.id !== id));
2114
+ setBlocks((prev) => {
2115
+ if (prev.some((b) => b.id === id)) {
2116
+ return prev.filter((b) => b.id !== id);
2117
+ }
2118
+ return prev.map(
2119
+ (b) => b.children?.some((c) => c.id === id) ? mirror({ ...b, children: b.children.filter((c) => c.id !== id) }) : b
2120
+ );
2121
+ });
2019
2122
  }, []);
2020
2123
  const duplicateBlock = useCallback((id) => {
2021
2124
  setBlocks((prev) => {
2022
- const idx = prev.findIndex((b) => b.id === id);
2023
- if (idx === -1) {
2125
+ const loc = locate(prev, id);
2126
+ if (!loc) {
2024
2127
  return prev;
2025
2128
  }
2026
- const source = prev[idx];
2027
- const next = [...prev];
2028
- next.splice(idx + 1, 0, wrap(structuredClone(source.block)));
2029
- return next;
2129
+ if (loc.kind === "top") {
2130
+ const next = [...prev];
2131
+ next.splice(loc.index + 1, 0, cloneNode(prev[loc.index]));
2132
+ return next;
2133
+ }
2134
+ return withChildren(prev, loc.parentId, (children) => {
2135
+ const next = [...children];
2136
+ next.splice(loc.index + 1, 0, cloneNode(children[loc.index]));
2137
+ return next;
2138
+ });
2030
2139
  });
2031
2140
  }, []);
2032
2141
  const reorderBlock = useCallback((fromId, toIndex) => {
2033
2142
  setBlocks((prev) => {
2034
- const fromIndex = prev.findIndex((b) => b.id === fromId);
2035
- if (fromIndex === -1) {
2143
+ const loc = locate(prev, fromId);
2144
+ if (!loc) {
2036
2145
  return prev;
2037
2146
  }
2038
- const next = [...prev];
2039
- const [moved] = next.splice(fromIndex, 1);
2040
- const clamped = Math.max(0, Math.min(toIndex, next.length));
2041
- next.splice(clamped, 0, moved);
2042
- return next;
2147
+ const reorder = (list) => {
2148
+ const next = [...list];
2149
+ const [moved] = next.splice(loc.index, 1);
2150
+ next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, moved);
2151
+ return next;
2152
+ };
2153
+ if (loc.kind === "top") {
2154
+ return reorder(prev);
2155
+ }
2156
+ return withChildren(prev, loc.parentId, reorder);
2157
+ });
2158
+ }, []);
2159
+ const moveBlock = useCallback((activeId, target) => {
2160
+ setBlocks((prev) => {
2161
+ const loc = locate(prev, activeId);
2162
+ if (!loc) {
2163
+ return prev;
2164
+ }
2165
+ const moved = loc.kind === "top" ? prev[loc.index] : prev.find((b) => b.id === loc.parentId)?.children?.[loc.index];
2166
+ if (!moved) {
2167
+ return prev;
2168
+ }
2169
+ if (target.kind === "container") {
2170
+ if (moved.children || !isContainerChildType(moved.block.type)) {
2171
+ return prev;
2172
+ }
2173
+ const sameContainer = loc.kind === "child" && loc.parentId === target.parentId;
2174
+ const destCount = prev.find((b) => b.id === target.parentId)?.children?.length ?? 0;
2175
+ if (!sameContainer && destCount >= MAX_CONTAINER_CHILDREN) {
2176
+ return prev;
2177
+ }
2178
+ }
2179
+ const detached = loc.kind === "top" ? prev.filter((_, i) => i !== loc.index) : withChildren(prev, loc.parentId, (children) => children.filter((_, i) => i !== loc.index));
2180
+ const insert = (list) => {
2181
+ const next = [...list];
2182
+ next.splice(Math.max(0, Math.min(target.index, next.length)), 0, moved);
2183
+ return next;
2184
+ };
2185
+ return target.kind === "container" ? withChildren(detached, target.parentId, insert) : insert(detached);
2043
2186
  });
2044
2187
  }, []);
2045
2188
  const replaceAll = useCallback((newBlocks) => {
@@ -2048,10 +2191,12 @@ function useBlockKitchenState({
2048
2191
  return {
2049
2192
  blocks,
2050
2193
  addBlock,
2194
+ addChild,
2051
2195
  updateBlock,
2052
2196
  removeBlock,
2053
2197
  duplicateBlock,
2054
2198
  reorderBlock,
2199
+ moveBlock,
2055
2200
  replaceAll
2056
2201
  };
2057
2202
  }
@@ -3560,22 +3705,10 @@ function CarouselEditor({ block, onChange }) {
3560
3705
  ] })
3561
3706
  ] });
3562
3707
  }
3563
- var MAX_CHILDREN = 10;
3564
3708
  var WIDTHS = ["narrow", "standard", "wide", "full"];
3565
3709
  function ContainerEditor({ block, onChange }) {
3566
- const children = block.child_blocks ?? [];
3567
3710
  const iconUrl = block.icon && "image_url" in block.icon ? block.icon.image_url ?? "" : "";
3568
3711
  const iconAlt = block.icon?.alt_text ?? "";
3569
- const removeChildAt = (idx) => {
3570
- onChange({ ...block, child_blocks: children.filter((_, i) => i !== idx) });
3571
- };
3572
- const addChild = () => {
3573
- const child = {
3574
- type: "section",
3575
- text: { type: "mrkdwn", text: "New block." }
3576
- };
3577
- onChange({ ...block, child_blocks: [...children, child] });
3578
- };
3579
3712
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
3580
3713
  /* @__PURE__ */ jsx(EditorField, { label: "Title", help: "Heading shown at the top of the container.", htmlFor: "container-title", children: /* @__PURE__ */ jsx(
3581
3714
  Input,
@@ -3650,42 +3783,7 @@ function ContainerEditor({ block, onChange }) {
3650
3783
  onChange: (checked) => onChange({ ...block, default_collapsed: checked || void 0 })
3651
3784
  }
3652
3785
  ),
3653
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
3654
- /* @__PURE__ */ jsx(Label, { children: "Child blocks" }),
3655
- /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "A container holds 1-10 blocks. To edit a child's content, use the View JSON drawer." }),
3656
- children.map((child, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 rounded-md border bg-muted/20 px-3 py-2", children: [
3657
- /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-foreground", children: [
3658
- idx + 1,
3659
- ". ",
3660
- labelForBlockType(child.type)
3661
- ] }),
3662
- /* @__PURE__ */ jsx(
3663
- "button",
3664
- {
3665
- type: "button",
3666
- "aria-label": "Remove child block",
3667
- onClick: () => removeChildAt(idx),
3668
- disabled: children.length <= 1,
3669
- className: "rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:cursor-not-allowed disabled:opacity-40",
3670
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
3671
- }
3672
- )
3673
- ] }, idx)),
3674
- /* @__PURE__ */ jsxs(
3675
- Button,
3676
- {
3677
- type: "button",
3678
- size: "sm",
3679
- onClick: addChild,
3680
- disabled: children.length >= MAX_CHILDREN,
3681
- className: "self-start",
3682
- children: [
3683
- /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
3684
- " Add block"
3685
- ]
3686
- }
3687
- )
3688
- ] })
3786
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "Drag blocks into the container on the canvas to add them (1-10), and drag them out to remove. Click a child to edit it." })
3689
3787
  ] });
3690
3788
  }
3691
3789
  function Checkbox({
@@ -8256,6 +8354,225 @@ function RichTextInlineEditor({
8256
8354
  }
8257
8355
  );
8258
8356
  }
8357
+ function ContainerRow({
8358
+ builderBlock,
8359
+ previewHooks,
8360
+ previewTheme,
8361
+ errorsByBlockId,
8362
+ openBlockId,
8363
+ onOpenBlockChange,
8364
+ onUpdate,
8365
+ onDuplicate,
8366
+ onDelete,
8367
+ onReorder,
8368
+ index,
8369
+ total,
8370
+ isPaletteDrag = false
8371
+ }) {
8372
+ const { id } = builderBlock;
8373
+ const block = builderBlock.block;
8374
+ const children = builderBlock.children ?? [];
8375
+ const errors = errorsByBlockId?.get(id);
8376
+ const hasErrors = !!errors && errors.length > 0;
8377
+ const isOpen = openBlockId === id;
8378
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
8379
+ const { role: _dndRole, tabIndex: _dndTabIndex, ...sortableA11yAttrs } = attributes;
8380
+ const { setNodeRef: setBodyRef, isOver: isBodyOver } = useDroppable({ id: containerBodyId(id) });
8381
+ const style = { transform: CSS.Transform.toString(transform), transition };
8382
+ const icon = block.icon && "image_url" in block.icon ? block.icon.image_url : void 0;
8383
+ return /* @__PURE__ */ jsxs(
8384
+ "div",
8385
+ {
8386
+ ref: setNodeRef,
8387
+ style,
8388
+ className: cn("group/container relative mb-2 last:mb-0 hover:z-10", isDragging && "opacity-40"),
8389
+ children: [
8390
+ /* @__PURE__ */ jsxs(
8391
+ "div",
8392
+ {
8393
+ className: cn(
8394
+ "overflow-hidden rounded-md border bg-card transition-shadow",
8395
+ hasErrors ? "border-destructive/60" : "border-border"
8396
+ ),
8397
+ children: [
8398
+ /* @__PURE__ */ jsxs(Popover, { open: isOpen, onOpenChange: (open) => onOpenBlockChange?.(open ? id : null), children: [
8399
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
8400
+ "div",
8401
+ {
8402
+ role: "button",
8403
+ tabIndex: 0,
8404
+ "aria-label": "Edit container",
8405
+ ...sortableA11yAttrs,
8406
+ ...listeners,
8407
+ onKeyDown: (e) => {
8408
+ if (e.key === "Enter" || e.key === " ") {
8409
+ e.preventDefault();
8410
+ onOpenBlockChange?.(!isOpen ? id : null);
8411
+ }
8412
+ },
8413
+ className: "flex w-full cursor-grab items-center gap-2 border-b bg-muted/40 px-3 py-2 text-left transition-colors hover:bg-muted active:cursor-grabbing",
8414
+ children: [
8415
+ icon ? /* @__PURE__ */ jsx("img", { src: icon, alt: "", "aria-hidden": "true", className: "h-5 w-5 shrink-0 rounded object-cover" }) : null,
8416
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
8417
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs font-semibold text-foreground", children: block.title?.text || "Container" }),
8418
+ block.subtitle?.text ? /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: block.subtitle.text }) : null
8419
+ ] }),
8420
+ /* @__PURE__ */ jsx("span", { className: "ml-auto shrink-0 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: "Container" })
8421
+ ]
8422
+ }
8423
+ ) }),
8424
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsx(BlockEditor, { block, errors, onChange: (next) => onUpdate(id, next) }) })
8425
+ ] }),
8426
+ /* @__PURE__ */ jsx(
8427
+ "div",
8428
+ {
8429
+ ref: setBodyRef,
8430
+ className: cn(
8431
+ "flex flex-col gap-2 p-3 transition-colors",
8432
+ isPaletteDrag && "outline-2 -outline-offset-2 outline-dashed outline-primary/40",
8433
+ isPaletteDrag && isBodyOver && "bg-primary/5"
8434
+ ),
8435
+ children: children.length === 0 ? /* @__PURE__ */ jsx(
8436
+ "div",
8437
+ {
8438
+ className: cn(
8439
+ "flex items-center justify-center rounded-md border border-dashed px-3 py-6 text-center text-[11px] transition-colors",
8440
+ isBodyOver ? "border-primary/60 text-primary" : "border-border text-muted-foreground"
8441
+ ),
8442
+ children: "Drag blocks here"
8443
+ }
8444
+ ) : /* @__PURE__ */ jsxs(SortableContext, { items: children.map((c) => c.id), strategy: verticalListSortingStrategy, children: [
8445
+ children.map((child, idx) => /* @__PURE__ */ jsx(
8446
+ BlockRow,
8447
+ {
8448
+ builderBlock: child,
8449
+ previewHooks,
8450
+ previewTheme,
8451
+ errors: errorsByBlockId?.get(child.id),
8452
+ isOpen: openBlockId === child.id,
8453
+ onOpenChange: (open) => onOpenBlockChange?.(open ? child.id : null),
8454
+ onUpdate,
8455
+ onDuplicate,
8456
+ onDelete,
8457
+ index: idx,
8458
+ total: children.length,
8459
+ onReorder,
8460
+ isPaletteDrag
8461
+ },
8462
+ child.id
8463
+ )),
8464
+ isPaletteDrag && isBodyOver ? /* @__PURE__ */ jsx(DropIndicator, {}) : null
8465
+ ] })
8466
+ }
8467
+ )
8468
+ ]
8469
+ }
8470
+ ),
8471
+ /* @__PURE__ */ jsxs(
8472
+ "div",
8473
+ {
8474
+ className: cn(
8475
+ "absolute -top-3 right-2 z-10 hidden items-center gap-0.5 rounded-md border bg-background p-0.5 shadow-sm transition-opacity md:flex",
8476
+ hasErrors ? "opacity-100" : "opacity-0 group-hover/container:opacity-100 group-focus-within/container:opacity-100"
8477
+ ),
8478
+ children: [
8479
+ hasErrors ? /* @__PURE__ */ jsxs(Tooltip, { children: [
8480
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
8481
+ "button",
8482
+ {
8483
+ type: "button",
8484
+ "aria-label": `Show ${errors.length} validation ${errors.length === 1 ? "issue" : "issues"}`,
8485
+ onClick: () => onOpenBlockChange?.(id),
8486
+ 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",
8487
+ children: /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5" })
8488
+ }
8489
+ ) }),
8490
+ /* @__PURE__ */ jsx(TooltipContent, { side: "top", align: "end", children: /* @__PURE__ */ jsxs("ul", { className: "flex flex-col gap-0.5", children: [
8491
+ errors.slice(0, 4).map((err, i) => /* @__PURE__ */ jsx("li", { children: err }, i)),
8492
+ errors.length > 4 ? /* @__PURE__ */ jsxs("li", { className: "text-muted-foreground", children: [
8493
+ "and ",
8494
+ errors.length - 4,
8495
+ " more"
8496
+ ] }) : null
8497
+ ] }) })
8498
+ ] }) : null,
8499
+ onReorder && typeof index === "number" && typeof total === "number" ? /* @__PURE__ */ jsxs(Fragment, { children: [
8500
+ /* @__PURE__ */ jsx(ToolbarButton2, { label: "Move up", disabled: index === 0, onClick: () => onReorder(id, index - 1), children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-3.5 w-3.5" }) }),
8501
+ /* @__PURE__ */ jsx(ToolbarButton2, { label: "Move down", disabled: index >= total - 1, onClick: () => onReorder(id, index + 1), children: /* @__PURE__ */ jsx(ArrowDown, { className: "h-3.5 w-3.5" }) })
8502
+ ] }) : null,
8503
+ /* @__PURE__ */ jsx(ToolbarButton2, { label: "Edit", onClick: () => onOpenBlockChange?.(id), children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" }) }),
8504
+ /* @__PURE__ */ jsx(ToolbarButton2, { label: "Duplicate", onClick: () => onDuplicate(id), children: /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" }) }),
8505
+ /* @__PURE__ */ jsx(ToolbarButton2, { label: "Delete", destructive: true, onClick: () => onDelete(id), children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" }) })
8506
+ ]
8507
+ }
8508
+ ),
8509
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center justify-between gap-2 rounded-md border border-dashed bg-muted/40 px-2 py-1 md:hidden", children: [
8510
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] font-medium text-muted-foreground", children: "Container" }),
8511
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
8512
+ /* @__PURE__ */ jsx(
8513
+ "button",
8514
+ {
8515
+ type: "button",
8516
+ "aria-label": "Edit container",
8517
+ onClick: () => onOpenBlockChange?.(id),
8518
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
8519
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4" })
8520
+ }
8521
+ ),
8522
+ /* @__PURE__ */ jsx(
8523
+ "button",
8524
+ {
8525
+ type: "button",
8526
+ "aria-label": "Duplicate container",
8527
+ onClick: () => onDuplicate(id),
8528
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
8529
+ children: /* @__PURE__ */ jsx(Copy, { className: "h-4 w-4" })
8530
+ }
8531
+ ),
8532
+ /* @__PURE__ */ jsx(
8533
+ "button",
8534
+ {
8535
+ type: "button",
8536
+ "aria-label": "Delete container",
8537
+ onClick: () => onDelete(id),
8538
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
8539
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
8540
+ }
8541
+ )
8542
+ ] })
8543
+ ] })
8544
+ ]
8545
+ }
8546
+ );
8547
+ }
8548
+ function ToolbarButton2({
8549
+ label,
8550
+ onClick,
8551
+ disabled,
8552
+ destructive,
8553
+ children
8554
+ }) {
8555
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
8556
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
8557
+ "button",
8558
+ {
8559
+ type: "button",
8560
+ "aria-label": label,
8561
+ disabled,
8562
+ onClick,
8563
+ className: cn(
8564
+ "flex h-6 w-6 items-center justify-center rounded text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40",
8565
+ destructive ? "hover:bg-destructive/10 hover:text-destructive" : "hover:bg-accent hover:text-foreground"
8566
+ ),
8567
+ children
8568
+ }
8569
+ ) }),
8570
+ /* @__PURE__ */ jsx(TooltipContent, { side: "top", children: label })
8571
+ ] });
8572
+ }
8573
+ function DropIndicator() {
8574
+ return /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "pointer-events-none relative my-1 h-0.5 w-full rounded-full bg-primary", children: /* @__PURE__ */ jsx("span", { className: "-left-1 -top-[3px] absolute h-2 w-2 rounded-full bg-primary shadow-[0_0_0_2px_var(--color-background)]" }) });
8575
+ }
8259
8576
  var SURFACE_DROPPABLE_ID = "builder-surface";
8260
8577
  function Surface({
8261
8578
  blocks,
@@ -8287,26 +8604,46 @@ function Surface({
8287
8604
  isPaletteDrag && isOver && (isDark ? "bg-[#2c3036]" : "bg-[#eaf0ff]")
8288
8605
  ),
8289
8606
  children: blocks.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { isDark, isPaletteDrag, isOver, onOpenPalette }) : /* @__PURE__ */ jsxs(SortableContext, { items: blocks.map((b) => b.id), strategy: verticalListSortingStrategy, children: [
8290
- blocks.map((block, idx) => /* @__PURE__ */ jsx(
8291
- BlockRow,
8292
- {
8293
- builderBlock: block,
8294
- previewHooks,
8295
- previewTheme,
8296
- errors: errorsByBlockId?.get(block.id),
8297
- isOpen: openBlockId === block.id,
8298
- onOpenChange: (open) => onOpenBlockChange?.(open ? block.id : null),
8299
- onUpdate,
8300
- onDuplicate,
8301
- onDelete,
8302
- index: idx,
8303
- total: blocks.length,
8304
- onReorder,
8305
- isPaletteDrag
8306
- },
8307
- block.id
8308
- )),
8309
- showEndDropZone ? /* @__PURE__ */ jsx(DropIndicator, {}) : null
8607
+ blocks.map(
8608
+ (block, idx) => block.block.type === "container" ? /* @__PURE__ */ jsx(
8609
+ ContainerRow,
8610
+ {
8611
+ builderBlock: block,
8612
+ previewHooks,
8613
+ previewTheme,
8614
+ errorsByBlockId,
8615
+ openBlockId,
8616
+ onOpenBlockChange,
8617
+ onUpdate,
8618
+ onDuplicate,
8619
+ onDelete,
8620
+ index: idx,
8621
+ total: blocks.length,
8622
+ onReorder,
8623
+ isPaletteDrag
8624
+ },
8625
+ block.id
8626
+ ) : /* @__PURE__ */ jsx(
8627
+ BlockRow,
8628
+ {
8629
+ builderBlock: block,
8630
+ previewHooks,
8631
+ previewTheme,
8632
+ errors: errorsByBlockId?.get(block.id),
8633
+ isOpen: openBlockId === block.id,
8634
+ onOpenChange: (open) => onOpenBlockChange?.(open ? block.id : null),
8635
+ onUpdate,
8636
+ onDuplicate,
8637
+ onDelete,
8638
+ index: idx,
8639
+ total: blocks.length,
8640
+ onReorder,
8641
+ isPaletteDrag
8642
+ },
8643
+ block.id
8644
+ )
8645
+ ),
8646
+ showEndDropZone ? /* @__PURE__ */ jsx(DropIndicator2, {}) : null
8310
8647
  ] })
8311
8648
  }
8312
8649
  );
@@ -8546,7 +8883,7 @@ function EmptyState({
8546
8883
  }
8547
8884
  );
8548
8885
  }
8549
- function DropIndicator() {
8886
+ function DropIndicator2() {
8550
8887
  return /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "pointer-events-none relative my-1 h-0.5 w-full rounded-full bg-primary", children: /* @__PURE__ */ jsx("span", { className: "-left-1 -top-[3px] absolute h-2 w-2 rounded-full bg-primary shadow-[0_0_0_2px_var(--color-background)]" }) });
8551
8888
  }
8552
8889
  var THEME_OPTIONS = [
@@ -8909,9 +9246,18 @@ function BlockKitchen(props) {
8909
9246
  }, [palette, disabledBlockTypes]);
8910
9247
  const variantById = useMemo(() => buildVariantById(paletteSections), [paletteSections]);
8911
9248
  const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
8912
- const { blocks, addBlock, updateBlock, removeBlock, duplicateBlock, reorderBlock, replaceAll } = useBlockKitchenState(
8913
- { initialBlocks, onChange }
8914
- );
9249
+ const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks, onChange });
9250
+ const childParentById = useMemo(() => {
9251
+ const map = /* @__PURE__ */ new Map();
9252
+ for (const top of blocks) {
9253
+ if (top.children) {
9254
+ for (const child of top.children) {
9255
+ map.set(child.id, top.id);
9256
+ }
9257
+ }
9258
+ }
9259
+ return map;
9260
+ }, [blocks]);
8915
9261
  const [jsonOpen, setJsonOpen] = useState(false);
8916
9262
  const [sendOpen, setSendOpen] = useState(false);
8917
9263
  const [issuesOpen, setIssuesOpen] = useState(false);
@@ -8936,14 +9282,22 @@ function BlockKitchen(props) {
8936
9282
  useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
8937
9283
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
8938
9284
  );
8939
- const collisionDetection = useCallback((args) => {
8940
- const pointerHits = pointerWithin(args);
8941
- if (pointerHits.length > 0) {
8942
- const blockHit = pointerHits.find((c) => c.id !== SURFACE_DROPPABLE_ID);
8943
- return blockHit ? [blockHit] : pointerHits;
8944
- }
8945
- return closestCenter(args);
8946
- }, []);
9285
+ const collisionDetection = useCallback(
9286
+ (args) => {
9287
+ const hits = pointerWithin(args);
9288
+ if (hits.length > 0) {
9289
+ const child = hits.find((c) => childParentById.has(c.id));
9290
+ if (child) return [child];
9291
+ const body = hits.find((c) => parseContainerBodyId(c.id) !== null);
9292
+ if (body) return [body];
9293
+ const block = hits.find((c) => c.id !== SURFACE_DROPPABLE_ID && parseContainerBodyId(c.id) === null);
9294
+ if (block) return [block];
9295
+ return hits;
9296
+ }
9297
+ return closestCenter(args);
9298
+ },
9299
+ [childParentById]
9300
+ );
8947
9301
  const handleDragStart = useCallback((event) => {
8948
9302
  const variantId = parsePaletteDragId(event.active.id);
8949
9303
  setActivePaletteVariantId(variantId);
@@ -8955,24 +9309,43 @@ function BlockKitchen(props) {
8955
9309
  if (!over) {
8956
9310
  return;
8957
9311
  }
9312
+ const overId = over.id;
9313
+ let target;
9314
+ const bodyParent = parseContainerBodyId(overId);
9315
+ const childParent = childParentById.get(overId);
9316
+ if (overId === SURFACE_DROPPABLE_ID) {
9317
+ target = { kind: "top", index: blocks.length };
9318
+ } else if (bodyParent) {
9319
+ const count = blocks.find((b) => b.id === bodyParent)?.children?.length ?? 0;
9320
+ target = { kind: "container", parentId: bodyParent, index: count };
9321
+ } else if (childParent) {
9322
+ const siblings = blocks.find((b) => b.id === childParent)?.children ?? [];
9323
+ target = { kind: "container", parentId: childParent, index: siblings.findIndex((c) => c.id === overId) };
9324
+ } else {
9325
+ const topIndex = blocks.findIndex((b) => b.id === overId);
9326
+ target = topIndex === -1 ? null : { kind: "top", index: topIndex };
9327
+ }
9328
+ if (!target) {
9329
+ return;
9330
+ }
8958
9331
  const variantId = parsePaletteDragId(active.id);
8959
9332
  if (variantId) {
8960
9333
  const variant = variantById.get(variantId);
8961
9334
  if (!variant) {
8962
9335
  return;
8963
9336
  }
8964
- const targetIndex = over.id === SURFACE_DROPPABLE_ID ? blocks.length : blocks.findIndex((b) => b.id === over.id);
8965
- addBlock(variant.factory(), targetIndex === -1 ? void 0 : targetIndex);
9337
+ if (target.kind === "container") {
9338
+ addChild(target.parentId, variant.factory(), target.index);
9339
+ } else {
9340
+ addBlock(variant.factory(), target.index);
9341
+ }
8966
9342
  return;
8967
9343
  }
8968
- if (active.id !== over.id) {
8969
- const overIndex = blocks.findIndex((b) => b.id === over.id);
8970
- if (overIndex !== -1) {
8971
- reorderBlock(active.id, overIndex);
8972
- }
9344
+ if (active.id !== overId) {
9345
+ moveBlock(active.id, target);
8973
9346
  }
8974
9347
  },
8975
- [addBlock, blocks, reorderBlock, variantById]
9348
+ [addBlock, addChild, blocks, childParentById, moveBlock, variantById]
8976
9349
  );
8977
9350
  const handleDragCancel = useCallback(() => {
8978
9351
  setActivePaletteVariantId(null);