@tightknitai/block-kitchen 0.10.0 → 0.10.2

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
@@ -2066,6 +2066,69 @@ function useBlockKitValidation(blocks, surface) {
2066
2066
  }, [blocks, surface]);
2067
2067
  return state;
2068
2068
  }
2069
+ var MAX_HISTORY = 20;
2070
+ function useHistoryState(initial) {
2071
+ const [record, setRecord] = React.useState(() => ({
2072
+ past: [],
2073
+ present: typeof initial === "function" ? initial() : initial,
2074
+ future: [],
2075
+ lastTag: null
2076
+ }));
2077
+ const commit = React.useCallback((updater, options) => {
2078
+ const tag = options?.tag ?? null;
2079
+ setRecord((h) => {
2080
+ const next = typeof updater === "function" ? updater(h.present) : updater;
2081
+ if (Object.is(next, h.present)) {
2082
+ return h;
2083
+ }
2084
+ if (tag !== null && h.lastTag === tag) {
2085
+ return { ...h, present: next, future: [] };
2086
+ }
2087
+ const past = [...h.past, h.present];
2088
+ return {
2089
+ past: past.length > MAX_HISTORY ? past.slice(past.length - MAX_HISTORY) : past,
2090
+ present: next,
2091
+ future: [],
2092
+ lastTag: tag
2093
+ };
2094
+ });
2095
+ }, []);
2096
+ const undo = React.useCallback(() => {
2097
+ setRecord((h) => {
2098
+ if (h.past.length === 0) {
2099
+ return h;
2100
+ }
2101
+ const previous = h.past[h.past.length - 1];
2102
+ return { past: h.past.slice(0, -1), present: previous, future: [h.present, ...h.future], lastTag: null };
2103
+ });
2104
+ }, []);
2105
+ const redo = React.useCallback(() => {
2106
+ setRecord((h) => {
2107
+ if (h.future.length === 0) {
2108
+ return h;
2109
+ }
2110
+ const [next, ...rest] = h.future;
2111
+ return { past: [...h.past, h.present], present: next, future: rest, lastTag: null };
2112
+ });
2113
+ }, []);
2114
+ const reset = React.useCallback((next) => {
2115
+ setRecord({ past: [], present: next, future: [], lastTag: null });
2116
+ }, []);
2117
+ return React.useMemo(
2118
+ () => ({
2119
+ present: record.present,
2120
+ canUndo: record.past.length > 0,
2121
+ canRedo: record.future.length > 0,
2122
+ commit,
2123
+ undo,
2124
+ redo,
2125
+ reset
2126
+ }),
2127
+ [record, commit, undo, redo, reset]
2128
+ );
2129
+ }
2130
+
2131
+ // src/state/use-block-kitchen-state.ts
2069
2132
  function mirror(node) {
2070
2133
  if (!node.children) {
2071
2134
  return node;
@@ -2116,7 +2179,15 @@ function useBlockKitchenState({
2116
2179
  initialBlocks,
2117
2180
  onChange
2118
2181
  } = {}) {
2119
- const [blocks, setBlocks] = React.useState(() => (initialBlocks ?? []).map(wrap));
2182
+ const {
2183
+ present: blocks,
2184
+ commit,
2185
+ undo,
2186
+ redo,
2187
+ reset,
2188
+ canUndo,
2189
+ canRedo
2190
+ } = useHistoryState(() => (initialBlocks ?? []).map(wrap));
2120
2191
  const onChangeRef = React.useRef(onChange);
2121
2192
  React.useEffect(() => {
2122
2193
  onChangeRef.current = onChange;
@@ -2129,121 +2200,155 @@ function useBlockKitchenState({
2129
2200
  }
2130
2201
  onChangeRef.current?.(blocks.map((b) => b.block));
2131
2202
  }, [blocks]);
2132
- const addBlock = React.useCallback((block, atIndex) => {
2133
- setBlocks((prev) => {
2134
- const next = [...prev];
2135
- const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2136
- next.splice(idx, 0, wrap(block));
2137
- return next;
2138
- });
2139
- }, []);
2140
- const addChild = React.useCallback((parentId, block, atIndex) => {
2141
- if (!isContainerChildType(block.type)) {
2142
- return;
2143
- }
2144
- setBlocks(
2145
- (prev) => withChildren(prev, parentId, (children) => {
2146
- if (children.length >= MAX_CONTAINER_CHILDREN) {
2147
- return children;
2148
- }
2149
- const next = [...children];
2203
+ const addBlock = React.useCallback(
2204
+ (block, atIndex) => {
2205
+ commit((prev) => {
2206
+ const next = [...prev];
2150
2207
  const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2151
2208
  next.splice(idx, 0, wrap(block));
2152
2209
  return next;
2153
- })
2154
- );
2155
- }, []);
2156
- const updateBlock = React.useCallback((id, block) => {
2157
- setBlocks(
2158
- (prev) => prev.map((b) => {
2159
- if (b.id === id) {
2160
- return applyUpdate(b, block);
2161
- }
2162
- if (b.children?.some((c) => c.id === id)) {
2163
- return mirror({ ...b, children: b.children.map((c) => c.id === id ? applyUpdate(c, block) : c) });
2164
- }
2165
- return b;
2166
- })
2167
- );
2168
- }, []);
2169
- const removeBlock = React.useCallback((id) => {
2170
- setBlocks((prev) => {
2171
- if (prev.some((b) => b.id === id)) {
2172
- return prev.filter((b) => b.id !== id);
2210
+ });
2211
+ },
2212
+ [commit]
2213
+ );
2214
+ const addChild = React.useCallback(
2215
+ (parentId, block, atIndex) => {
2216
+ if (!isContainerChildType(block.type)) {
2217
+ return;
2173
2218
  }
2174
- return prev.map(
2175
- (b) => b.children?.some((c) => c.id === id) ? mirror({ ...b, children: b.children.filter((c) => c.id !== id) }) : b
2219
+ commit(
2220
+ (prev) => withChildren(prev, parentId, (children) => {
2221
+ if (children.length >= MAX_CONTAINER_CHILDREN) {
2222
+ return children;
2223
+ }
2224
+ const next = [...children];
2225
+ const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2226
+ next.splice(idx, 0, wrap(block));
2227
+ return next;
2228
+ })
2176
2229
  );
2177
- });
2178
- }, []);
2179
- const duplicateBlock = React.useCallback((id) => {
2180
- setBlocks((prev) => {
2181
- const loc = locate(prev, id);
2182
- if (!loc) {
2183
- return prev;
2184
- }
2185
- if (loc.kind === "top") {
2186
- const next = [...prev];
2187
- next.splice(loc.index + 1, 0, cloneNode(prev[loc.index]));
2188
- return next;
2189
- }
2190
- return withChildren(prev, loc.parentId, (children) => {
2191
- const next = [...children];
2192
- next.splice(loc.index + 1, 0, cloneNode(children[loc.index]));
2193
- return next;
2230
+ },
2231
+ [commit]
2232
+ );
2233
+ const updateBlock = React.useCallback(
2234
+ (id, block) => {
2235
+ commit(
2236
+ (prev) => prev.map((b) => {
2237
+ if (b.id === id) {
2238
+ return applyUpdate(b, block);
2239
+ }
2240
+ if (b.children?.some((c) => c.id === id)) {
2241
+ return mirror({ ...b, children: b.children.map((c) => c.id === id ? applyUpdate(c, block) : c) });
2242
+ }
2243
+ return b;
2244
+ }),
2245
+ // Coalesce a run of edits to the same block (every keystroke in an
2246
+ // editor field lands here) into a single undo step. Editing a
2247
+ // different block, or any structural change, starts a new step.
2248
+ { tag: `update:${id}` }
2249
+ );
2250
+ },
2251
+ [commit]
2252
+ );
2253
+ const removeBlock = React.useCallback(
2254
+ (id) => {
2255
+ commit((prev) => {
2256
+ if (prev.some((b) => b.id === id)) {
2257
+ return prev.filter((b) => b.id !== id);
2258
+ }
2259
+ return prev.map(
2260
+ (b) => b.children?.some((c) => c.id === id) ? mirror({ ...b, children: b.children.filter((c) => c.id !== id) }) : b
2261
+ );
2194
2262
  });
2195
- });
2196
- }, []);
2197
- const reorderBlock = React.useCallback((fromId, toIndex) => {
2198
- setBlocks((prev) => {
2199
- const loc = locate(prev, fromId);
2200
- if (!loc) {
2201
- return prev;
2202
- }
2203
- const reorder = (list) => {
2204
- const next = [...list];
2205
- const [moved] = next.splice(loc.index, 1);
2206
- next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, moved);
2207
- return next;
2208
- };
2209
- if (loc.kind === "top") {
2210
- return reorder(prev);
2211
- }
2212
- return withChildren(prev, loc.parentId, reorder);
2213
- });
2214
- }, []);
2215
- const moveBlock = React.useCallback((activeId, target) => {
2216
- setBlocks((prev) => {
2217
- const loc = locate(prev, activeId);
2218
- if (!loc) {
2219
- return prev;
2220
- }
2221
- const moved = loc.kind === "top" ? prev[loc.index] : prev.find((b) => b.id === loc.parentId)?.children?.[loc.index];
2222
- if (!moved) {
2223
- return prev;
2224
- }
2225
- if (target.kind === "container") {
2226
- if (moved.children || !isContainerChildType(moved.block.type)) {
2263
+ },
2264
+ [commit]
2265
+ );
2266
+ const duplicateBlock = React.useCallback(
2267
+ (id) => {
2268
+ commit((prev) => {
2269
+ const loc = locate(prev, id);
2270
+ if (!loc) {
2227
2271
  return prev;
2228
2272
  }
2229
- const sameContainer = loc.kind === "child" && loc.parentId === target.parentId;
2230
- const destCount = prev.find((b) => b.id === target.parentId)?.children?.length ?? 0;
2231
- if (!sameContainer && destCount >= MAX_CONTAINER_CHILDREN) {
2273
+ if (loc.kind === "top") {
2274
+ const next = [...prev];
2275
+ next.splice(loc.index + 1, 0, cloneNode(prev[loc.index]));
2276
+ return next;
2277
+ }
2278
+ return withChildren(prev, loc.parentId, (children) => {
2279
+ const next = [...children];
2280
+ next.splice(loc.index + 1, 0, cloneNode(children[loc.index]));
2281
+ return next;
2282
+ });
2283
+ });
2284
+ },
2285
+ [commit]
2286
+ );
2287
+ const reorderBlock = React.useCallback(
2288
+ (fromId, toIndex) => {
2289
+ commit((prev) => {
2290
+ const loc = locate(prev, fromId);
2291
+ if (!loc) {
2232
2292
  return prev;
2233
2293
  }
2234
- }
2235
- const detached = loc.kind === "top" ? prev.filter((_, i) => i !== loc.index) : withChildren(prev, loc.parentId, (children) => children.filter((_, i) => i !== loc.index));
2236
- const insert = (list) => {
2237
- const next = [...list];
2238
- next.splice(Math.max(0, Math.min(target.index, next.length)), 0, moved);
2239
- return next;
2240
- };
2241
- return target.kind === "container" ? withChildren(detached, target.parentId, insert) : insert(detached);
2242
- });
2243
- }, []);
2244
- const replaceAll = React.useCallback((newBlocks) => {
2245
- setBlocks(sanitizeBlocks(newBlocks).map(wrap));
2246
- }, []);
2294
+ const reorder = (list) => {
2295
+ const next = [...list];
2296
+ const [moved] = next.splice(loc.index, 1);
2297
+ next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, moved);
2298
+ return next;
2299
+ };
2300
+ if (loc.kind === "top") {
2301
+ return reorder(prev);
2302
+ }
2303
+ return withChildren(prev, loc.parentId, reorder);
2304
+ });
2305
+ },
2306
+ [commit]
2307
+ );
2308
+ const moveBlock = React.useCallback(
2309
+ (activeId, target) => {
2310
+ commit((prev) => {
2311
+ const loc = locate(prev, activeId);
2312
+ if (!loc) {
2313
+ return prev;
2314
+ }
2315
+ const moved = loc.kind === "top" ? prev[loc.index] : prev.find((b) => b.id === loc.parentId)?.children?.[loc.index];
2316
+ if (!moved) {
2317
+ return prev;
2318
+ }
2319
+ if (target.kind === "container") {
2320
+ if (moved.children || !isContainerChildType(moved.block.type)) {
2321
+ return prev;
2322
+ }
2323
+ const sameContainer = loc.kind === "child" && loc.parentId === target.parentId;
2324
+ const destCount = prev.find((b) => b.id === target.parentId)?.children?.length ?? 0;
2325
+ if (!sameContainer && destCount >= MAX_CONTAINER_CHILDREN) {
2326
+ return prev;
2327
+ }
2328
+ }
2329
+ const detached = loc.kind === "top" ? prev.filter((_, i) => i !== loc.index) : withChildren(prev, loc.parentId, (children) => children.filter((_, i) => i !== loc.index));
2330
+ const insert = (list) => {
2331
+ const next = [...list];
2332
+ next.splice(Math.max(0, Math.min(target.index, next.length)), 0, moved);
2333
+ return next;
2334
+ };
2335
+ return target.kind === "container" ? withChildren(detached, target.parentId, insert) : insert(detached);
2336
+ });
2337
+ },
2338
+ [commit]
2339
+ );
2340
+ const replaceAll = React.useCallback(
2341
+ (newBlocks) => {
2342
+ commit(sanitizeBlocks(newBlocks).map(wrap));
2343
+ },
2344
+ [commit]
2345
+ );
2346
+ const resetAll = React.useCallback(
2347
+ (newBlocks) => {
2348
+ reset(sanitizeBlocks(newBlocks).map(wrap));
2349
+ },
2350
+ [reset]
2351
+ );
2247
2352
  return {
2248
2353
  blocks,
2249
2354
  addBlock,
@@ -2253,7 +2358,12 @@ function useBlockKitchenState({
2253
2358
  duplicateBlock,
2254
2359
  reorderBlock,
2255
2360
  moveBlock,
2256
- replaceAll
2361
+ replaceAll,
2362
+ resetAll,
2363
+ undo,
2364
+ redo,
2365
+ canUndo,
2366
+ canRedo
2257
2367
  };
2258
2368
  }
2259
2369
  function useDevWarning(condition, message) {
@@ -3801,6 +3911,27 @@ function EditorField({
3801
3911
  help && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: help })
3802
3912
  ] });
3803
3913
  }
3914
+ function AdvancedIdField({
3915
+ label,
3916
+ help,
3917
+ htmlFor,
3918
+ value,
3919
+ placeholder,
3920
+ onChange
3921
+ }) {
3922
+ return /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "rounded-md border bg-muted/20 px-2.5 py-1.5", children: [
3923
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { className: "cursor-pointer text-[11px] font-medium text-muted-foreground hover:text-foreground", children: "Advanced" }),
3924
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-2 pb-1", children: /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label, help, htmlFor, children: /* @__PURE__ */ jsxRuntime.jsx(
3925
+ Input,
3926
+ {
3927
+ id: htmlFor,
3928
+ value: value ?? "",
3929
+ placeholder,
3930
+ onChange: (e) => onChange(e.target.value || void 0)
3931
+ }
3932
+ ) }) })
3933
+ ] });
3934
+ }
3804
3935
  function ActionsEditor({ block, onChange }) {
3805
3936
  const elements = block.elements ?? [];
3806
3937
  const updateButton = (idx, update) => {
@@ -3899,7 +4030,18 @@ function ActionsEditor({ block, onChange }) {
3899
4030
  /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `btn-style-${idx}-${s}`, className: "text-xs capitalize", children: s })
3900
4031
  ] }, s))
3901
4032
  }
3902
- ) })
4033
+ ) }),
4034
+ /* @__PURE__ */ jsxRuntime.jsx(
4035
+ AdvancedIdField,
4036
+ {
4037
+ label: "Action ID",
4038
+ help: "Optional. Sent in the interaction payload so your app knows which button was clicked.",
4039
+ htmlFor: `btn-action-${idx}`,
4040
+ value: el.action_id,
4041
+ placeholder: "e.g. approve_request",
4042
+ onChange: (next) => updateButton(idx, { action_id: next })
4043
+ }
4044
+ )
3903
4045
  ] }, idx);
3904
4046
  }),
3905
4047
  /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: addButton, className: "self-start", children: [
@@ -4182,7 +4324,18 @@ function ButtonsField({
4182
4324
  /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `${idPrefix}-btn-style-${idx}-${s}`, className: "text-xs capitalize", children: s })
4183
4325
  ] }, s))
4184
4326
  }
4185
- ) })
4327
+ ) }),
4328
+ /* @__PURE__ */ jsxRuntime.jsx(
4329
+ AdvancedIdField,
4330
+ {
4331
+ label: "Action ID",
4332
+ help: "Optional. Sent in the interaction payload so your app knows which button was clicked.",
4333
+ htmlFor: `${idPrefix}-btn-action-${idx}`,
4334
+ value: btn.action_id,
4335
+ placeholder: "e.g. open_details",
4336
+ onChange: (next) => update(idx, { action_id: next })
4337
+ }
4338
+ )
4186
4339
  ] }, idx);
4187
4340
  }),
4188
4341
  /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: addButton, disabled: atLimit, className: "self-start", children: [
@@ -8010,7 +8163,18 @@ function ButtonAccessoryFields({ button, onChange }) {
8010
8163
  /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `section-acc-btn-style-${s}`, className: "text-xs capitalize", children: s })
8011
8164
  ] }, s))
8012
8165
  }
8013
- ) })
8166
+ ) }),
8167
+ /* @__PURE__ */ jsxRuntime.jsx(
8168
+ AdvancedIdField,
8169
+ {
8170
+ label: "Action ID",
8171
+ help: "Optional. Sent in the interaction payload so your app knows which button was clicked.",
8172
+ htmlFor: "section-acc-btn-action",
8173
+ value: button.action_id,
8174
+ placeholder: "e.g. learn_more",
8175
+ onChange: (next) => onChange({ ...button, action_id: next })
8176
+ }
8177
+ )
8014
8178
  ] });
8015
8179
  }
8016
8180
  function ImageAccessoryFields({ image, onChange }) {
@@ -8482,7 +8646,8 @@ function VideoEditor({ block, onChange }) {
8482
8646
  function BlockEditor({
8483
8647
  block,
8484
8648
  errors,
8485
- onChange
8649
+ onChange,
8650
+ onDone
8486
8651
  }) {
8487
8652
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
8488
8653
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-sm font-semibold text-foreground", children: [
@@ -8490,7 +8655,19 @@ function BlockEditor({
8490
8655
  labelForBlockType(block.type)
8491
8656
  ] }) }),
8492
8657
  errors && errors.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-0.5 rounded-md border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive", children: errors.map((err, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: err }, i)) }) : null,
8493
- dispatch(block, onChange)
8658
+ dispatch(block, onChange),
8659
+ /* @__PURE__ */ jsxRuntime.jsx(
8660
+ AdvancedIdField,
8661
+ {
8662
+ label: "Block ID",
8663
+ help: "Optional. Unique within the message or view. Slack returns it in interaction payloads.",
8664
+ htmlFor: "block-id",
8665
+ value: block.block_id,
8666
+ placeholder: "e.g. approval_row",
8667
+ onChange: (next) => onChange({ ...block, block_id: next })
8668
+ }
8669
+ ),
8670
+ onDone ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end border-t pt-3", children: /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", size: "sm", onClick: onDone, children: "Done" }) }) : null
8494
8671
  ] });
8495
8672
  }
8496
8673
  function dispatch(block, onChange) {
@@ -8707,7 +8884,8 @@ function BlockRow({
8707
8884
  {
8708
8885
  block: builderBlock.block,
8709
8886
  errors,
8710
- onChange: (next) => onUpdate(builderBlock.id, next)
8887
+ onChange: (next) => onUpdate(builderBlock.id, next),
8888
+ onDone: () => onOpenChange?.(false)
8711
8889
  }
8712
8890
  ) })
8713
8891
  ] }),
@@ -8971,7 +9149,15 @@ function ContainerRow({
8971
9149
  ]
8972
9150
  }
8973
9151
  ) }),
8974
- /* @__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) }) })
9152
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsxRuntime.jsx(
9153
+ BlockEditor,
9154
+ {
9155
+ block,
9156
+ errors,
9157
+ onChange: (next) => onUpdate(id, next),
9158
+ onDone: () => onOpenBlockChange?.(null)
9159
+ }
9160
+ ) })
8975
9161
  ] }),
8976
9162
  /* @__PURE__ */ jsxRuntime.jsx(
8977
9163
  "div",
@@ -9452,6 +9638,10 @@ function formatMessageTs(ts) {
9452
9638
  }
9453
9639
  var SEND_MENU_ITEM = "flex w-full cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-left text-sm text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50";
9454
9640
  function Toolbar2({
9641
+ canUndo = false,
9642
+ canRedo = false,
9643
+ onUndo,
9644
+ onRedo,
9455
9645
  onClear,
9456
9646
  onOpenJson,
9457
9647
  onOpenIssues,
@@ -9565,6 +9755,36 @@ function Toolbar2({
9565
9755
  ) : null
9566
9756
  ] }),
9567
9757
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 sm:gap-2", children: [
9758
+ onUndo && onRedo ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center", children: [
9759
+ /* @__PURE__ */ jsxRuntime.jsx(
9760
+ Button,
9761
+ {
9762
+ type: "button",
9763
+ variant: "ghost",
9764
+ size: "sm",
9765
+ onClick: onUndo,
9766
+ disabled: !canUndo,
9767
+ "aria-label": "Undo",
9768
+ title: "Undo (Ctrl+Z)",
9769
+ className: "px-2",
9770
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { className: "h-3.5 w-3.5" })
9771
+ }
9772
+ ),
9773
+ /* @__PURE__ */ jsxRuntime.jsx(
9774
+ Button,
9775
+ {
9776
+ type: "button",
9777
+ variant: "ghost",
9778
+ size: "sm",
9779
+ onClick: onRedo,
9780
+ disabled: !canRedo,
9781
+ "aria-label": "Redo",
9782
+ title: "Redo (Ctrl+Shift+Z)",
9783
+ className: "px-2",
9784
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { className: "h-3.5 w-3.5" })
9785
+ }
9786
+ )
9787
+ ] }) : null,
9568
9788
  errorCount > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
9569
9789
  Button,
9570
9790
  {
@@ -9965,7 +10185,22 @@ function BlockKitchen(props) {
9965
10185
  Boolean(primaryAction && sendEnabled),
9966
10186
  "[BlockKitchen] `primaryAction` is only available in compose-only mode \u2014 with the send integration wired, the built-in Send/Update flow owns the toolbar\u2019s primary action; ignoring `primaryAction`."
9967
10187
  );
9968
- const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
10188
+ const {
10189
+ blocks,
10190
+ addBlock,
10191
+ addChild,
10192
+ updateBlock,
10193
+ removeBlock,
10194
+ duplicateBlock,
10195
+ reorderBlock,
10196
+ moveBlock,
10197
+ replaceAll,
10198
+ resetAll,
10199
+ undo,
10200
+ redo,
10201
+ canUndo,
10202
+ canRedo
10203
+ } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
9969
10204
  const childParentById = React.useMemo(() => {
9970
10205
  const map = /* @__PURE__ */ new Map();
9971
10206
  for (const top of blocks) {
@@ -10081,6 +10316,30 @@ function BlockKitchen(props) {
10081
10316
  const handleDragCancel = React.useCallback(() => {
10082
10317
  setActivePaletteVariantId(null);
10083
10318
  }, []);
10319
+ const handleKeyDown = React.useCallback(
10320
+ (event) => {
10321
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) {
10322
+ return;
10323
+ }
10324
+ const key = event.key.toLowerCase();
10325
+ const isUndo = key === "z" && !event.shiftKey;
10326
+ const isRedo = key === "y" || key === "z" && event.shiftKey;
10327
+ if (!isUndo && !isRedo) {
10328
+ return;
10329
+ }
10330
+ const target = event.target;
10331
+ if (target && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))) {
10332
+ return;
10333
+ }
10334
+ event.preventDefault();
10335
+ if (isRedo) {
10336
+ if (canRedo) redo();
10337
+ } else if (canUndo) {
10338
+ undo();
10339
+ }
10340
+ },
10341
+ [canRedo, canUndo, redo, undo]
10342
+ );
10084
10343
  const activePaletteVariant = activePaletteVariantId ? variantById.get(activePaletteVariantId) : null;
10085
10344
  const blockPayloads = blocks.map((b) => b.block);
10086
10345
  return /* @__PURE__ */ jsxRuntime.jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsxRuntime.jsx(CustomEmojiProvider, { customEmojis, children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -10092,91 +10351,102 @@ function BlockKitchen(props) {
10092
10351
  onDragEnd: handleDragEnd,
10093
10352
  onDragCancel: handleDragCancel,
10094
10353
  children: [
10095
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground", children: [
10096
- /* @__PURE__ */ jsxRuntime.jsx(
10097
- Toolbar2,
10098
- {
10099
- onClear: () => replaceAll([]),
10100
- onOpenJson: () => setJsonOpen(true),
10101
- onOpenIssues: () => setIssuesOpen(true),
10102
- onOpenSend: () => setSendOpen(true),
10103
- onOpenPalette: () => setPaletteOpen(true),
10104
- canSend: blocks.length > 0,
10105
- canClear: blocks.length > 0,
10106
- errorCount: validation.total,
10107
- previewTheme,
10108
- onPreviewThemeChange: setUncontrolledPreviewTheme,
10109
- previewSurface,
10110
- onPreviewSurfaceChange: setPreviewSurface,
10111
- allowedSurfaces,
10112
- showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
10113
- docsLink,
10114
- showSend: sendEnabled,
10115
- primaryAction: !sendEnabled && primaryAction ? {
10116
- label: primaryAction.label,
10117
- onClick: () => primaryAction.onClick({
10118
- blocks: blockPayloads,
10119
- validation: validationSummary,
10120
- loadedMessage: activeEditTarget
10121
- }),
10122
- disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10123
- } : null,
10124
- sendButtonLabel,
10125
- loadEnabled,
10126
- updateEnabled: Boolean(onUpdate),
10127
- editBadge: activeEditTarget ? {
10128
- channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
10129
- ts: activeEditTarget.ts
10130
- } : null,
10131
- onOpenLoad: () => setLoadOpen(true),
10132
- onOpenUpdate: () => setUpdateOpen(true),
10133
- onExitEdit: () => {
10134
- if (sendEnabled) {
10135
- setEditTarget(null);
10136
- replaceAll([]);
10137
- setLoadOpen(true);
10138
- return;
10354
+ /* @__PURE__ */ jsxRuntime.jsxs(
10355
+ "div",
10356
+ {
10357
+ className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground",
10358
+ onKeyDown: handleKeyDown,
10359
+ children: [
10360
+ /* @__PURE__ */ jsxRuntime.jsx(
10361
+ Toolbar2,
10362
+ {
10363
+ canUndo,
10364
+ canRedo,
10365
+ onUndo: undo,
10366
+ onRedo: redo,
10367
+ onClear: () => replaceAll([]),
10368
+ onOpenJson: () => setJsonOpen(true),
10369
+ onOpenIssues: () => setIssuesOpen(true),
10370
+ onOpenSend: () => setSendOpen(true),
10371
+ onOpenPalette: () => setPaletteOpen(true),
10372
+ canSend: blocks.length > 0,
10373
+ canClear: blocks.length > 0,
10374
+ errorCount: validation.total,
10375
+ previewTheme,
10376
+ onPreviewThemeChange: setUncontrolledPreviewTheme,
10377
+ previewSurface,
10378
+ onPreviewSurfaceChange: setPreviewSurface,
10379
+ allowedSurfaces,
10380
+ showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
10381
+ docsLink,
10382
+ showSend: sendEnabled,
10383
+ primaryAction: !sendEnabled && primaryAction ? {
10384
+ label: primaryAction.label,
10385
+ onClick: () => primaryAction.onClick({
10386
+ blocks: blockPayloads,
10387
+ validation: validationSummary,
10388
+ loadedMessage: activeEditTarget
10389
+ }),
10390
+ disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10391
+ } : null,
10392
+ sendButtonLabel,
10393
+ loadEnabled,
10394
+ updateEnabled: Boolean(onUpdate),
10395
+ editBadge: activeEditTarget ? {
10396
+ channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
10397
+ ts: activeEditTarget.ts
10398
+ } : null,
10399
+ onOpenLoad: () => setLoadOpen(true),
10400
+ onOpenUpdate: () => setUpdateOpen(true),
10401
+ onExitEdit: () => {
10402
+ if (sendEnabled) {
10403
+ setEditTarget(null);
10404
+ resetAll([]);
10405
+ setLoadOpen(true);
10406
+ return;
10407
+ }
10408
+ setEditTarget(null);
10409
+ },
10410
+ loadButtonLabel,
10411
+ updateButtonLabel
10139
10412
  }
10140
- setEditTarget(null);
10141
- },
10142
- loadButtonLabel,
10143
- updateButtonLabel
10144
- }
10145
- ),
10146
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10147
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(
10148
- Palette,
10149
- {
10150
- onAddBlock: (block) => addBlock(block),
10151
- sections: paletteSections,
10152
- showSearch: showPaletteSearch,
10153
- searchPlaceholder: paletteSearchPlaceholder,
10154
- defaultOpenSections
10155
- }
10156
- ) }),
10157
- /* @__PURE__ */ jsxRuntime.jsx(
10158
- Surface,
10159
- {
10160
- blocks,
10161
- workspaceName,
10162
- authorName: activeEditTarget?.username,
10163
- authorIcon: activeEditTarget?.iconUrl,
10164
- previewHooks: mergedPreviewHooks,
10165
- previewTheme,
10166
- previewSurface,
10167
- errorsByBlockId: validation.byBlockId,
10168
- openBlockId,
10169
- onOpenBlockChange: setOpenBlockId,
10170
- onUpdate: updateBlock,
10171
- onDuplicate: duplicateBlock,
10172
- onDelete: removeBlock,
10173
- onReorder: reorderBlock,
10174
- isPaletteDrag: activePaletteVariant !== null,
10175
- onOpenPalette: () => setPaletteOpen(true)
10176
- }
10177
- )
10178
- ] })
10179
- ] }),
10413
+ ),
10414
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10415
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(
10416
+ Palette,
10417
+ {
10418
+ onAddBlock: (block) => addBlock(block),
10419
+ sections: paletteSections,
10420
+ showSearch: showPaletteSearch,
10421
+ searchPlaceholder: paletteSearchPlaceholder,
10422
+ defaultOpenSections
10423
+ }
10424
+ ) }),
10425
+ /* @__PURE__ */ jsxRuntime.jsx(
10426
+ Surface,
10427
+ {
10428
+ blocks,
10429
+ workspaceName,
10430
+ authorName: activeEditTarget?.username,
10431
+ authorIcon: activeEditTarget?.iconUrl,
10432
+ previewHooks: mergedPreviewHooks,
10433
+ previewTheme,
10434
+ previewSurface,
10435
+ errorsByBlockId: validation.byBlockId,
10436
+ openBlockId,
10437
+ onOpenBlockChange: setOpenBlockId,
10438
+ onUpdate: updateBlock,
10439
+ onDuplicate: duplicateBlock,
10440
+ onDelete: removeBlock,
10441
+ onReorder: reorderBlock,
10442
+ isPaletteDrag: activePaletteVariant !== null,
10443
+ onOpenPalette: () => setPaletteOpen(true)
10444
+ }
10445
+ )
10446
+ ] })
10447
+ ]
10448
+ }
10449
+ ),
10180
10450
  /* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { dropAnimation: null, children: activePaletteVariant ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 rounded border bg-background px-1.5 py-1 text-xs text-foreground shadow-md", children: [
10181
10451
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVertical, { className: "h-3 w-3 shrink-0" }),
10182
10452
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: activePaletteVariant.label })
@@ -10253,13 +10523,13 @@ function BlockKitchen(props) {
10253
10523
  loadRecentMessages: loading.loadRecentMessages,
10254
10524
  loadChannels: recentChannelSource,
10255
10525
  onLoaded: (result) => {
10256
- replaceAll(result.blocks);
10526
+ resetAll(result.blocks);
10257
10527
  setEditTarget(toLoadedMessage(result));
10258
10528
  setLoadOpen(false);
10259
10529
  },
10260
10530
  onOpenAsNew: (loadedBlocks) => {
10261
10531
  if (loadedBlocks) {
10262
- replaceAll(loadedBlocks);
10532
+ resetAll(loadedBlocks);
10263
10533
  }
10264
10534
  setEditTarget(null);
10265
10535
  setLoadOpen(false);