@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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, pointerWithin, closestCenter, DndContext, DragOverlay, useDroppable, useDraggable } from '@dnd-kit/core';
2
2
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
3
- import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, Loader2, ExternalLink, AlertTriangle, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, MailSearch, Plus, ChevronDown, BookOpen, Trash2, Code2, Pencil, Send, Search, Check, Copy, Info, CheckCircle2, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Undo2, Redo2, Smile, Link as Link$1 } from 'lucide-react';
3
+ import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, Loader2, ExternalLink, AlertTriangle, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, MailSearch, Plus, ChevronDown, BookOpen, Undo2, Redo2, Trash2, Code2, Pencil, Send, Search, Check, Copy, Info, CheckCircle2, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Smile, Link as Link$1 } from 'lucide-react';
4
4
  import * as React from 'react';
5
5
  import { createContext, useState, useRef, useEffect, useCallback, useMemo, useId, useContext } from 'react';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -2036,6 +2036,69 @@ function useBlockKitValidation(blocks, surface) {
2036
2036
  }, [blocks, surface]);
2037
2037
  return state;
2038
2038
  }
2039
+ var MAX_HISTORY = 20;
2040
+ function useHistoryState(initial) {
2041
+ const [record, setRecord] = useState(() => ({
2042
+ past: [],
2043
+ present: typeof initial === "function" ? initial() : initial,
2044
+ future: [],
2045
+ lastTag: null
2046
+ }));
2047
+ const commit = useCallback((updater, options) => {
2048
+ const tag = options?.tag ?? null;
2049
+ setRecord((h) => {
2050
+ const next = typeof updater === "function" ? updater(h.present) : updater;
2051
+ if (Object.is(next, h.present)) {
2052
+ return h;
2053
+ }
2054
+ if (tag !== null && h.lastTag === tag) {
2055
+ return { ...h, present: next, future: [] };
2056
+ }
2057
+ const past = [...h.past, h.present];
2058
+ return {
2059
+ past: past.length > MAX_HISTORY ? past.slice(past.length - MAX_HISTORY) : past,
2060
+ present: next,
2061
+ future: [],
2062
+ lastTag: tag
2063
+ };
2064
+ });
2065
+ }, []);
2066
+ const undo = useCallback(() => {
2067
+ setRecord((h) => {
2068
+ if (h.past.length === 0) {
2069
+ return h;
2070
+ }
2071
+ const previous = h.past[h.past.length - 1];
2072
+ return { past: h.past.slice(0, -1), present: previous, future: [h.present, ...h.future], lastTag: null };
2073
+ });
2074
+ }, []);
2075
+ const redo = useCallback(() => {
2076
+ setRecord((h) => {
2077
+ if (h.future.length === 0) {
2078
+ return h;
2079
+ }
2080
+ const [next, ...rest] = h.future;
2081
+ return { past: [...h.past, h.present], present: next, future: rest, lastTag: null };
2082
+ });
2083
+ }, []);
2084
+ const reset = useCallback((next) => {
2085
+ setRecord({ past: [], present: next, future: [], lastTag: null });
2086
+ }, []);
2087
+ return useMemo(
2088
+ () => ({
2089
+ present: record.present,
2090
+ canUndo: record.past.length > 0,
2091
+ canRedo: record.future.length > 0,
2092
+ commit,
2093
+ undo,
2094
+ redo,
2095
+ reset
2096
+ }),
2097
+ [record, commit, undo, redo, reset]
2098
+ );
2099
+ }
2100
+
2101
+ // src/state/use-block-kitchen-state.ts
2039
2102
  function mirror(node) {
2040
2103
  if (!node.children) {
2041
2104
  return node;
@@ -2086,7 +2149,15 @@ function useBlockKitchenState({
2086
2149
  initialBlocks,
2087
2150
  onChange
2088
2151
  } = {}) {
2089
- const [blocks, setBlocks] = useState(() => (initialBlocks ?? []).map(wrap));
2152
+ const {
2153
+ present: blocks,
2154
+ commit,
2155
+ undo,
2156
+ redo,
2157
+ reset,
2158
+ canUndo,
2159
+ canRedo
2160
+ } = useHistoryState(() => (initialBlocks ?? []).map(wrap));
2090
2161
  const onChangeRef = useRef(onChange);
2091
2162
  useEffect(() => {
2092
2163
  onChangeRef.current = onChange;
@@ -2099,121 +2170,155 @@ function useBlockKitchenState({
2099
2170
  }
2100
2171
  onChangeRef.current?.(blocks.map((b) => b.block));
2101
2172
  }, [blocks]);
2102
- const addBlock = useCallback((block, atIndex) => {
2103
- setBlocks((prev) => {
2104
- const next = [...prev];
2105
- const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2106
- next.splice(idx, 0, wrap(block));
2107
- return next;
2108
- });
2109
- }, []);
2110
- const addChild = useCallback((parentId, block, atIndex) => {
2111
- if (!isContainerChildType(block.type)) {
2112
- return;
2113
- }
2114
- setBlocks(
2115
- (prev) => withChildren(prev, parentId, (children) => {
2116
- if (children.length >= MAX_CONTAINER_CHILDREN) {
2117
- return children;
2118
- }
2119
- const next = [...children];
2173
+ const addBlock = useCallback(
2174
+ (block, atIndex) => {
2175
+ commit((prev) => {
2176
+ const next = [...prev];
2120
2177
  const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2121
2178
  next.splice(idx, 0, wrap(block));
2122
2179
  return next;
2123
- })
2124
- );
2125
- }, []);
2126
- const updateBlock = useCallback((id, block) => {
2127
- setBlocks(
2128
- (prev) => prev.map((b) => {
2129
- if (b.id === id) {
2130
- return applyUpdate(b, block);
2131
- }
2132
- if (b.children?.some((c) => c.id === id)) {
2133
- return mirror({ ...b, children: b.children.map((c) => c.id === id ? applyUpdate(c, block) : c) });
2134
- }
2135
- return b;
2136
- })
2137
- );
2138
- }, []);
2139
- const removeBlock = useCallback((id) => {
2140
- setBlocks((prev) => {
2141
- if (prev.some((b) => b.id === id)) {
2142
- return prev.filter((b) => b.id !== id);
2180
+ });
2181
+ },
2182
+ [commit]
2183
+ );
2184
+ const addChild = useCallback(
2185
+ (parentId, block, atIndex) => {
2186
+ if (!isContainerChildType(block.type)) {
2187
+ return;
2143
2188
  }
2144
- return prev.map(
2145
- (b) => b.children?.some((c) => c.id === id) ? mirror({ ...b, children: b.children.filter((c) => c.id !== id) }) : b
2189
+ commit(
2190
+ (prev) => withChildren(prev, parentId, (children) => {
2191
+ if (children.length >= MAX_CONTAINER_CHILDREN) {
2192
+ return children;
2193
+ }
2194
+ const next = [...children];
2195
+ const idx = atIndex === void 0 ? next.length : Math.max(0, Math.min(atIndex, next.length));
2196
+ next.splice(idx, 0, wrap(block));
2197
+ return next;
2198
+ })
2146
2199
  );
2147
- });
2148
- }, []);
2149
- const duplicateBlock = useCallback((id) => {
2150
- setBlocks((prev) => {
2151
- const loc = locate(prev, id);
2152
- if (!loc) {
2153
- return prev;
2154
- }
2155
- if (loc.kind === "top") {
2156
- const next = [...prev];
2157
- next.splice(loc.index + 1, 0, cloneNode(prev[loc.index]));
2158
- return next;
2159
- }
2160
- return withChildren(prev, loc.parentId, (children) => {
2161
- const next = [...children];
2162
- next.splice(loc.index + 1, 0, cloneNode(children[loc.index]));
2163
- return next;
2200
+ },
2201
+ [commit]
2202
+ );
2203
+ const updateBlock = useCallback(
2204
+ (id, block) => {
2205
+ commit(
2206
+ (prev) => prev.map((b) => {
2207
+ if (b.id === id) {
2208
+ return applyUpdate(b, block);
2209
+ }
2210
+ if (b.children?.some((c) => c.id === id)) {
2211
+ return mirror({ ...b, children: b.children.map((c) => c.id === id ? applyUpdate(c, block) : c) });
2212
+ }
2213
+ return b;
2214
+ }),
2215
+ // Coalesce a run of edits to the same block (every keystroke in an
2216
+ // editor field lands here) into a single undo step. Editing a
2217
+ // different block, or any structural change, starts a new step.
2218
+ { tag: `update:${id}` }
2219
+ );
2220
+ },
2221
+ [commit]
2222
+ );
2223
+ const removeBlock = useCallback(
2224
+ (id) => {
2225
+ commit((prev) => {
2226
+ if (prev.some((b) => b.id === id)) {
2227
+ return prev.filter((b) => b.id !== id);
2228
+ }
2229
+ return prev.map(
2230
+ (b) => b.children?.some((c) => c.id === id) ? mirror({ ...b, children: b.children.filter((c) => c.id !== id) }) : b
2231
+ );
2164
2232
  });
2165
- });
2166
- }, []);
2167
- const reorderBlock = useCallback((fromId, toIndex) => {
2168
- setBlocks((prev) => {
2169
- const loc = locate(prev, fromId);
2170
- if (!loc) {
2171
- return prev;
2172
- }
2173
- const reorder = (list) => {
2174
- const next = [...list];
2175
- const [moved] = next.splice(loc.index, 1);
2176
- next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, moved);
2177
- return next;
2178
- };
2179
- if (loc.kind === "top") {
2180
- return reorder(prev);
2181
- }
2182
- return withChildren(prev, loc.parentId, reorder);
2183
- });
2184
- }, []);
2185
- const moveBlock = useCallback((activeId, target) => {
2186
- setBlocks((prev) => {
2187
- const loc = locate(prev, activeId);
2188
- if (!loc) {
2189
- return prev;
2190
- }
2191
- const moved = loc.kind === "top" ? prev[loc.index] : prev.find((b) => b.id === loc.parentId)?.children?.[loc.index];
2192
- if (!moved) {
2193
- return prev;
2194
- }
2195
- if (target.kind === "container") {
2196
- if (moved.children || !isContainerChildType(moved.block.type)) {
2233
+ },
2234
+ [commit]
2235
+ );
2236
+ const duplicateBlock = useCallback(
2237
+ (id) => {
2238
+ commit((prev) => {
2239
+ const loc = locate(prev, id);
2240
+ if (!loc) {
2197
2241
  return prev;
2198
2242
  }
2199
- const sameContainer = loc.kind === "child" && loc.parentId === target.parentId;
2200
- const destCount = prev.find((b) => b.id === target.parentId)?.children?.length ?? 0;
2201
- if (!sameContainer && destCount >= MAX_CONTAINER_CHILDREN) {
2243
+ if (loc.kind === "top") {
2244
+ const next = [...prev];
2245
+ next.splice(loc.index + 1, 0, cloneNode(prev[loc.index]));
2246
+ return next;
2247
+ }
2248
+ return withChildren(prev, loc.parentId, (children) => {
2249
+ const next = [...children];
2250
+ next.splice(loc.index + 1, 0, cloneNode(children[loc.index]));
2251
+ return next;
2252
+ });
2253
+ });
2254
+ },
2255
+ [commit]
2256
+ );
2257
+ const reorderBlock = useCallback(
2258
+ (fromId, toIndex) => {
2259
+ commit((prev) => {
2260
+ const loc = locate(prev, fromId);
2261
+ if (!loc) {
2202
2262
  return prev;
2203
2263
  }
2204
- }
2205
- const detached = loc.kind === "top" ? prev.filter((_, i) => i !== loc.index) : withChildren(prev, loc.parentId, (children) => children.filter((_, i) => i !== loc.index));
2206
- const insert = (list) => {
2207
- const next = [...list];
2208
- next.splice(Math.max(0, Math.min(target.index, next.length)), 0, moved);
2209
- return next;
2210
- };
2211
- return target.kind === "container" ? withChildren(detached, target.parentId, insert) : insert(detached);
2212
- });
2213
- }, []);
2214
- const replaceAll = useCallback((newBlocks) => {
2215
- setBlocks(sanitizeBlocks(newBlocks).map(wrap));
2216
- }, []);
2264
+ const reorder = (list) => {
2265
+ const next = [...list];
2266
+ const [moved] = next.splice(loc.index, 1);
2267
+ next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, moved);
2268
+ return next;
2269
+ };
2270
+ if (loc.kind === "top") {
2271
+ return reorder(prev);
2272
+ }
2273
+ return withChildren(prev, loc.parentId, reorder);
2274
+ });
2275
+ },
2276
+ [commit]
2277
+ );
2278
+ const moveBlock = useCallback(
2279
+ (activeId, target) => {
2280
+ commit((prev) => {
2281
+ const loc = locate(prev, activeId);
2282
+ if (!loc) {
2283
+ return prev;
2284
+ }
2285
+ const moved = loc.kind === "top" ? prev[loc.index] : prev.find((b) => b.id === loc.parentId)?.children?.[loc.index];
2286
+ if (!moved) {
2287
+ return prev;
2288
+ }
2289
+ if (target.kind === "container") {
2290
+ if (moved.children || !isContainerChildType(moved.block.type)) {
2291
+ return prev;
2292
+ }
2293
+ const sameContainer = loc.kind === "child" && loc.parentId === target.parentId;
2294
+ const destCount = prev.find((b) => b.id === target.parentId)?.children?.length ?? 0;
2295
+ if (!sameContainer && destCount >= MAX_CONTAINER_CHILDREN) {
2296
+ return prev;
2297
+ }
2298
+ }
2299
+ const detached = loc.kind === "top" ? prev.filter((_, i) => i !== loc.index) : withChildren(prev, loc.parentId, (children) => children.filter((_, i) => i !== loc.index));
2300
+ const insert = (list) => {
2301
+ const next = [...list];
2302
+ next.splice(Math.max(0, Math.min(target.index, next.length)), 0, moved);
2303
+ return next;
2304
+ };
2305
+ return target.kind === "container" ? withChildren(detached, target.parentId, insert) : insert(detached);
2306
+ });
2307
+ },
2308
+ [commit]
2309
+ );
2310
+ const replaceAll = useCallback(
2311
+ (newBlocks) => {
2312
+ commit(sanitizeBlocks(newBlocks).map(wrap));
2313
+ },
2314
+ [commit]
2315
+ );
2316
+ const resetAll = useCallback(
2317
+ (newBlocks) => {
2318
+ reset(sanitizeBlocks(newBlocks).map(wrap));
2319
+ },
2320
+ [reset]
2321
+ );
2217
2322
  return {
2218
2323
  blocks,
2219
2324
  addBlock,
@@ -2223,7 +2328,12 @@ function useBlockKitchenState({
2223
2328
  duplicateBlock,
2224
2329
  reorderBlock,
2225
2330
  moveBlock,
2226
- replaceAll
2331
+ replaceAll,
2332
+ resetAll,
2333
+ undo,
2334
+ redo,
2335
+ canUndo,
2336
+ canRedo
2227
2337
  };
2228
2338
  }
2229
2339
  function useDevWarning(condition, message) {
@@ -3771,6 +3881,27 @@ function EditorField({
3771
3881
  help && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: help })
3772
3882
  ] });
3773
3883
  }
3884
+ function AdvancedIdField({
3885
+ label,
3886
+ help,
3887
+ htmlFor,
3888
+ value,
3889
+ placeholder,
3890
+ onChange
3891
+ }) {
3892
+ return /* @__PURE__ */ jsxs("details", { className: "rounded-md border bg-muted/20 px-2.5 py-1.5", children: [
3893
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer text-[11px] font-medium text-muted-foreground hover:text-foreground", children: "Advanced" }),
3894
+ /* @__PURE__ */ jsx("div", { className: "pt-2 pb-1", children: /* @__PURE__ */ jsx(EditorField, { label, help, htmlFor, children: /* @__PURE__ */ jsx(
3895
+ Input,
3896
+ {
3897
+ id: htmlFor,
3898
+ value: value ?? "",
3899
+ placeholder,
3900
+ onChange: (e) => onChange(e.target.value || void 0)
3901
+ }
3902
+ ) }) })
3903
+ ] });
3904
+ }
3774
3905
  function ActionsEditor({ block, onChange }) {
3775
3906
  const elements = block.elements ?? [];
3776
3907
  const updateButton = (idx, update) => {
@@ -3869,7 +4000,18 @@ function ActionsEditor({ block, onChange }) {
3869
4000
  /* @__PURE__ */ jsx(Label, { htmlFor: `btn-style-${idx}-${s}`, className: "text-xs capitalize", children: s })
3870
4001
  ] }, s))
3871
4002
  }
3872
- ) })
4003
+ ) }),
4004
+ /* @__PURE__ */ jsx(
4005
+ AdvancedIdField,
4006
+ {
4007
+ label: "Action ID",
4008
+ help: "Optional. Sent in the interaction payload so your app knows which button was clicked.",
4009
+ htmlFor: `btn-action-${idx}`,
4010
+ value: el.action_id,
4011
+ placeholder: "e.g. approve_request",
4012
+ onChange: (next) => updateButton(idx, { action_id: next })
4013
+ }
4014
+ )
3873
4015
  ] }, idx);
3874
4016
  }),
3875
4017
  /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: addButton, className: "self-start", children: [
@@ -4152,7 +4294,18 @@ function ButtonsField({
4152
4294
  /* @__PURE__ */ jsx(Label, { htmlFor: `${idPrefix}-btn-style-${idx}-${s}`, className: "text-xs capitalize", children: s })
4153
4295
  ] }, s))
4154
4296
  }
4155
- ) })
4297
+ ) }),
4298
+ /* @__PURE__ */ jsx(
4299
+ AdvancedIdField,
4300
+ {
4301
+ label: "Action ID",
4302
+ help: "Optional. Sent in the interaction payload so your app knows which button was clicked.",
4303
+ htmlFor: `${idPrefix}-btn-action-${idx}`,
4304
+ value: btn.action_id,
4305
+ placeholder: "e.g. open_details",
4306
+ onChange: (next) => update(idx, { action_id: next })
4307
+ }
4308
+ )
4156
4309
  ] }, idx);
4157
4310
  }),
4158
4311
  /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: addButton, disabled: atLimit, className: "self-start", children: [
@@ -7980,7 +8133,18 @@ function ButtonAccessoryFields({ button, onChange }) {
7980
8133
  /* @__PURE__ */ jsx(Label, { htmlFor: `section-acc-btn-style-${s}`, className: "text-xs capitalize", children: s })
7981
8134
  ] }, s))
7982
8135
  }
7983
- ) })
8136
+ ) }),
8137
+ /* @__PURE__ */ jsx(
8138
+ AdvancedIdField,
8139
+ {
8140
+ label: "Action ID",
8141
+ help: "Optional. Sent in the interaction payload so your app knows which button was clicked.",
8142
+ htmlFor: "section-acc-btn-action",
8143
+ value: button.action_id,
8144
+ placeholder: "e.g. learn_more",
8145
+ onChange: (next) => onChange({ ...button, action_id: next })
8146
+ }
8147
+ )
7984
8148
  ] });
7985
8149
  }
7986
8150
  function ImageAccessoryFields({ image, onChange }) {
@@ -8452,7 +8616,8 @@ function VideoEditor({ block, onChange }) {
8452
8616
  function BlockEditor({
8453
8617
  block,
8454
8618
  errors,
8455
- onChange
8619
+ onChange,
8620
+ onDone
8456
8621
  }) {
8457
8622
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
8458
8623
  /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold text-foreground", children: [
@@ -8460,7 +8625,19 @@ function BlockEditor({
8460
8625
  labelForBlockType(block.type)
8461
8626
  ] }) }),
8462
8627
  errors && errors.length > 0 ? /* @__PURE__ */ 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__ */ jsx("li", { children: err }, i)) }) : null,
8463
- dispatch(block, onChange)
8628
+ dispatch(block, onChange),
8629
+ /* @__PURE__ */ jsx(
8630
+ AdvancedIdField,
8631
+ {
8632
+ label: "Block ID",
8633
+ help: "Optional. Unique within the message or view. Slack returns it in interaction payloads.",
8634
+ htmlFor: "block-id",
8635
+ value: block.block_id,
8636
+ placeholder: "e.g. approval_row",
8637
+ onChange: (next) => onChange({ ...block, block_id: next })
8638
+ }
8639
+ ),
8640
+ onDone ? /* @__PURE__ */ jsx("div", { className: "flex justify-end border-t pt-3", children: /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: onDone, children: "Done" }) }) : null
8464
8641
  ] });
8465
8642
  }
8466
8643
  function dispatch(block, onChange) {
@@ -8677,7 +8854,8 @@ function BlockRow({
8677
8854
  {
8678
8855
  block: builderBlock.block,
8679
8856
  errors,
8680
- onChange: (next) => onUpdate(builderBlock.id, next)
8857
+ onChange: (next) => onUpdate(builderBlock.id, next),
8858
+ onDone: () => onOpenChange?.(false)
8681
8859
  }
8682
8860
  ) })
8683
8861
  ] }),
@@ -8941,7 +9119,15 @@ function ContainerRow({
8941
9119
  ]
8942
9120
  }
8943
9121
  ) }),
8944
- /* @__PURE__ */ jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsx(BlockEditor, { block, errors, onChange: (next) => onUpdate(id, next) }) })
9122
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsx(
9123
+ BlockEditor,
9124
+ {
9125
+ block,
9126
+ errors,
9127
+ onChange: (next) => onUpdate(id, next),
9128
+ onDone: () => onOpenBlockChange?.(null)
9129
+ }
9130
+ ) })
8945
9131
  ] }),
8946
9132
  /* @__PURE__ */ jsx(
8947
9133
  "div",
@@ -9422,6 +9608,10 @@ function formatMessageTs(ts) {
9422
9608
  }
9423
9609
  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";
9424
9610
  function Toolbar2({
9611
+ canUndo = false,
9612
+ canRedo = false,
9613
+ onUndo,
9614
+ onRedo,
9425
9615
  onClear,
9426
9616
  onOpenJson,
9427
9617
  onOpenIssues,
@@ -9535,6 +9725,36 @@ function Toolbar2({
9535
9725
  ) : null
9536
9726
  ] }),
9537
9727
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 sm:gap-2", children: [
9728
+ onUndo && onRedo ? /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
9729
+ /* @__PURE__ */ jsx(
9730
+ Button,
9731
+ {
9732
+ type: "button",
9733
+ variant: "ghost",
9734
+ size: "sm",
9735
+ onClick: onUndo,
9736
+ disabled: !canUndo,
9737
+ "aria-label": "Undo",
9738
+ title: "Undo (Ctrl+Z)",
9739
+ className: "px-2",
9740
+ children: /* @__PURE__ */ jsx(Undo2, { className: "h-3.5 w-3.5" })
9741
+ }
9742
+ ),
9743
+ /* @__PURE__ */ jsx(
9744
+ Button,
9745
+ {
9746
+ type: "button",
9747
+ variant: "ghost",
9748
+ size: "sm",
9749
+ onClick: onRedo,
9750
+ disabled: !canRedo,
9751
+ "aria-label": "Redo",
9752
+ title: "Redo (Ctrl+Shift+Z)",
9753
+ className: "px-2",
9754
+ children: /* @__PURE__ */ jsx(Redo2, { className: "h-3.5 w-3.5" })
9755
+ }
9756
+ )
9757
+ ] }) : null,
9538
9758
  errorCount > 0 ? /* @__PURE__ */ jsxs(
9539
9759
  Button,
9540
9760
  {
@@ -9935,7 +10155,22 @@ function BlockKitchen(props) {
9935
10155
  Boolean(primaryAction && sendEnabled),
9936
10156
  "[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`."
9937
10157
  );
9938
- const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
10158
+ const {
10159
+ blocks,
10160
+ addBlock,
10161
+ addChild,
10162
+ updateBlock,
10163
+ removeBlock,
10164
+ duplicateBlock,
10165
+ reorderBlock,
10166
+ moveBlock,
10167
+ replaceAll,
10168
+ resetAll,
10169
+ undo,
10170
+ redo,
10171
+ canUndo,
10172
+ canRedo
10173
+ } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
9939
10174
  const childParentById = useMemo(() => {
9940
10175
  const map = /* @__PURE__ */ new Map();
9941
10176
  for (const top of blocks) {
@@ -10051,6 +10286,30 @@ function BlockKitchen(props) {
10051
10286
  const handleDragCancel = useCallback(() => {
10052
10287
  setActivePaletteVariantId(null);
10053
10288
  }, []);
10289
+ const handleKeyDown = useCallback(
10290
+ (event) => {
10291
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) {
10292
+ return;
10293
+ }
10294
+ const key = event.key.toLowerCase();
10295
+ const isUndo = key === "z" && !event.shiftKey;
10296
+ const isRedo = key === "y" || key === "z" && event.shiftKey;
10297
+ if (!isUndo && !isRedo) {
10298
+ return;
10299
+ }
10300
+ const target = event.target;
10301
+ if (target && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))) {
10302
+ return;
10303
+ }
10304
+ event.preventDefault();
10305
+ if (isRedo) {
10306
+ if (canRedo) redo();
10307
+ } else if (canUndo) {
10308
+ undo();
10309
+ }
10310
+ },
10311
+ [canRedo, canUndo, redo, undo]
10312
+ );
10054
10313
  const activePaletteVariant = activePaletteVariantId ? variantById.get(activePaletteVariantId) : null;
10055
10314
  const blockPayloads = blocks.map((b) => b.block);
10056
10315
  return /* @__PURE__ */ jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsx(CustomEmojiProvider, { customEmojis, children: /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(
@@ -10062,91 +10321,102 @@ function BlockKitchen(props) {
10062
10321
  onDragEnd: handleDragEnd,
10063
10322
  onDragCancel: handleDragCancel,
10064
10323
  children: [
10065
- /* @__PURE__ */ jsxs("div", { className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground", children: [
10066
- /* @__PURE__ */ jsx(
10067
- Toolbar2,
10068
- {
10069
- onClear: () => replaceAll([]),
10070
- onOpenJson: () => setJsonOpen(true),
10071
- onOpenIssues: () => setIssuesOpen(true),
10072
- onOpenSend: () => setSendOpen(true),
10073
- onOpenPalette: () => setPaletteOpen(true),
10074
- canSend: blocks.length > 0,
10075
- canClear: blocks.length > 0,
10076
- errorCount: validation.total,
10077
- previewTheme,
10078
- onPreviewThemeChange: setUncontrolledPreviewTheme,
10079
- previewSurface,
10080
- onPreviewSurfaceChange: setPreviewSurface,
10081
- allowedSurfaces,
10082
- showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
10083
- docsLink,
10084
- showSend: sendEnabled,
10085
- primaryAction: !sendEnabled && primaryAction ? {
10086
- label: primaryAction.label,
10087
- onClick: () => primaryAction.onClick({
10088
- blocks: blockPayloads,
10089
- validation: validationSummary,
10090
- loadedMessage: activeEditTarget
10091
- }),
10092
- disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10093
- } : null,
10094
- sendButtonLabel,
10095
- loadEnabled,
10096
- updateEnabled: Boolean(onUpdate),
10097
- editBadge: activeEditTarget ? {
10098
- channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
10099
- ts: activeEditTarget.ts
10100
- } : null,
10101
- onOpenLoad: () => setLoadOpen(true),
10102
- onOpenUpdate: () => setUpdateOpen(true),
10103
- onExitEdit: () => {
10104
- if (sendEnabled) {
10105
- setEditTarget(null);
10106
- replaceAll([]);
10107
- setLoadOpen(true);
10108
- return;
10324
+ /* @__PURE__ */ jsxs(
10325
+ "div",
10326
+ {
10327
+ className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground",
10328
+ onKeyDown: handleKeyDown,
10329
+ children: [
10330
+ /* @__PURE__ */ jsx(
10331
+ Toolbar2,
10332
+ {
10333
+ canUndo,
10334
+ canRedo,
10335
+ onUndo: undo,
10336
+ onRedo: redo,
10337
+ onClear: () => replaceAll([]),
10338
+ onOpenJson: () => setJsonOpen(true),
10339
+ onOpenIssues: () => setIssuesOpen(true),
10340
+ onOpenSend: () => setSendOpen(true),
10341
+ onOpenPalette: () => setPaletteOpen(true),
10342
+ canSend: blocks.length > 0,
10343
+ canClear: blocks.length > 0,
10344
+ errorCount: validation.total,
10345
+ previewTheme,
10346
+ onPreviewThemeChange: setUncontrolledPreviewTheme,
10347
+ previewSurface,
10348
+ onPreviewSurfaceChange: setPreviewSurface,
10349
+ allowedSurfaces,
10350
+ showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
10351
+ docsLink,
10352
+ showSend: sendEnabled,
10353
+ primaryAction: !sendEnabled && primaryAction ? {
10354
+ label: primaryAction.label,
10355
+ onClick: () => primaryAction.onClick({
10356
+ blocks: blockPayloads,
10357
+ validation: validationSummary,
10358
+ loadedMessage: activeEditTarget
10359
+ }),
10360
+ disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10361
+ } : null,
10362
+ sendButtonLabel,
10363
+ loadEnabled,
10364
+ updateEnabled: Boolean(onUpdate),
10365
+ editBadge: activeEditTarget ? {
10366
+ channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
10367
+ ts: activeEditTarget.ts
10368
+ } : null,
10369
+ onOpenLoad: () => setLoadOpen(true),
10370
+ onOpenUpdate: () => setUpdateOpen(true),
10371
+ onExitEdit: () => {
10372
+ if (sendEnabled) {
10373
+ setEditTarget(null);
10374
+ resetAll([]);
10375
+ setLoadOpen(true);
10376
+ return;
10377
+ }
10378
+ setEditTarget(null);
10379
+ },
10380
+ loadButtonLabel,
10381
+ updateButtonLabel
10109
10382
  }
10110
- setEditTarget(null);
10111
- },
10112
- loadButtonLabel,
10113
- updateButtonLabel
10114
- }
10115
- ),
10116
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10117
- /* @__PURE__ */ jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsx(
10118
- Palette,
10119
- {
10120
- onAddBlock: (block) => addBlock(block),
10121
- sections: paletteSections,
10122
- showSearch: showPaletteSearch,
10123
- searchPlaceholder: paletteSearchPlaceholder,
10124
- defaultOpenSections
10125
- }
10126
- ) }),
10127
- /* @__PURE__ */ jsx(
10128
- Surface,
10129
- {
10130
- blocks,
10131
- workspaceName,
10132
- authorName: activeEditTarget?.username,
10133
- authorIcon: activeEditTarget?.iconUrl,
10134
- previewHooks: mergedPreviewHooks,
10135
- previewTheme,
10136
- previewSurface,
10137
- errorsByBlockId: validation.byBlockId,
10138
- openBlockId,
10139
- onOpenBlockChange: setOpenBlockId,
10140
- onUpdate: updateBlock,
10141
- onDuplicate: duplicateBlock,
10142
- onDelete: removeBlock,
10143
- onReorder: reorderBlock,
10144
- isPaletteDrag: activePaletteVariant !== null,
10145
- onOpenPalette: () => setPaletteOpen(true)
10146
- }
10147
- )
10148
- ] })
10149
- ] }),
10383
+ ),
10384
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10385
+ /* @__PURE__ */ jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsx(
10386
+ Palette,
10387
+ {
10388
+ onAddBlock: (block) => addBlock(block),
10389
+ sections: paletteSections,
10390
+ showSearch: showPaletteSearch,
10391
+ searchPlaceholder: paletteSearchPlaceholder,
10392
+ defaultOpenSections
10393
+ }
10394
+ ) }),
10395
+ /* @__PURE__ */ jsx(
10396
+ Surface,
10397
+ {
10398
+ blocks,
10399
+ workspaceName,
10400
+ authorName: activeEditTarget?.username,
10401
+ authorIcon: activeEditTarget?.iconUrl,
10402
+ previewHooks: mergedPreviewHooks,
10403
+ previewTheme,
10404
+ previewSurface,
10405
+ errorsByBlockId: validation.byBlockId,
10406
+ openBlockId,
10407
+ onOpenBlockChange: setOpenBlockId,
10408
+ onUpdate: updateBlock,
10409
+ onDuplicate: duplicateBlock,
10410
+ onDelete: removeBlock,
10411
+ onReorder: reorderBlock,
10412
+ isPaletteDrag: activePaletteVariant !== null,
10413
+ onOpenPalette: () => setPaletteOpen(true)
10414
+ }
10415
+ )
10416
+ ] })
10417
+ ]
10418
+ }
10419
+ ),
10150
10420
  /* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: activePaletteVariant ? /* @__PURE__ */ 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: [
10151
10421
  /* @__PURE__ */ jsx(GripVertical, { className: "h-3 w-3 shrink-0" }),
10152
10422
  /* @__PURE__ */ jsx("span", { className: "truncate", children: activePaletteVariant.label })
@@ -10223,13 +10493,13 @@ function BlockKitchen(props) {
10223
10493
  loadRecentMessages: loading.loadRecentMessages,
10224
10494
  loadChannels: recentChannelSource,
10225
10495
  onLoaded: (result) => {
10226
- replaceAll(result.blocks);
10496
+ resetAll(result.blocks);
10227
10497
  setEditTarget(toLoadedMessage(result));
10228
10498
  setLoadOpen(false);
10229
10499
  },
10230
10500
  onOpenAsNew: (loadedBlocks) => {
10231
10501
  if (loadedBlocks) {
10232
- replaceAll(loadedBlocks);
10502
+ resetAll(loadedBlocks);
10233
10503
  }
10234
10504
  setEditTarget(null);
10235
10505
  setLoadOpen(false);