@tightknitai/block-kitchen 0.8.8 → 0.8.10

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
  }
@@ -3280,12 +3425,16 @@ function AlertEditor({ block, onChange }) {
3280
3425
  ) })
3281
3426
  ] });
3282
3427
  }
3283
- function CardEditor({ block, onChange }) {
3428
+ function CardEditor({
3429
+ block,
3430
+ onChange,
3431
+ idPrefix = "card"
3432
+ }) {
3284
3433
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
3285
- /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Title", help: "Up to 150 characters. Supports mrkdwn formatting.", htmlFor: "card-title", children: /* @__PURE__ */ jsxRuntime.jsx(
3434
+ /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Title", help: "Up to 150 characters. Supports mrkdwn formatting.", htmlFor: `${idPrefix}-title`, children: /* @__PURE__ */ jsxRuntime.jsx(
3286
3435
  Input,
3287
3436
  {
3288
- id: "card-title",
3437
+ id: `${idPrefix}-title`,
3289
3438
  value: block.title?.text ?? "",
3290
3439
  maxLength: 150,
3291
3440
  placeholder: "e.g. New launch update",
@@ -3295,10 +3444,10 @@ function CardEditor({ block, onChange }) {
3295
3444
  })
3296
3445
  }
3297
3446
  ) }),
3298
- /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Subtitle", help: "Up to 150 characters.", htmlFor: "card-subtitle", children: /* @__PURE__ */ jsxRuntime.jsx(
3447
+ /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Subtitle", help: "Up to 150 characters.", htmlFor: `${idPrefix}-subtitle`, children: /* @__PURE__ */ jsxRuntime.jsx(
3299
3448
  Input,
3300
3449
  {
3301
- id: "card-subtitle",
3450
+ id: `${idPrefix}-subtitle`,
3302
3451
  value: block.subtitle?.text ?? "",
3303
3452
  maxLength: 150,
3304
3453
  placeholder: "e.g. Released April 28",
@@ -3308,10 +3457,10 @@ function CardEditor({ block, onChange }) {
3308
3457
  })
3309
3458
  }
3310
3459
  ) }),
3311
- /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Body", help: "Up to 200 characters. Supports mrkdwn formatting.", htmlFor: "card-body", children: /* @__PURE__ */ jsxRuntime.jsx(
3460
+ /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Body", help: "Up to 200 characters. Supports mrkdwn formatting.", htmlFor: `${idPrefix}-body`, children: /* @__PURE__ */ jsxRuntime.jsx(
3312
3461
  Textarea,
3313
3462
  {
3314
- id: "card-body",
3463
+ id: `${idPrefix}-body`,
3315
3464
  value: block.body?.text ?? "",
3316
3465
  maxLength: 200,
3317
3466
  rows: 3,
@@ -3327,7 +3476,7 @@ function CardEditor({ block, onChange }) {
3327
3476
  {
3328
3477
  label: "Hero image",
3329
3478
  help: "Wide image displayed at the top of the card.",
3330
- idPrefix: "card-hero",
3479
+ idPrefix: `${idPrefix}-hero`,
3331
3480
  image: block.hero_image,
3332
3481
  onChange: (next) => onChange({ ...block, hero_image: next })
3333
3482
  }
@@ -3337,7 +3486,7 @@ function CardEditor({ block, onChange }) {
3337
3486
  {
3338
3487
  label: "Icon",
3339
3488
  help: "Small image shown next to the title.",
3340
- idPrefix: "card-icon",
3489
+ idPrefix: `${idPrefix}-icon`,
3341
3490
  image: block.icon,
3342
3491
  onChange: (next) => onChange({ ...block, icon: next })
3343
3492
  }
@@ -3345,6 +3494,7 @@ function CardEditor({ block, onChange }) {
3345
3494
  /* @__PURE__ */ jsxRuntime.jsx(
3346
3495
  ButtonsField,
3347
3496
  {
3497
+ idPrefix,
3348
3498
  buttons: block.actions ?? [],
3349
3499
  onChange: (next) => onChange({ ...block, actions: next.length > 0 ? next : void 0 })
3350
3500
  }
@@ -3401,7 +3551,11 @@ function ImageFields({
3401
3551
  ) })
3402
3552
  ] });
3403
3553
  }
3404
- function ButtonsField({ buttons, onChange }) {
3554
+ function ButtonsField({
3555
+ buttons,
3556
+ onChange,
3557
+ idPrefix = "card"
3558
+ }) {
3405
3559
  const update = (idx, change) => {
3406
3560
  onChange(buttons.map((b, i) => i === idx ? { ...b, ...change } : b));
3407
3561
  };
@@ -3444,10 +3598,10 @@ function ButtonsField({ buttons, onChange }) {
3444
3598
  }
3445
3599
  )
3446
3600
  ] }),
3447
- /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Label", htmlFor: `card-btn-label-${idx}`, children: /* @__PURE__ */ jsxRuntime.jsx(
3601
+ /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Label", htmlFor: `${idPrefix}-btn-label-${idx}`, children: /* @__PURE__ */ jsxRuntime.jsx(
3448
3602
  Input,
3449
3603
  {
3450
- id: `card-btn-label-${idx}`,
3604
+ id: `${idPrefix}-btn-label-${idx}`,
3451
3605
  value: label,
3452
3606
  placeholder: "e.g. Open",
3453
3607
  onChange: (e) => update(idx, {
@@ -3464,11 +3618,11 @@ function ButtonsField({ buttons, onChange }) {
3464
3618
  {
3465
3619
  label: "Link URL",
3466
3620
  help: "Optional. If set, the button opens this link.",
3467
- htmlFor: `card-btn-url-${idx}`,
3621
+ htmlFor: `${idPrefix}-btn-url-${idx}`,
3468
3622
  children: /* @__PURE__ */ jsxRuntime.jsx(
3469
3623
  Input,
3470
3624
  {
3471
- id: `card-btn-url-${idx}`,
3625
+ id: `${idPrefix}-btn-url-${idx}`,
3472
3626
  type: "url",
3473
3627
  value: url,
3474
3628
  placeholder: "e.g. https://example.com",
@@ -3487,8 +3641,8 @@ function ButtonsField({ buttons, onChange }) {
3487
3641
  },
3488
3642
  className: "flex flex-row gap-3",
3489
3643
  children: ["default", "primary", "danger"].map((s) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
3490
- /* @__PURE__ */ jsxRuntime.jsx(RadioGroupItem, { value: s, id: `card-btn-style-${idx}-${s}` }),
3491
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `card-btn-style-${idx}-${s}`, className: "text-xs capitalize", children: s })
3644
+ /* @__PURE__ */ jsxRuntime.jsx(RadioGroupItem, { value: s, id: `${idPrefix}-btn-style-${idx}-${s}` }),
3645
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `${idPrefix}-btn-style-${idx}-${s}`, className: "text-xs capitalize", children: s })
3492
3646
  ] }, s))
3493
3647
  }
3494
3648
  ) })
@@ -3537,8 +3691,8 @@ function CarouselEditor({ block, onChange }) {
3537
3691
  });
3538
3692
  };
3539
3693
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
3540
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "Carousels hold 1-10 cards. For icon, hero image, or action edits, use the View JSON drawer." }),
3541
- elements.map((card, idx) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 rounded-md border bg-muted/20 p-3", children: [
3694
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "Carousels hold 1-10 cards." }),
3695
+ elements.map((card, idx) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 rounded-md border bg-muted/20 p-3", children: [
3542
3696
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
3543
3697
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-medium text-foreground", children: [
3544
3698
  "Card ",
@@ -3556,33 +3710,7 @@ function CarouselEditor({ block, onChange }) {
3556
3710
  }
3557
3711
  )
3558
3712
  ] }),
3559
- /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Title", htmlFor: `carousel-card-title-${idx}`, children: /* @__PURE__ */ jsxRuntime.jsx(
3560
- Input,
3561
- {
3562
- id: `carousel-card-title-${idx}`,
3563
- value: card.title?.text ?? "",
3564
- maxLength: 150,
3565
- placeholder: "e.g. Card title",
3566
- onChange: (e) => updateCard(idx, {
3567
- ...card,
3568
- title: e.target.value ? { type: "mrkdwn", text: e.target.value } : void 0
3569
- })
3570
- }
3571
- ) }),
3572
- /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Body", htmlFor: `carousel-card-body-${idx}`, children: /* @__PURE__ */ jsxRuntime.jsx(
3573
- Textarea,
3574
- {
3575
- id: `carousel-card-body-${idx}`,
3576
- value: card.body?.text ?? "",
3577
- maxLength: 200,
3578
- rows: 2,
3579
- placeholder: "e.g. Short card description.",
3580
- onChange: (e) => updateCard(idx, {
3581
- ...card,
3582
- body: e.target.value ? { type: "mrkdwn", text: e.target.value } : void 0
3583
- })
3584
- }
3585
- ) })
3713
+ /* @__PURE__ */ jsxRuntime.jsx(CardEditor, { block: card, idPrefix: `carousel-card-${idx}`, onChange: (next) => updateCard(idx, next) })
3586
3714
  ] }, idx)),
3587
3715
  /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: addCard, disabled: elements.length >= MAX_CARDS, className: "self-start", children: [
3588
3716
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
@@ -3590,22 +3718,10 @@ function CarouselEditor({ block, onChange }) {
3590
3718
  ] })
3591
3719
  ] });
3592
3720
  }
3593
- var MAX_CHILDREN = 10;
3594
3721
  var WIDTHS = ["narrow", "standard", "wide", "full"];
3595
3722
  function ContainerEditor({ block, onChange }) {
3596
- const children = block.child_blocks ?? [];
3597
3723
  const iconUrl = block.icon && "image_url" in block.icon ? block.icon.image_url ?? "" : "";
3598
3724
  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
3725
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
3610
3726
  /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Title", help: "Heading shown at the top of the container.", htmlFor: "container-title", children: /* @__PURE__ */ jsxRuntime.jsx(
3611
3727
  Input,
@@ -3680,42 +3796,7 @@ function ContainerEditor({ block, onChange }) {
3680
3796
  onChange: (checked) => onChange({ ...block, default_collapsed: checked || void 0 })
3681
3797
  }
3682
3798
  ),
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
- ] })
3799
+ /* @__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
3800
  ] });
3720
3801
  }
3721
3802
  function Checkbox({
@@ -8286,6 +8367,225 @@ function RichTextInlineEditor({
8286
8367
  }
8287
8368
  );
8288
8369
  }
8370
+ function ContainerRow({
8371
+ builderBlock,
8372
+ previewHooks,
8373
+ previewTheme,
8374
+ errorsByBlockId,
8375
+ openBlockId,
8376
+ onOpenBlockChange,
8377
+ onUpdate,
8378
+ onDuplicate,
8379
+ onDelete,
8380
+ onReorder,
8381
+ index,
8382
+ total,
8383
+ isPaletteDrag = false
8384
+ }) {
8385
+ const { id } = builderBlock;
8386
+ const block = builderBlock.block;
8387
+ const children = builderBlock.children ?? [];
8388
+ const errors = errorsByBlockId?.get(id);
8389
+ const hasErrors = !!errors && errors.length > 0;
8390
+ const isOpen = openBlockId === id;
8391
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = sortable.useSortable({ id });
8392
+ const { role: _dndRole, tabIndex: _dndTabIndex, ...sortableA11yAttrs } = attributes;
8393
+ const { setNodeRef: setBodyRef, isOver: isBodyOver } = core.useDroppable({ id: containerBodyId(id) });
8394
+ const style = { transform: utilities.CSS.Transform.toString(transform), transition };
8395
+ const icon = block.icon && "image_url" in block.icon ? block.icon.image_url : void 0;
8396
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8397
+ "div",
8398
+ {
8399
+ ref: setNodeRef,
8400
+ style,
8401
+ className: cn("group/container relative mb-2 last:mb-0 hover:z-10", isDragging && "opacity-40"),
8402
+ children: [
8403
+ /* @__PURE__ */ jsxRuntime.jsxs(
8404
+ "div",
8405
+ {
8406
+ className: cn(
8407
+ "overflow-hidden rounded-md border bg-card transition-shadow",
8408
+ hasErrors ? "border-destructive/60" : "border-border"
8409
+ ),
8410
+ children: [
8411
+ /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open: isOpen, onOpenChange: (open) => onOpenBlockChange?.(open ? id : null), children: [
8412
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
8413
+ "div",
8414
+ {
8415
+ role: "button",
8416
+ tabIndex: 0,
8417
+ "aria-label": "Edit container",
8418
+ ...sortableA11yAttrs,
8419
+ ...listeners,
8420
+ onKeyDown: (e) => {
8421
+ if (e.key === "Enter" || e.key === " ") {
8422
+ e.preventDefault();
8423
+ onOpenBlockChange?.(!isOpen ? id : null);
8424
+ }
8425
+ },
8426
+ 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",
8427
+ children: [
8428
+ icon ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: icon, alt: "", "aria-hidden": "true", className: "h-5 w-5 shrink-0 rounded object-cover" }) : null,
8429
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col", children: [
8430
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs font-semibold text-foreground", children: block.title?.text || "Container" }),
8431
+ block.subtitle?.text ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: block.subtitle.text }) : null
8432
+ ] }),
8433
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto shrink-0 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: "Container" })
8434
+ ]
8435
+ }
8436
+ ) }),
8437
+ /* @__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) }) })
8438
+ ] }),
8439
+ /* @__PURE__ */ jsxRuntime.jsx(
8440
+ "div",
8441
+ {
8442
+ ref: setBodyRef,
8443
+ className: cn(
8444
+ "flex flex-col gap-2 p-3 transition-colors",
8445
+ isPaletteDrag && "outline-2 -outline-offset-2 outline-dashed outline-primary/40",
8446
+ isPaletteDrag && isBodyOver && "bg-primary/5"
8447
+ ),
8448
+ children: children.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
8449
+ "div",
8450
+ {
8451
+ className: cn(
8452
+ "flex items-center justify-center rounded-md border border-dashed px-3 py-6 text-center text-[11px] transition-colors",
8453
+ isBodyOver ? "border-primary/60 text-primary" : "border-border text-muted-foreground"
8454
+ ),
8455
+ children: "Drag blocks here"
8456
+ }
8457
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(sortable.SortableContext, { items: children.map((c) => c.id), strategy: sortable.verticalListSortingStrategy, children: [
8458
+ children.map((child, idx) => /* @__PURE__ */ jsxRuntime.jsx(
8459
+ BlockRow,
8460
+ {
8461
+ builderBlock: child,
8462
+ previewHooks,
8463
+ previewTheme,
8464
+ errors: errorsByBlockId?.get(child.id),
8465
+ isOpen: openBlockId === child.id,
8466
+ onOpenChange: (open) => onOpenBlockChange?.(open ? child.id : null),
8467
+ onUpdate,
8468
+ onDuplicate,
8469
+ onDelete,
8470
+ index: idx,
8471
+ total: children.length,
8472
+ onReorder,
8473
+ isPaletteDrag
8474
+ },
8475
+ child.id
8476
+ )),
8477
+ isPaletteDrag && isBodyOver ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator, {}) : null
8478
+ ] })
8479
+ }
8480
+ )
8481
+ ]
8482
+ }
8483
+ ),
8484
+ /* @__PURE__ */ jsxRuntime.jsxs(
8485
+ "div",
8486
+ {
8487
+ className: cn(
8488
+ "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",
8489
+ hasErrors ? "opacity-100" : "opacity-0 group-hover/container:opacity-100 group-focus-within/container:opacity-100"
8490
+ ),
8491
+ children: [
8492
+ hasErrors ? /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
8493
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
8494
+ "button",
8495
+ {
8496
+ type: "button",
8497
+ "aria-label": `Show ${errors.length} validation ${errors.length === 1 ? "issue" : "issues"}`,
8498
+ onClick: () => onOpenBlockChange?.(id),
8499
+ 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",
8500
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "h-3.5 w-3.5" })
8501
+ }
8502
+ ) }),
8503
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "top", align: "end", children: /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "flex flex-col gap-0.5", children: [
8504
+ errors.slice(0, 4).map((err, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: err }, i)),
8505
+ errors.length > 4 ? /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "text-muted-foreground", children: [
8506
+ "and ",
8507
+ errors.length - 4,
8508
+ " more"
8509
+ ] }) : null
8510
+ ] }) })
8511
+ ] }) : null,
8512
+ onReorder && typeof index === "number" && typeof total === "number" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8513
+ /* @__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" }) }),
8514
+ /* @__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" }) })
8515
+ ] }) : null,
8516
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Edit", onClick: () => onOpenBlockChange?.(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-3.5 w-3.5" }) }),
8517
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Duplicate", onClick: () => onDuplicate(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-3.5 w-3.5" }) }),
8518
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Delete", destructive: true, onClick: () => onDelete(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" }) })
8519
+ ]
8520
+ }
8521
+ ),
8522
+ /* @__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: [
8523
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] font-medium text-muted-foreground", children: "Container" }),
8524
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
8525
+ /* @__PURE__ */ jsxRuntime.jsx(
8526
+ "button",
8527
+ {
8528
+ type: "button",
8529
+ "aria-label": "Edit container",
8530
+ onClick: () => onOpenBlockChange?.(id),
8531
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
8532
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-4 w-4" })
8533
+ }
8534
+ ),
8535
+ /* @__PURE__ */ jsxRuntime.jsx(
8536
+ "button",
8537
+ {
8538
+ type: "button",
8539
+ "aria-label": "Duplicate container",
8540
+ onClick: () => onDuplicate(id),
8541
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
8542
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-4 w-4" })
8543
+ }
8544
+ ),
8545
+ /* @__PURE__ */ jsxRuntime.jsx(
8546
+ "button",
8547
+ {
8548
+ type: "button",
8549
+ "aria-label": "Delete container",
8550
+ onClick: () => onDelete(id),
8551
+ className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
8552
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4" })
8553
+ }
8554
+ )
8555
+ ] })
8556
+ ] })
8557
+ ]
8558
+ }
8559
+ );
8560
+ }
8561
+ function ToolbarButton2({
8562
+ label,
8563
+ onClick,
8564
+ disabled,
8565
+ destructive,
8566
+ children
8567
+ }) {
8568
+ return /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
8569
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
8570
+ "button",
8571
+ {
8572
+ type: "button",
8573
+ "aria-label": label,
8574
+ disabled,
8575
+ onClick,
8576
+ className: cn(
8577
+ "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",
8578
+ destructive ? "hover:bg-destructive/10 hover:text-destructive" : "hover:bg-accent hover:text-foreground"
8579
+ ),
8580
+ children
8581
+ }
8582
+ ) }),
8583
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "top", children: label })
8584
+ ] });
8585
+ }
8586
+ function DropIndicator() {
8587
+ 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)]" }) });
8588
+ }
8289
8589
  var SURFACE_DROPPABLE_ID = "builder-surface";
8290
8590
  function Surface({
8291
8591
  blocks,
@@ -8317,26 +8617,46 @@ function Surface({
8317
8617
  isPaletteDrag && isOver && (isDark ? "bg-[#2c3036]" : "bg-[#eaf0ff]")
8318
8618
  ),
8319
8619
  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
8620
+ blocks.map(
8621
+ (block, idx) => block.block.type === "container" ? /* @__PURE__ */ jsxRuntime.jsx(
8622
+ ContainerRow,
8623
+ {
8624
+ builderBlock: block,
8625
+ previewHooks,
8626
+ previewTheme,
8627
+ errorsByBlockId,
8628
+ openBlockId,
8629
+ onOpenBlockChange,
8630
+ onUpdate,
8631
+ onDuplicate,
8632
+ onDelete,
8633
+ index: idx,
8634
+ total: blocks.length,
8635
+ onReorder,
8636
+ isPaletteDrag
8637
+ },
8638
+ block.id
8639
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
8640
+ BlockRow,
8641
+ {
8642
+ builderBlock: block,
8643
+ previewHooks,
8644
+ previewTheme,
8645
+ errors: errorsByBlockId?.get(block.id),
8646
+ isOpen: openBlockId === block.id,
8647
+ onOpenChange: (open) => onOpenBlockChange?.(open ? block.id : null),
8648
+ onUpdate,
8649
+ onDuplicate,
8650
+ onDelete,
8651
+ index: idx,
8652
+ total: blocks.length,
8653
+ onReorder,
8654
+ isPaletteDrag
8655
+ },
8656
+ block.id
8657
+ )
8658
+ ),
8659
+ showEndDropZone ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator2, {}) : null
8340
8660
  ] })
8341
8661
  }
8342
8662
  );
@@ -8576,7 +8896,7 @@ function EmptyState({
8576
8896
  }
8577
8897
  );
8578
8898
  }
8579
- function DropIndicator() {
8899
+ function DropIndicator2() {
8580
8900
  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
8901
  }
8582
8902
  var THEME_OPTIONS = [
@@ -8939,9 +9259,18 @@ function BlockKitchen(props) {
8939
9259
  }, [palette, disabledBlockTypes]);
8940
9260
  const variantById = React.useMemo(() => buildVariantById(paletteSections), [paletteSections]);
8941
9261
  const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
8942
- const { blocks, addBlock, updateBlock, removeBlock, duplicateBlock, reorderBlock, replaceAll } = useBlockKitchenState(
8943
- { initialBlocks, onChange }
8944
- );
9262
+ const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks, onChange });
9263
+ const childParentById = React.useMemo(() => {
9264
+ const map = /* @__PURE__ */ new Map();
9265
+ for (const top of blocks) {
9266
+ if (top.children) {
9267
+ for (const child of top.children) {
9268
+ map.set(child.id, top.id);
9269
+ }
9270
+ }
9271
+ }
9272
+ return map;
9273
+ }, [blocks]);
8945
9274
  const [jsonOpen, setJsonOpen] = React.useState(false);
8946
9275
  const [sendOpen, setSendOpen] = React.useState(false);
8947
9276
  const [issuesOpen, setIssuesOpen] = React.useState(false);
@@ -8966,14 +9295,22 @@ function BlockKitchen(props) {
8966
9295
  core.useSensor(core.TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
8967
9296
  core.useSensor(core.KeyboardSensor, { coordinateGetter: sortable.sortableKeyboardCoordinates })
8968
9297
  );
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
- }, []);
9298
+ const collisionDetection = React.useCallback(
9299
+ (args) => {
9300
+ const hits = core.pointerWithin(args);
9301
+ if (hits.length > 0) {
9302
+ const child = hits.find((c) => childParentById.has(c.id));
9303
+ if (child) return [child];
9304
+ const body = hits.find((c) => parseContainerBodyId(c.id) !== null);
9305
+ if (body) return [body];
9306
+ const block = hits.find((c) => c.id !== SURFACE_DROPPABLE_ID && parseContainerBodyId(c.id) === null);
9307
+ if (block) return [block];
9308
+ return hits;
9309
+ }
9310
+ return core.closestCenter(args);
9311
+ },
9312
+ [childParentById]
9313
+ );
8977
9314
  const handleDragStart = React.useCallback((event) => {
8978
9315
  const variantId = parsePaletteDragId(event.active.id);
8979
9316
  setActivePaletteVariantId(variantId);
@@ -8985,24 +9322,43 @@ function BlockKitchen(props) {
8985
9322
  if (!over) {
8986
9323
  return;
8987
9324
  }
9325
+ const overId = over.id;
9326
+ let target;
9327
+ const bodyParent = parseContainerBodyId(overId);
9328
+ const childParent = childParentById.get(overId);
9329
+ if (overId === SURFACE_DROPPABLE_ID) {
9330
+ target = { kind: "top", index: blocks.length };
9331
+ } else if (bodyParent) {
9332
+ const count = blocks.find((b) => b.id === bodyParent)?.children?.length ?? 0;
9333
+ target = { kind: "container", parentId: bodyParent, index: count };
9334
+ } else if (childParent) {
9335
+ const siblings = blocks.find((b) => b.id === childParent)?.children ?? [];
9336
+ target = { kind: "container", parentId: childParent, index: siblings.findIndex((c) => c.id === overId) };
9337
+ } else {
9338
+ const topIndex = blocks.findIndex((b) => b.id === overId);
9339
+ target = topIndex === -1 ? null : { kind: "top", index: topIndex };
9340
+ }
9341
+ if (!target) {
9342
+ return;
9343
+ }
8988
9344
  const variantId = parsePaletteDragId(active.id);
8989
9345
  if (variantId) {
8990
9346
  const variant = variantById.get(variantId);
8991
9347
  if (!variant) {
8992
9348
  return;
8993
9349
  }
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);
9350
+ if (target.kind === "container") {
9351
+ addChild(target.parentId, variant.factory(), target.index);
9352
+ } else {
9353
+ addBlock(variant.factory(), target.index);
9354
+ }
8996
9355
  return;
8997
9356
  }
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
- }
9357
+ if (active.id !== overId) {
9358
+ moveBlock(active.id, target);
9003
9359
  }
9004
9360
  },
9005
- [addBlock, blocks, reorderBlock, variantById]
9361
+ [addBlock, addChild, blocks, childParentById, moveBlock, variantById]
9006
9362
  );
9007
9363
  const handleDragCancel = React.useCallback(() => {
9008
9364
  setActivePaletteVariantId(null);