@tightknitai/block-kitchen 0.9.10 → 0.10.1

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,9 +2328,37 @@ 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
  }
2339
+ function useDevWarning(condition, message) {
2340
+ const warnedRef = useRef(false);
2341
+ useEffect(() => {
2342
+ if (condition && !warnedRef.current) {
2343
+ warnedRef.current = true;
2344
+ console.warn(message);
2345
+ }
2346
+ }, [condition, message]);
2347
+ }
2348
+ function useNotifyOnChange(value, toKey, callback) {
2349
+ const lastNotifiedRef = useRef(null);
2350
+ useEffect(() => {
2351
+ if (!callback) {
2352
+ return;
2353
+ }
2354
+ const key = toKey(value);
2355
+ if (lastNotifiedRef.current === key) {
2356
+ return;
2357
+ }
2358
+ lastNotifiedRef.current = key;
2359
+ callback(value);
2360
+ }, [value, toKey, callback]);
2361
+ }
2229
2362
 
2230
2363
  // src/lib/brand-theme.ts
2231
2364
  var TOKEN_TO_CSS_VAR = {
@@ -3023,7 +3156,7 @@ function LoadMessageDialog({
3023
3156
  loadRecentMessagesRef.current = loadRecentMessages;
3024
3157
  loadChannelsRef.current = loadChannels;
3025
3158
  });
3026
- const hasRecent = !!loadRecentMessages;
3159
+ const hasRecent = !!loadRecentMessages && !!loadChannels;
3027
3160
  useEffect(() => {
3028
3161
  if (!open) {
3029
3162
  return;
@@ -3034,7 +3167,10 @@ function LoadMessageDialog({
3034
3167
  setRecent(null);
3035
3168
  setRecentError(null);
3036
3169
  setSelectedRecent(null);
3037
- if (!loadRecentMessagesRef.current) {
3170
+ }, [open]);
3171
+ useEffect(() => {
3172
+ const load = loadChannelsRef.current;
3173
+ if (!open || !hasRecent || !load) {
3038
3174
  setChannels([]);
3039
3175
  setChannelsError(null);
3040
3176
  return;
@@ -3042,7 +3178,7 @@ function LoadMessageDialog({
3042
3178
  setChannels(null);
3043
3179
  setChannelsError(null);
3044
3180
  let cancelled = false;
3045
- loadChannelsRef.current().then((list) => {
3181
+ load().then((list) => {
3046
3182
  if (!cancelled) {
3047
3183
  setChannels(list);
3048
3184
  }
@@ -3054,7 +3190,7 @@ function LoadMessageDialog({
3054
3190
  return () => {
3055
3191
  cancelled = true;
3056
3192
  };
3057
- }, [open]);
3193
+ }, [open, hasRecent]);
3058
3194
  useEffect(() => {
3059
3195
  if (!open || !loadRecentMessagesRef.current || !channelId) {
3060
3196
  return;
@@ -8426,7 +8562,8 @@ function VideoEditor({ block, onChange }) {
8426
8562
  function BlockEditor({
8427
8563
  block,
8428
8564
  errors,
8429
- onChange
8565
+ onChange,
8566
+ onDone
8430
8567
  }) {
8431
8568
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
8432
8569
  /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold text-foreground", children: [
@@ -8434,7 +8571,8 @@ function BlockEditor({
8434
8571
  labelForBlockType(block.type)
8435
8572
  ] }) }),
8436
8573
  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,
8437
- dispatch(block, onChange)
8574
+ dispatch(block, onChange),
8575
+ 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
8438
8576
  ] });
8439
8577
  }
8440
8578
  function dispatch(block, onChange) {
@@ -8651,7 +8789,8 @@ function BlockRow({
8651
8789
  {
8652
8790
  block: builderBlock.block,
8653
8791
  errors,
8654
- onChange: (next) => onUpdate(builderBlock.id, next)
8792
+ onChange: (next) => onUpdate(builderBlock.id, next),
8793
+ onDone: () => onOpenChange?.(false)
8655
8794
  }
8656
8795
  ) })
8657
8796
  ] }),
@@ -8915,7 +9054,15 @@ function ContainerRow({
8915
9054
  ]
8916
9055
  }
8917
9056
  ) }),
8918
- /* @__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) }) })
9057
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsx(
9058
+ BlockEditor,
9059
+ {
9060
+ block,
9061
+ errors,
9062
+ onChange: (next) => onUpdate(id, next),
9063
+ onDone: () => onOpenBlockChange?.(null)
9064
+ }
9065
+ ) })
8919
9066
  ] }),
8920
9067
  /* @__PURE__ */ jsx(
8921
9068
  "div",
@@ -9396,6 +9543,10 @@ function formatMessageTs(ts) {
9396
9543
  }
9397
9544
  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";
9398
9545
  function Toolbar2({
9546
+ canUndo = false,
9547
+ canRedo = false,
9548
+ onUndo,
9549
+ onRedo,
9399
9550
  onClear,
9400
9551
  onOpenJson,
9401
9552
  onOpenIssues,
@@ -9414,7 +9565,8 @@ function Toolbar2({
9414
9565
  showSend = true,
9415
9566
  primaryAction,
9416
9567
  sendButtonLabel = "Review & send",
9417
- editingEnabled = false,
9568
+ loadEnabled = false,
9569
+ updateEnabled = false,
9418
9570
  editBadge,
9419
9571
  onOpenLoad,
9420
9572
  onOpenUpdate,
@@ -9434,7 +9586,7 @@ function Toolbar2({
9434
9586
  return /* @__PURE__ */ jsxs(Fragment, { children: [
9435
9587
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-x-2 gap-y-1 border-b bg-background px-2 py-1.5 sm:px-3 sm:py-2", children: [
9436
9588
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 sm:gap-2", children: [
9437
- editingEnabled ? /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9589
+ loadEnabled ? /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9438
9590
  /* @__PURE__ */ jsx(MailSearch, { className: "h-3.5 w-3.5" }),
9439
9591
  /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: loadButtonLabel })
9440
9592
  ] }) : null,
@@ -9508,6 +9660,36 @@ function Toolbar2({
9508
9660
  ) : null
9509
9661
  ] }),
9510
9662
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 sm:gap-2", children: [
9663
+ onUndo && onRedo ? /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
9664
+ /* @__PURE__ */ jsx(
9665
+ Button,
9666
+ {
9667
+ type: "button",
9668
+ variant: "ghost",
9669
+ size: "sm",
9670
+ onClick: onUndo,
9671
+ disabled: !canUndo,
9672
+ "aria-label": "Undo",
9673
+ title: "Undo (Ctrl+Z)",
9674
+ className: "px-2",
9675
+ children: /* @__PURE__ */ jsx(Undo2, { className: "h-3.5 w-3.5" })
9676
+ }
9677
+ ),
9678
+ /* @__PURE__ */ jsx(
9679
+ Button,
9680
+ {
9681
+ type: "button",
9682
+ variant: "ghost",
9683
+ size: "sm",
9684
+ onClick: onRedo,
9685
+ disabled: !canRedo,
9686
+ "aria-label": "Redo",
9687
+ title: "Redo (Ctrl+Shift+Z)",
9688
+ className: "px-2",
9689
+ children: /* @__PURE__ */ jsx(Redo2, { className: "h-3.5 w-3.5" })
9690
+ }
9691
+ )
9692
+ ] }) : null,
9511
9693
  errorCount > 0 ? /* @__PURE__ */ jsxs(
9512
9694
  Button,
9513
9695
  {
@@ -9564,9 +9746,12 @@ function Toolbar2({
9564
9746
  children: /* @__PURE__ */ jsx("span", { children: primaryAction.label })
9565
9747
  }
9566
9748
  )
9567
- ) : null : editBadge ? (
9568
- // A message is loaded: split button. Main action updates it in
9569
- // place; the menu also offers posting the blocks as a new message.
9749
+ ) : null : editBadge && updateEnabled ? (
9750
+ // A message is loaded and update-in-place is wired: split button.
9751
+ // Main action updates it in place; the menu also offers posting
9752
+ // the blocks as a new message. Without `updateEnabled` (loading
9753
+ // configured but no `editing.onUpdate`), the plain send button
9754
+ // below stays — the loaded message can only be posted as new.
9570
9755
  /* @__PURE__ */ jsxs("div", { className: "flex items-stretch", children: [
9571
9756
  /* @__PURE__ */ jsxs(
9572
9757
  Button,
@@ -9639,7 +9824,7 @@ function Toolbar2({
9639
9824
  ] })
9640
9825
  ] })
9641
9826
  ] }),
9642
- editingEnabled && editBadge ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b bg-muted px-2 py-2 text-foreground sm:px-3", children: [
9827
+ loadEnabled && editBadge ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b bg-muted px-2 py-2 text-foreground sm:px-3", children: [
9643
9828
  /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 shrink-0 text-primary" }),
9644
9829
  /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 text-sm", children: [
9645
9830
  "References an existing message in ",
@@ -9802,6 +9987,19 @@ function UpdateDialog({
9802
9987
  ] })
9803
9988
  ] }) });
9804
9989
  }
9990
+ function toLoadedMessage({
9991
+ ok: _ok,
9992
+ blocks: _blocks,
9993
+ ...loadedMessage
9994
+ }) {
9995
+ return loadedMessage;
9996
+ }
9997
+ function loadedMessageKey(message) {
9998
+ return message ? JSON.stringify(message) : null;
9999
+ }
10000
+ function validationSummaryKey(summary) {
10001
+ return JSON.stringify([summary.valid, summary.errorCount, ...summary.errors]);
10002
+ }
9805
10003
  function BlockKitchen(props) {
9806
10004
  const {
9807
10005
  workspaceName,
@@ -9814,7 +10012,8 @@ function BlockKitchen(props) {
9814
10012
  loadSendAsUserStatus,
9815
10013
  onSend,
9816
10014
  renderSendExtras,
9817
- editing,
10015
+ loading,
10016
+ onUpdate: onUpdateProp,
9818
10017
  primaryAction,
9819
10018
  loadButtonLabel,
9820
10019
  updateButtonLabel,
@@ -9858,37 +10057,55 @@ function BlockKitchen(props) {
9858
10057
  const variantById = useMemo(() => buildVariantById(paletteSections), [paletteSections]);
9859
10058
  const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
9860
10059
  const sendEnabled = Boolean(loadChannels && loadSendAsUserStatus && onSend);
9861
- const editingConfig = sendEnabled ? editing : void 0;
9862
- const seededBlocks = editingConfig?.initialTarget?.blocks ?? initialBlocks;
9863
- useEffect(() => {
9864
- if (editingConfig?.initialTarget && initialBlocks) {
9865
- console.warn(
9866
- "[BlockKitchen] Both `initialBlocks` and `editing.initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
9867
- );
9868
- }
9869
- const sendPropsProvided = [loadChannels, loadSendAsUserStatus, onSend].filter(Boolean).length;
9870
- if (sendPropsProvided > 0 && sendPropsProvided < 3) {
9871
- console.warn(
9872
- "[BlockKitchen] Partial send wiring: `loadChannels`, `loadSendAsUserStatus`, and `onSend` are all-or-nothing. The send button is hidden until all three are provided."
9873
- );
9874
- }
9875
- if (editing && !sendEnabled) {
9876
- console.warn(
9877
- "[BlockKitchen] `editing` requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `editing`."
9878
- );
9879
- }
9880
- if (renderSendExtras && !sendEnabled) {
9881
- console.warn(
9882
- "[BlockKitchen] `renderSendExtras` extends the built-in send dialog, which requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `renderSendExtras`."
9883
- );
9884
- }
9885
- if (primaryAction && sendEnabled) {
9886
- console.warn(
9887
- "[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`."
9888
- );
9889
- }
9890
- }, []);
9891
- const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
10060
+ const loadEnabled = Boolean(loading);
10061
+ const recentChannelSource = loading?.loadChannels ?? (sendEnabled ? loadChannels : void 0);
10062
+ const onUpdate = sendEnabled ? onUpdateProp : void 0;
10063
+ const seededBlocks = loading?.initialTarget?.blocks ?? initialBlocks;
10064
+ const sendPropsProvided = [loadChannels, loadSendAsUserStatus, onSend].filter(Boolean).length;
10065
+ useDevWarning(
10066
+ Boolean(loading?.initialTarget && initialBlocks),
10067
+ "[BlockKitchen] Both `initialBlocks` and an `initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
10068
+ );
10069
+ useDevWarning(
10070
+ sendPropsProvided > 0 && sendPropsProvided < 3,
10071
+ "[BlockKitchen] Partial send wiring: `loadChannels`, `loadSendAsUserStatus`, and `onSend` are all-or-nothing. The send button is hidden until all three are provided."
10072
+ );
10073
+ useDevWarning(
10074
+ Boolean(onUpdateProp && !sendEnabled),
10075
+ "[BlockKitchen] `onUpdate` requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `onUpdate`. To load an existing message without the send integration, use the `loading` prop instead."
10076
+ );
10077
+ useDevWarning(
10078
+ Boolean(sendEnabled && onUpdateProp && !loading),
10079
+ "[BlockKitchen] `onUpdate` needs a loaded message, but no load source is configured \u2014 provide the `loading` prop. The update flow is unreachable."
10080
+ );
10081
+ useDevWarning(
10082
+ Boolean(loading?.loadRecentMessages && !recentChannelSource),
10083
+ "[BlockKitchen] `loading.loadRecentMessages` needs a channel list to scope its lookup \u2014 provide `loading.loadChannels` (compose-only mode has no send integration to reuse). Hiding the recent-messages picker."
10084
+ );
10085
+ useDevWarning(
10086
+ Boolean(renderSendExtras && !sendEnabled),
10087
+ "[BlockKitchen] `renderSendExtras` extends the built-in send dialog, which requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `renderSendExtras`."
10088
+ );
10089
+ useDevWarning(
10090
+ Boolean(primaryAction && sendEnabled),
10091
+ "[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`."
10092
+ );
10093
+ const {
10094
+ blocks,
10095
+ addBlock,
10096
+ addChild,
10097
+ updateBlock,
10098
+ removeBlock,
10099
+ duplicateBlock,
10100
+ reorderBlock,
10101
+ moveBlock,
10102
+ replaceAll,
10103
+ resetAll,
10104
+ undo,
10105
+ redo,
10106
+ canUndo,
10107
+ canRedo
10108
+ } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
9892
10109
  const childParentById = useMemo(() => {
9893
10110
  const map = /* @__PURE__ */ new Map();
9894
10111
  for (const top of blocks) {
@@ -9904,8 +10121,11 @@ function BlockKitchen(props) {
9904
10121
  const [sendOpen, setSendOpen] = useState(false);
9905
10122
  const [updateOpen, setUpdateOpen] = useState(false);
9906
10123
  const [loadOpen, setLoadOpen] = useState(false);
9907
- const [editTarget, setEditTarget] = useState(() => editingConfig?.initialTarget ?? null);
9908
- const activeEditTarget = editingConfig ? editTarget : null;
10124
+ const [editTarget, setEditTarget] = useState(
10125
+ () => loading?.initialTarget ? toLoadedMessage(loading.initialTarget) : null
10126
+ );
10127
+ const activeEditTarget = loadEnabled ? editTarget : null;
10128
+ useNotifyOnChange(activeEditTarget, loadedMessageKey, loading?.onLoadedMessageChange);
9909
10129
  const [issuesOpen, setIssuesOpen] = useState(false);
9910
10130
  const [paletteOpen, setPaletteOpen] = useState(false);
9911
10131
  const [openBlockId, setOpenBlockId] = useState(null);
@@ -9927,18 +10147,7 @@ function BlockKitchen(props) {
9927
10147
  () => ({ valid: validation.valid, errorCount: validation.total, errors: validation.errors }),
9928
10148
  [validation]
9929
10149
  );
9930
- const lastNotifiedValidationRef = useRef(null);
9931
- useEffect(() => {
9932
- if (!onValidationChange) {
9933
- return;
9934
- }
9935
- const key = JSON.stringify([validationSummary.valid, validationSummary.errorCount, ...validationSummary.errors]);
9936
- if (lastNotifiedValidationRef.current === key) {
9937
- return;
9938
- }
9939
- lastNotifiedValidationRef.current = key;
9940
- onValidationChange(validationSummary);
9941
- }, [validationSummary, onValidationChange]);
10150
+ useNotifyOnChange(validationSummary, validationSummaryKey, onValidationChange);
9942
10151
  const sensors = useSensors(
9943
10152
  useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
9944
10153
  useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
@@ -10012,6 +10221,30 @@ function BlockKitchen(props) {
10012
10221
  const handleDragCancel = useCallback(() => {
10013
10222
  setActivePaletteVariantId(null);
10014
10223
  }, []);
10224
+ const handleKeyDown = useCallback(
10225
+ (event) => {
10226
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) {
10227
+ return;
10228
+ }
10229
+ const key = event.key.toLowerCase();
10230
+ const isUndo = key === "z" && !event.shiftKey;
10231
+ const isRedo = key === "y" || key === "z" && event.shiftKey;
10232
+ if (!isUndo && !isRedo) {
10233
+ return;
10234
+ }
10235
+ const target = event.target;
10236
+ if (target && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))) {
10237
+ return;
10238
+ }
10239
+ event.preventDefault();
10240
+ if (isRedo) {
10241
+ if (canRedo) redo();
10242
+ } else if (canUndo) {
10243
+ undo();
10244
+ }
10245
+ },
10246
+ [canRedo, canUndo, redo, undo]
10247
+ );
10015
10248
  const activePaletteVariant = activePaletteVariantId ? variantById.get(activePaletteVariantId) : null;
10016
10249
  const blockPayloads = blocks.map((b) => b.block);
10017
10250
  return /* @__PURE__ */ jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsx(CustomEmojiProvider, { customEmojis, children: /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(
@@ -10023,82 +10256,102 @@ function BlockKitchen(props) {
10023
10256
  onDragEnd: handleDragEnd,
10024
10257
  onDragCancel: handleDragCancel,
10025
10258
  children: [
10026
- /* @__PURE__ */ jsxs("div", { className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground", children: [
10027
- /* @__PURE__ */ jsx(
10028
- Toolbar2,
10029
- {
10030
- onClear: () => replaceAll([]),
10031
- onOpenJson: () => setJsonOpen(true),
10032
- onOpenIssues: () => setIssuesOpen(true),
10033
- onOpenSend: () => setSendOpen(true),
10034
- onOpenPalette: () => setPaletteOpen(true),
10035
- canSend: blocks.length > 0,
10036
- canClear: blocks.length > 0,
10037
- errorCount: validation.total,
10038
- previewTheme,
10039
- onPreviewThemeChange: setUncontrolledPreviewTheme,
10040
- previewSurface,
10041
- onPreviewSurfaceChange: setPreviewSurface,
10042
- allowedSurfaces,
10043
- showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
10044
- docsLink,
10045
- showSend: sendEnabled,
10046
- primaryAction: !sendEnabled && primaryAction ? {
10047
- label: primaryAction.label,
10048
- onClick: () => primaryAction.onClick({ blocks: blockPayloads, validation: validationSummary }),
10049
- disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10050
- } : null,
10051
- sendButtonLabel,
10052
- editingEnabled: !!editingConfig,
10053
- editBadge: activeEditTarget ? {
10054
- channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
10055
- ts: activeEditTarget.ts
10056
- } : null,
10057
- onOpenLoad: () => setLoadOpen(true),
10058
- onOpenUpdate: () => setUpdateOpen(true),
10059
- onExitEdit: () => {
10060
- setEditTarget(null);
10061
- replaceAll([]);
10062
- setLoadOpen(true);
10063
- },
10064
- loadButtonLabel,
10065
- updateButtonLabel
10066
- }
10067
- ),
10068
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10069
- /* @__PURE__ */ jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsx(
10070
- Palette,
10071
- {
10072
- onAddBlock: (block) => addBlock(block),
10073
- sections: paletteSections,
10074
- showSearch: showPaletteSearch,
10075
- searchPlaceholder: paletteSearchPlaceholder,
10076
- defaultOpenSections
10077
- }
10078
- ) }),
10079
- /* @__PURE__ */ jsx(
10080
- Surface,
10081
- {
10082
- blocks,
10083
- workspaceName,
10084
- authorName: activeEditTarget?.username,
10085
- authorIcon: activeEditTarget?.iconUrl,
10086
- previewHooks: mergedPreviewHooks,
10087
- previewTheme,
10088
- previewSurface,
10089
- errorsByBlockId: validation.byBlockId,
10090
- openBlockId,
10091
- onOpenBlockChange: setOpenBlockId,
10092
- onUpdate: updateBlock,
10093
- onDuplicate: duplicateBlock,
10094
- onDelete: removeBlock,
10095
- onReorder: reorderBlock,
10096
- isPaletteDrag: activePaletteVariant !== null,
10097
- onOpenPalette: () => setPaletteOpen(true)
10098
- }
10099
- )
10100
- ] })
10101
- ] }),
10259
+ /* @__PURE__ */ jsxs(
10260
+ "div",
10261
+ {
10262
+ className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground",
10263
+ onKeyDown: handleKeyDown,
10264
+ children: [
10265
+ /* @__PURE__ */ jsx(
10266
+ Toolbar2,
10267
+ {
10268
+ canUndo,
10269
+ canRedo,
10270
+ onUndo: undo,
10271
+ onRedo: redo,
10272
+ onClear: () => replaceAll([]),
10273
+ onOpenJson: () => setJsonOpen(true),
10274
+ onOpenIssues: () => setIssuesOpen(true),
10275
+ onOpenSend: () => setSendOpen(true),
10276
+ onOpenPalette: () => setPaletteOpen(true),
10277
+ canSend: blocks.length > 0,
10278
+ canClear: blocks.length > 0,
10279
+ errorCount: validation.total,
10280
+ previewTheme,
10281
+ onPreviewThemeChange: setUncontrolledPreviewTheme,
10282
+ previewSurface,
10283
+ onPreviewSurfaceChange: setPreviewSurface,
10284
+ allowedSurfaces,
10285
+ showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
10286
+ docsLink,
10287
+ showSend: sendEnabled,
10288
+ primaryAction: !sendEnabled && primaryAction ? {
10289
+ label: primaryAction.label,
10290
+ onClick: () => primaryAction.onClick({
10291
+ blocks: blockPayloads,
10292
+ validation: validationSummary,
10293
+ loadedMessage: activeEditTarget
10294
+ }),
10295
+ disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10296
+ } : null,
10297
+ sendButtonLabel,
10298
+ loadEnabled,
10299
+ updateEnabled: Boolean(onUpdate),
10300
+ editBadge: activeEditTarget ? {
10301
+ channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
10302
+ ts: activeEditTarget.ts
10303
+ } : null,
10304
+ onOpenLoad: () => setLoadOpen(true),
10305
+ onOpenUpdate: () => setUpdateOpen(true),
10306
+ onExitEdit: () => {
10307
+ if (sendEnabled) {
10308
+ setEditTarget(null);
10309
+ resetAll([]);
10310
+ setLoadOpen(true);
10311
+ return;
10312
+ }
10313
+ setEditTarget(null);
10314
+ },
10315
+ loadButtonLabel,
10316
+ updateButtonLabel
10317
+ }
10318
+ ),
10319
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10320
+ /* @__PURE__ */ jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsx(
10321
+ Palette,
10322
+ {
10323
+ onAddBlock: (block) => addBlock(block),
10324
+ sections: paletteSections,
10325
+ showSearch: showPaletteSearch,
10326
+ searchPlaceholder: paletteSearchPlaceholder,
10327
+ defaultOpenSections
10328
+ }
10329
+ ) }),
10330
+ /* @__PURE__ */ jsx(
10331
+ Surface,
10332
+ {
10333
+ blocks,
10334
+ workspaceName,
10335
+ authorName: activeEditTarget?.username,
10336
+ authorIcon: activeEditTarget?.iconUrl,
10337
+ previewHooks: mergedPreviewHooks,
10338
+ previewTheme,
10339
+ previewSurface,
10340
+ errorsByBlockId: validation.byBlockId,
10341
+ openBlockId,
10342
+ onOpenBlockChange: setOpenBlockId,
10343
+ onUpdate: updateBlock,
10344
+ onDuplicate: duplicateBlock,
10345
+ onDelete: removeBlock,
10346
+ onReorder: reorderBlock,
10347
+ isPaletteDrag: activePaletteVariant !== null,
10348
+ onOpenPalette: () => setPaletteOpen(true)
10349
+ }
10350
+ )
10351
+ ] })
10352
+ ]
10353
+ }
10354
+ ),
10102
10355
  /* @__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: [
10103
10356
  /* @__PURE__ */ jsx(GripVertical, { className: "h-3 w-3 shrink-0" }),
10104
10357
  /* @__PURE__ */ jsx("span", { className: "truncate", children: activePaletteVariant.label })
@@ -10149,7 +10402,7 @@ function BlockKitchen(props) {
10149
10402
  }
10150
10403
  }
10151
10404
  ) : null,
10152
- activeEditTarget && editingConfig && loadSendAsUserStatus ? /* @__PURE__ */ jsx(
10405
+ activeEditTarget && onUpdate && loadSendAsUserStatus ? /* @__PURE__ */ jsx(
10153
10406
  UpdateDialog,
10154
10407
  {
10155
10408
  open: updateOpen,
@@ -10157,7 +10410,7 @@ function BlockKitchen(props) {
10157
10410
  target: activeEditTarget,
10158
10411
  blocks: blockPayloads,
10159
10412
  loadSendAsUserStatus,
10160
- onUpdate: editingConfig.onUpdate,
10413
+ onUpdate,
10161
10414
  confirmUpdateLabel,
10162
10415
  errorCount: validation.total,
10163
10416
  onShowIssues: () => {
@@ -10166,30 +10419,22 @@ function BlockKitchen(props) {
10166
10419
  }
10167
10420
  }
10168
10421
  ) : null,
10169
- editingConfig && loadChannels ? /* @__PURE__ */ jsx(
10422
+ loading ? /* @__PURE__ */ jsx(
10170
10423
  LoadMessageDialog,
10171
10424
  {
10172
10425
  open: loadOpen,
10173
10426
  onOpenChange: setLoadOpen,
10174
- onLoadMessage: editingConfig.onLoadMessage,
10175
- loadRecentMessages: editingConfig.loadRecentMessages,
10176
- loadChannels,
10427
+ onLoadMessage: loading.onLoadMessage,
10428
+ loadRecentMessages: loading.loadRecentMessages,
10429
+ loadChannels: recentChannelSource,
10177
10430
  onLoaded: (result) => {
10178
- replaceAll(result.blocks);
10179
- setEditTarget({
10180
- channelId: result.channelId,
10181
- channelName: result.channelName,
10182
- ts: result.ts,
10183
- editableVia: result.editableVia,
10184
- workspaceName: result.workspaceName,
10185
- username: result.username,
10186
- iconUrl: result.iconUrl
10187
- });
10431
+ resetAll(result.blocks);
10432
+ setEditTarget(toLoadedMessage(result));
10188
10433
  setLoadOpen(false);
10189
10434
  },
10190
10435
  onOpenAsNew: (loadedBlocks) => {
10191
10436
  if (loadedBlocks) {
10192
- replaceAll(loadedBlocks);
10437
+ resetAll(loadedBlocks);
10193
10438
  }
10194
10439
  setEditTarget(null);
10195
10440
  setLoadOpen(false);