@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.cjs CHANGED
@@ -53,6 +53,34 @@ var Link__default = /*#__PURE__*/_interopDefault(Link);
53
53
  var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
54
54
 
55
55
  // src/components/block-kitchen.tsx
56
+
57
+ // src/lib/container-blocks.ts
58
+ var CONTAINER_CHILD_TYPES = /* @__PURE__ */ new Set([
59
+ "actions",
60
+ "context",
61
+ "divider",
62
+ "header",
63
+ "image",
64
+ "input",
65
+ "rich_text",
66
+ "section",
67
+ "table",
68
+ "video"
69
+ ]);
70
+ var MAX_CONTAINER_CHILDREN = 10;
71
+ function isContainerChildType(type) {
72
+ return CONTAINER_CHILD_TYPES.has(type);
73
+ }
74
+ var BODY_PREFIX = "container-body:";
75
+ function containerBodyId(containerId) {
76
+ return `${BODY_PREFIX}${containerId}`;
77
+ }
78
+ function parseContainerBodyId(id) {
79
+ if (typeof id !== "string" || !id.startsWith(BODY_PREFIX)) {
80
+ return null;
81
+ }
82
+ return id.slice(BODY_PREFIX.length);
83
+ }
56
84
  function makeEmojiHook(byName) {
57
85
  return (data, parse) => {
58
86
  const entry = byName.get(data.name);
@@ -2012,9 +2040,52 @@ function useBlockKitValidation(blocks, surface) {
2012
2040
  }, [blocks, surface]);
2013
2041
  return state;
2014
2042
  }
2043
+ function mirror(node) {
2044
+ if (!node.children) {
2045
+ return node;
2046
+ }
2047
+ return {
2048
+ ...node,
2049
+ block: { ...node.block, child_blocks: node.children.map((c) => c.block) }
2050
+ };
2051
+ }
2015
2052
  function wrap(block) {
2053
+ if (block.type === "container") {
2054
+ const children = (block.child_blocks ?? []).map(wrap);
2055
+ return mirror({ id: nanoid.nanoid(8), block, children });
2056
+ }
2016
2057
  return { id: nanoid.nanoid(8), block };
2017
2058
  }
2059
+ function cloneNode(node) {
2060
+ if (node.children) {
2061
+ return mirror({ id: nanoid.nanoid(8), block: structuredClone(node.block), children: node.children.map(cloneNode) });
2062
+ }
2063
+ return { id: nanoid.nanoid(8), block: structuredClone(node.block) };
2064
+ }
2065
+ function applyUpdate(node, block) {
2066
+ if (node.children) {
2067
+ return mirror({ ...node, block });
2068
+ }
2069
+ return { ...node, block };
2070
+ }
2071
+ function locate(blocks, id) {
2072
+ const top = blocks.findIndex((b) => b.id === id);
2073
+ if (top !== -1) {
2074
+ return { kind: "top", index: top };
2075
+ }
2076
+ for (const block of blocks) {
2077
+ if (block.children) {
2078
+ const idx = block.children.findIndex((c) => c.id === id);
2079
+ if (idx !== -1) {
2080
+ return { kind: "child", parentId: block.id, index: idx };
2081
+ }
2082
+ }
2083
+ }
2084
+ return null;
2085
+ }
2086
+ function withChildren(blocks, parentId, fn) {
2087
+ return blocks.map((b) => b.id === parentId && b.children ? mirror({ ...b, children: fn(b.children) }) : b);
2088
+ }
2018
2089
  function useBlockKitchenState({
2019
2090
  initialBlocks,
2020
2091
  onChange
@@ -2035,41 +2106,113 @@ function useBlockKitchenState({
2035
2106
  const addBlock = React.useCallback((block, atIndex) => {
2036
2107
  setBlocks((prev) => {
2037
2108
  const next = [...prev];
2038
- const inserted = wrap(block);
2039
2109
  const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2040
- next.splice(idx, 0, inserted);
2110
+ next.splice(idx, 0, wrap(block));
2041
2111
  return next;
2042
2112
  });
2043
2113
  }, []);
2114
+ const addChild = React.useCallback((parentId, block, atIndex) => {
2115
+ if (!isContainerChildType(block.type)) {
2116
+ return;
2117
+ }
2118
+ setBlocks(
2119
+ (prev) => withChildren(prev, parentId, (children) => {
2120
+ if (children.length >= MAX_CONTAINER_CHILDREN) {
2121
+ return children;
2122
+ }
2123
+ const next = [...children];
2124
+ const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2125
+ next.splice(idx, 0, wrap(block));
2126
+ return next;
2127
+ })
2128
+ );
2129
+ }, []);
2044
2130
  const updateBlock = React.useCallback((id, block) => {
2045
- setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, block } : b));
2131
+ setBlocks(
2132
+ (prev) => prev.map((b) => {
2133
+ if (b.id === id) {
2134
+ return applyUpdate(b, block);
2135
+ }
2136
+ if (b.children?.some((c) => c.id === id)) {
2137
+ return mirror({ ...b, children: b.children.map((c) => c.id === id ? applyUpdate(c, block) : c) });
2138
+ }
2139
+ return b;
2140
+ })
2141
+ );
2046
2142
  }, []);
2047
2143
  const removeBlock = React.useCallback((id) => {
2048
- setBlocks((prev) => prev.filter((b) => b.id !== id));
2144
+ setBlocks((prev) => {
2145
+ if (prev.some((b) => b.id === id)) {
2146
+ return prev.filter((b) => b.id !== id);
2147
+ }
2148
+ return prev.map(
2149
+ (b) => b.children?.some((c) => c.id === id) ? mirror({ ...b, children: b.children.filter((c) => c.id !== id) }) : b
2150
+ );
2151
+ });
2049
2152
  }, []);
2050
2153
  const duplicateBlock = React.useCallback((id) => {
2051
2154
  setBlocks((prev) => {
2052
- const idx = prev.findIndex((b) => b.id === id);
2053
- if (idx === -1) {
2155
+ const loc = locate(prev, id);
2156
+ if (!loc) {
2054
2157
  return prev;
2055
2158
  }
2056
- const source = prev[idx];
2057
- const next = [...prev];
2058
- next.splice(idx + 1, 0, wrap(structuredClone(source.block)));
2059
- return next;
2159
+ if (loc.kind === "top") {
2160
+ const next = [...prev];
2161
+ next.splice(loc.index + 1, 0, cloneNode(prev[loc.index]));
2162
+ return next;
2163
+ }
2164
+ return withChildren(prev, loc.parentId, (children) => {
2165
+ const next = [...children];
2166
+ next.splice(loc.index + 1, 0, cloneNode(children[loc.index]));
2167
+ return next;
2168
+ });
2060
2169
  });
2061
2170
  }, []);
2062
2171
  const reorderBlock = React.useCallback((fromId, toIndex) => {
2063
2172
  setBlocks((prev) => {
2064
- const fromIndex = prev.findIndex((b) => b.id === fromId);
2065
- if (fromIndex === -1) {
2173
+ const loc = locate(prev, fromId);
2174
+ if (!loc) {
2066
2175
  return prev;
2067
2176
  }
2068
- const next = [...prev];
2069
- const [moved] = next.splice(fromIndex, 1);
2070
- const clamped = Math.max(0, Math.min(toIndex, next.length));
2071
- next.splice(clamped, 0, moved);
2072
- return next;
2177
+ const reorder = (list) => {
2178
+ const next = [...list];
2179
+ const [moved] = next.splice(loc.index, 1);
2180
+ next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, moved);
2181
+ return next;
2182
+ };
2183
+ if (loc.kind === "top") {
2184
+ return reorder(prev);
2185
+ }
2186
+ return withChildren(prev, loc.parentId, reorder);
2187
+ });
2188
+ }, []);
2189
+ const moveBlock = React.useCallback((activeId, target) => {
2190
+ setBlocks((prev) => {
2191
+ const loc = locate(prev, activeId);
2192
+ if (!loc) {
2193
+ return prev;
2194
+ }
2195
+ const moved = loc.kind === "top" ? prev[loc.index] : prev.find((b) => b.id === loc.parentId)?.children?.[loc.index];
2196
+ if (!moved) {
2197
+ return prev;
2198
+ }
2199
+ if (target.kind === "container") {
2200
+ if (moved.children || !isContainerChildType(moved.block.type)) {
2201
+ return prev;
2202
+ }
2203
+ const sameContainer = loc.kind === "child" && loc.parentId === target.parentId;
2204
+ const destCount = prev.find((b) => b.id === target.parentId)?.children?.length ?? 0;
2205
+ if (!sameContainer && destCount >= MAX_CONTAINER_CHILDREN) {
2206
+ return prev;
2207
+ }
2208
+ }
2209
+ const detached = loc.kind === "top" ? prev.filter((_, i) => i !== loc.index) : withChildren(prev, loc.parentId, (children) => children.filter((_, i) => i !== loc.index));
2210
+ const insert = (list) => {
2211
+ const next = [...list];
2212
+ next.splice(Math.max(0, Math.min(target.index, next.length)), 0, moved);
2213
+ return next;
2214
+ };
2215
+ return target.kind === "container" ? withChildren(detached, target.parentId, insert) : insert(detached);
2073
2216
  });
2074
2217
  }, []);
2075
2218
  const replaceAll = React.useCallback((newBlocks) => {
@@ -2078,10 +2221,12 @@ function useBlockKitchenState({
2078
2221
  return {
2079
2222
  blocks,
2080
2223
  addBlock,
2224
+ addChild,
2081
2225
  updateBlock,
2082
2226
  removeBlock,
2083
2227
  duplicateBlock,
2084
2228
  reorderBlock,
2229
+ moveBlock,
2085
2230
  replaceAll
2086
2231
  };
2087
2232
  }
@@ -3590,22 +3735,10 @@ function CarouselEditor({ block, onChange }) {
3590
3735
  ] })
3591
3736
  ] });
3592
3737
  }
3593
- var MAX_CHILDREN = 10;
3594
3738
  var WIDTHS = ["narrow", "standard", "wide", "full"];
3595
3739
  function ContainerEditor({ block, onChange }) {
3596
- const children = block.child_blocks ?? [];
3597
3740
  const iconUrl = block.icon && "image_url" in block.icon ? block.icon.image_url ?? "" : "";
3598
3741
  const iconAlt = block.icon?.alt_text ?? "";
3599
- const removeChildAt = (idx) => {
3600
- onChange({ ...block, child_blocks: children.filter((_, i) => i !== idx) });
3601
- };
3602
- const addChild = () => {
3603
- const child = {
3604
- type: "section",
3605
- text: { type: "mrkdwn", text: "New block." }
3606
- };
3607
- onChange({ ...block, child_blocks: [...children, child] });
3608
- };
3609
3742
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
3610
3743
  /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Title", help: "Heading shown at the top of the container.", htmlFor: "container-title", children: /* @__PURE__ */ jsxRuntime.jsx(
3611
3744
  Input,
@@ -3680,42 +3813,7 @@ function ContainerEditor({ block, onChange }) {
3680
3813
  onChange: (checked) => onChange({ ...block, default_collapsed: checked || void 0 })
3681
3814
  }
3682
3815
  ),
3683
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
3684
- /* @__PURE__ */ jsxRuntime.jsx(Label, { children: "Child blocks" }),
3685
- /* @__PURE__ */ jsxRuntime.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." }),
3686
- children.map((child, idx) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 rounded-md border bg-muted/20 px-3 py-2", children: [
3687
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-medium text-foreground", children: [
3688
- idx + 1,
3689
- ". ",
3690
- labelForBlockType(child.type)
3691
- ] }),
3692
- /* @__PURE__ */ jsxRuntime.jsx(
3693
- "button",
3694
- {
3695
- type: "button",
3696
- "aria-label": "Remove child block",
3697
- onClick: () => removeChildAt(idx),
3698
- disabled: children.length <= 1,
3699
- className: "rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:cursor-not-allowed disabled:opacity-40",
3700
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" })
3701
- }
3702
- )
3703
- ] }, idx)),
3704
- /* @__PURE__ */ jsxRuntime.jsxs(
3705
- Button,
3706
- {
3707
- type: "button",
3708
- size: "sm",
3709
- onClick: addChild,
3710
- disabled: children.length >= MAX_CHILDREN,
3711
- className: "self-start",
3712
- children: [
3713
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
3714
- " Add block"
3715
- ]
3716
- }
3717
- )
3718
- ] })
3816
+ /* @__PURE__ */ jsxRuntime.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." })
3719
3817
  ] });
3720
3818
  }
3721
3819
  function Checkbox({
@@ -8286,6 +8384,225 @@ function RichTextInlineEditor({
8286
8384
  }
8287
8385
  );
8288
8386
  }
8387
+ function ContainerRow({
8388
+ builderBlock,
8389
+ previewHooks,
8390
+ previewTheme,
8391
+ errorsByBlockId,
8392
+ openBlockId,
8393
+ onOpenBlockChange,
8394
+ onUpdate,
8395
+ onDuplicate,
8396
+ onDelete,
8397
+ onReorder,
8398
+ index,
8399
+ total,
8400
+ isPaletteDrag = false
8401
+ }) {
8402
+ const { id } = builderBlock;
8403
+ const block = builderBlock.block;
8404
+ const children = builderBlock.children ?? [];
8405
+ const errors = errorsByBlockId?.get(id);
8406
+ const hasErrors = !!errors && errors.length > 0;
8407
+ const isOpen = openBlockId === id;
8408
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = sortable.useSortable({ id });
8409
+ const { role: _dndRole, tabIndex: _dndTabIndex, ...sortableA11yAttrs } = attributes;
8410
+ const { setNodeRef: setBodyRef, isOver: isBodyOver } = core.useDroppable({ id: containerBodyId(id) });
8411
+ const style = { transform: utilities.CSS.Transform.toString(transform), transition };
8412
+ const icon = block.icon && "image_url" in block.icon ? block.icon.image_url : void 0;
8413
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8414
+ "div",
8415
+ {
8416
+ ref: setNodeRef,
8417
+ style,
8418
+ className: cn("group/container relative mb-2 last:mb-0 hover:z-10", isDragging && "opacity-40"),
8419
+ children: [
8420
+ /* @__PURE__ */ jsxRuntime.jsxs(
8421
+ "div",
8422
+ {
8423
+ className: cn(
8424
+ "overflow-hidden rounded-md border bg-card transition-shadow",
8425
+ hasErrors ? "border-destructive/60" : "border-border"
8426
+ ),
8427
+ children: [
8428
+ /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open: isOpen, onOpenChange: (open) => onOpenBlockChange?.(open ? id : null), children: [
8429
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
8430
+ "div",
8431
+ {
8432
+ role: "button",
8433
+ tabIndex: 0,
8434
+ "aria-label": "Edit container",
8435
+ ...sortableA11yAttrs,
8436
+ ...listeners,
8437
+ onKeyDown: (e) => {
8438
+ if (e.key === "Enter" || e.key === " ") {
8439
+ e.preventDefault();
8440
+ onOpenBlockChange?.(!isOpen ? id : null);
8441
+ }
8442
+ },
8443
+ 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",
8444
+ children: [
8445
+ icon ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: icon, alt: "", "aria-hidden": "true", className: "h-5 w-5 shrink-0 rounded object-cover" }) : null,
8446
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col", children: [
8447
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs font-semibold text-foreground", children: block.title?.text || "Container" }),
8448
+ block.subtitle?.text ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: block.subtitle.text }) : null
8449
+ ] }),
8450
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto shrink-0 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: "Container" })
8451
+ ]
8452
+ }
8453
+ ) }),
8454
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsxRuntime.jsx(BlockEditor, { block, errors, onChange: (next) => onUpdate(id, next) }) })
8455
+ ] }),
8456
+ /* @__PURE__ */ jsxRuntime.jsx(
8457
+ "div",
8458
+ {
8459
+ ref: setBodyRef,
8460
+ className: cn(
8461
+ "flex flex-col gap-2 p-3 transition-colors",
8462
+ isPaletteDrag && "outline-2 -outline-offset-2 outline-dashed outline-primary/40",
8463
+ isPaletteDrag && isBodyOver && "bg-primary/5"
8464
+ ),
8465
+ children: children.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
8466
+ "div",
8467
+ {
8468
+ className: cn(
8469
+ "flex items-center justify-center rounded-md border border-dashed px-3 py-6 text-center text-[11px] transition-colors",
8470
+ isBodyOver ? "border-primary/60 text-primary" : "border-border text-muted-foreground"
8471
+ ),
8472
+ children: "Drag blocks here"
8473
+ }
8474
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(sortable.SortableContext, { items: children.map((c) => c.id), strategy: sortable.verticalListSortingStrategy, children: [
8475
+ children.map((child, idx) => /* @__PURE__ */ jsxRuntime.jsx(
8476
+ BlockRow,
8477
+ {
8478
+ builderBlock: child,
8479
+ previewHooks,
8480
+ previewTheme,
8481
+ errors: errorsByBlockId?.get(child.id),
8482
+ isOpen: openBlockId === child.id,
8483
+ onOpenChange: (open) => onOpenBlockChange?.(open ? child.id : null),
8484
+ onUpdate,
8485
+ onDuplicate,
8486
+ onDelete,
8487
+ index: idx,
8488
+ total: children.length,
8489
+ onReorder,
8490
+ isPaletteDrag
8491
+ },
8492
+ child.id
8493
+ )),
8494
+ isPaletteDrag && isBodyOver ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator, {}) : null
8495
+ ] })
8496
+ }
8497
+ )
8498
+ ]
8499
+ }
8500
+ ),
8501
+ /* @__PURE__ */ jsxRuntime.jsxs(
8502
+ "div",
8503
+ {
8504
+ className: cn(
8505
+ "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",
8506
+ hasErrors ? "opacity-100" : "opacity-0 group-hover/container:opacity-100 group-focus-within/container:opacity-100"
8507
+ ),
8508
+ children: [
8509
+ hasErrors ? /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
8510
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
8511
+ "button",
8512
+ {
8513
+ type: "button",
8514
+ "aria-label": `Show ${errors.length} validation ${errors.length === 1 ? "issue" : "issues"}`,
8515
+ onClick: () => onOpenBlockChange?.(id),
8516
+ 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",
8517
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "h-3.5 w-3.5" })
8518
+ }
8519
+ ) }),
8520
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "top", align: "end", children: /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "flex flex-col gap-0.5", children: [
8521
+ errors.slice(0, 4).map((err, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: err }, i)),
8522
+ errors.length > 4 ? /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "text-muted-foreground", children: [
8523
+ "and ",
8524
+ errors.length - 4,
8525
+ " more"
8526
+ ] }) : null
8527
+ ] }) })
8528
+ ] }) : null,
8529
+ onReorder && typeof index === "number" && typeof total === "number" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8530
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Move up", disabled: index === 0, onClick: () => onReorder(id, index - 1), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-3.5 w-3.5" }) }),
8531
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Move down", disabled: index >= total - 1, onClick: () => onReorder(id, index + 1), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowDown, { className: "h-3.5 w-3.5" }) })
8532
+ ] }) : null,
8533
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Edit", onClick: () => onOpenBlockChange?.(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-3.5 w-3.5" }) }),
8534
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Duplicate", onClick: () => onDuplicate(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-3.5 w-3.5" }) }),
8535
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Delete", destructive: true, onClick: () => onDelete(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" }) })
8536
+ ]
8537
+ }
8538
+ ),
8539
+ /* @__PURE__ */ jsxRuntime.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: [
8540
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] font-medium text-muted-foreground", children: "Container" }),
8541
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
8542
+ /* @__PURE__ */ jsxRuntime.jsx(
8543
+ "button",
8544
+ {
8545
+ type: "button",
8546
+ "aria-label": "Edit container",
8547
+ onClick: () => onOpenBlockChange?.(id),
8548
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
8549
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-4 w-4" })
8550
+ }
8551
+ ),
8552
+ /* @__PURE__ */ jsxRuntime.jsx(
8553
+ "button",
8554
+ {
8555
+ type: "button",
8556
+ "aria-label": "Duplicate container",
8557
+ onClick: () => onDuplicate(id),
8558
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
8559
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-4 w-4" })
8560
+ }
8561
+ ),
8562
+ /* @__PURE__ */ jsxRuntime.jsx(
8563
+ "button",
8564
+ {
8565
+ type: "button",
8566
+ "aria-label": "Delete container",
8567
+ onClick: () => onDelete(id),
8568
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
8569
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4" })
8570
+ }
8571
+ )
8572
+ ] })
8573
+ ] })
8574
+ ]
8575
+ }
8576
+ );
8577
+ }
8578
+ function ToolbarButton2({
8579
+ label,
8580
+ onClick,
8581
+ disabled,
8582
+ destructive,
8583
+ children
8584
+ }) {
8585
+ return /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
8586
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
8587
+ "button",
8588
+ {
8589
+ type: "button",
8590
+ "aria-label": label,
8591
+ disabled,
8592
+ onClick,
8593
+ className: cn(
8594
+ "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",
8595
+ destructive ? "hover:bg-destructive/10 hover:text-destructive" : "hover:bg-accent hover:text-foreground"
8596
+ ),
8597
+ children
8598
+ }
8599
+ ) }),
8600
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "top", children: label })
8601
+ ] });
8602
+ }
8603
+ function DropIndicator() {
8604
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", className: "pointer-events-none relative my-1 h-0.5 w-full rounded-full bg-primary", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "-left-1 -top-[3px] absolute h-2 w-2 rounded-full bg-primary shadow-[0_0_0_2px_var(--color-background)]" }) });
8605
+ }
8289
8606
  var SURFACE_DROPPABLE_ID = "builder-surface";
8290
8607
  function Surface({
8291
8608
  blocks,
@@ -8317,26 +8634,46 @@ function Surface({
8317
8634
  isPaletteDrag && isOver && (isDark ? "bg-[#2c3036]" : "bg-[#eaf0ff]")
8318
8635
  ),
8319
8636
  children: blocks.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { isDark, isPaletteDrag, isOver, onOpenPalette }) : /* @__PURE__ */ jsxRuntime.jsxs(sortable.SortableContext, { items: blocks.map((b) => b.id), strategy: sortable.verticalListSortingStrategy, children: [
8320
- blocks.map((block, idx) => /* @__PURE__ */ jsxRuntime.jsx(
8321
- BlockRow,
8322
- {
8323
- builderBlock: block,
8324
- previewHooks,
8325
- previewTheme,
8326
- errors: errorsByBlockId?.get(block.id),
8327
- isOpen: openBlockId === block.id,
8328
- onOpenChange: (open) => onOpenBlockChange?.(open ? block.id : null),
8329
- onUpdate,
8330
- onDuplicate,
8331
- onDelete,
8332
- index: idx,
8333
- total: blocks.length,
8334
- onReorder,
8335
- isPaletteDrag
8336
- },
8337
- block.id
8338
- )),
8339
- showEndDropZone ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator, {}) : null
8637
+ blocks.map(
8638
+ (block, idx) => block.block.type === "container" ? /* @__PURE__ */ jsxRuntime.jsx(
8639
+ ContainerRow,
8640
+ {
8641
+ builderBlock: block,
8642
+ previewHooks,
8643
+ previewTheme,
8644
+ errorsByBlockId,
8645
+ openBlockId,
8646
+ onOpenBlockChange,
8647
+ onUpdate,
8648
+ onDuplicate,
8649
+ onDelete,
8650
+ index: idx,
8651
+ total: blocks.length,
8652
+ onReorder,
8653
+ isPaletteDrag
8654
+ },
8655
+ block.id
8656
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
8657
+ BlockRow,
8658
+ {
8659
+ builderBlock: block,
8660
+ previewHooks,
8661
+ previewTheme,
8662
+ errors: errorsByBlockId?.get(block.id),
8663
+ isOpen: openBlockId === block.id,
8664
+ onOpenChange: (open) => onOpenBlockChange?.(open ? block.id : null),
8665
+ onUpdate,
8666
+ onDuplicate,
8667
+ onDelete,
8668
+ index: idx,
8669
+ total: blocks.length,
8670
+ onReorder,
8671
+ isPaletteDrag
8672
+ },
8673
+ block.id
8674
+ )
8675
+ ),
8676
+ showEndDropZone ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator2, {}) : null
8340
8677
  ] })
8341
8678
  }
8342
8679
  );
@@ -8576,7 +8913,7 @@ function EmptyState({
8576
8913
  }
8577
8914
  );
8578
8915
  }
8579
- function DropIndicator() {
8916
+ function DropIndicator2() {
8580
8917
  return /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", className: "pointer-events-none relative my-1 h-0.5 w-full rounded-full bg-primary", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "-left-1 -top-[3px] absolute h-2 w-2 rounded-full bg-primary shadow-[0_0_0_2px_var(--color-background)]" }) });
8581
8918
  }
8582
8919
  var THEME_OPTIONS = [
@@ -8939,9 +9276,18 @@ function BlockKitchen(props) {
8939
9276
  }, [palette, disabledBlockTypes]);
8940
9277
  const variantById = React.useMemo(() => buildVariantById(paletteSections), [paletteSections]);
8941
9278
  const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
8942
- const { blocks, addBlock, updateBlock, removeBlock, duplicateBlock, reorderBlock, replaceAll } = useBlockKitchenState(
8943
- { initialBlocks, onChange }
8944
- );
9279
+ const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks, onChange });
9280
+ const childParentById = React.useMemo(() => {
9281
+ const map = /* @__PURE__ */ new Map();
9282
+ for (const top of blocks) {
9283
+ if (top.children) {
9284
+ for (const child of top.children) {
9285
+ map.set(child.id, top.id);
9286
+ }
9287
+ }
9288
+ }
9289
+ return map;
9290
+ }, [blocks]);
8945
9291
  const [jsonOpen, setJsonOpen] = React.useState(false);
8946
9292
  const [sendOpen, setSendOpen] = React.useState(false);
8947
9293
  const [issuesOpen, setIssuesOpen] = React.useState(false);
@@ -8966,14 +9312,22 @@ function BlockKitchen(props) {
8966
9312
  core.useSensor(core.TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
8967
9313
  core.useSensor(core.KeyboardSensor, { coordinateGetter: sortable.sortableKeyboardCoordinates })
8968
9314
  );
8969
- const collisionDetection = React.useCallback((args) => {
8970
- const pointerHits = core.pointerWithin(args);
8971
- if (pointerHits.length > 0) {
8972
- const blockHit = pointerHits.find((c) => c.id !== SURFACE_DROPPABLE_ID);
8973
- return blockHit ? [blockHit] : pointerHits;
8974
- }
8975
- return core.closestCenter(args);
8976
- }, []);
9315
+ const collisionDetection = React.useCallback(
9316
+ (args) => {
9317
+ const hits = core.pointerWithin(args);
9318
+ if (hits.length > 0) {
9319
+ const child = hits.find((c) => childParentById.has(c.id));
9320
+ if (child) return [child];
9321
+ const body = hits.find((c) => parseContainerBodyId(c.id) !== null);
9322
+ if (body) return [body];
9323
+ const block = hits.find((c) => c.id !== SURFACE_DROPPABLE_ID && parseContainerBodyId(c.id) === null);
9324
+ if (block) return [block];
9325
+ return hits;
9326
+ }
9327
+ return core.closestCenter(args);
9328
+ },
9329
+ [childParentById]
9330
+ );
8977
9331
  const handleDragStart = React.useCallback((event) => {
8978
9332
  const variantId = parsePaletteDragId(event.active.id);
8979
9333
  setActivePaletteVariantId(variantId);
@@ -8985,24 +9339,43 @@ function BlockKitchen(props) {
8985
9339
  if (!over) {
8986
9340
  return;
8987
9341
  }
9342
+ const overId = over.id;
9343
+ let target;
9344
+ const bodyParent = parseContainerBodyId(overId);
9345
+ const childParent = childParentById.get(overId);
9346
+ if (overId === SURFACE_DROPPABLE_ID) {
9347
+ target = { kind: "top", index: blocks.length };
9348
+ } else if (bodyParent) {
9349
+ const count = blocks.find((b) => b.id === bodyParent)?.children?.length ?? 0;
9350
+ target = { kind: "container", parentId: bodyParent, index: count };
9351
+ } else if (childParent) {
9352
+ const siblings = blocks.find((b) => b.id === childParent)?.children ?? [];
9353
+ target = { kind: "container", parentId: childParent, index: siblings.findIndex((c) => c.id === overId) };
9354
+ } else {
9355
+ const topIndex = blocks.findIndex((b) => b.id === overId);
9356
+ target = topIndex === -1 ? null : { kind: "top", index: topIndex };
9357
+ }
9358
+ if (!target) {
9359
+ return;
9360
+ }
8988
9361
  const variantId = parsePaletteDragId(active.id);
8989
9362
  if (variantId) {
8990
9363
  const variant = variantById.get(variantId);
8991
9364
  if (!variant) {
8992
9365
  return;
8993
9366
  }
8994
- const targetIndex = over.id === SURFACE_DROPPABLE_ID ? blocks.length : blocks.findIndex((b) => b.id === over.id);
8995
- addBlock(variant.factory(), targetIndex === -1 ? void 0 : targetIndex);
9367
+ if (target.kind === "container") {
9368
+ addChild(target.parentId, variant.factory(), target.index);
9369
+ } else {
9370
+ addBlock(variant.factory(), target.index);
9371
+ }
8996
9372
  return;
8997
9373
  }
8998
- if (active.id !== over.id) {
8999
- const overIndex = blocks.findIndex((b) => b.id === over.id);
9000
- if (overIndex !== -1) {
9001
- reorderBlock(active.id, overIndex);
9002
- }
9374
+ if (active.id !== overId) {
9375
+ moveBlock(active.id, target);
9003
9376
  }
9004
9377
  },
9005
- [addBlock, blocks, reorderBlock, variantById]
9378
+ [addBlock, addChild, blocks, childParentById, moveBlock, variantById]
9006
9379
  );
9007
9380
  const handleDragCancel = React.useCallback(() => {
9008
9381
  setActivePaletteVariantId(null);