@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.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,9 +2358,37 @@ 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
  }
2369
+ function useDevWarning(condition, message) {
2370
+ const warnedRef = React.useRef(false);
2371
+ React.useEffect(() => {
2372
+ if (condition && !warnedRef.current) {
2373
+ warnedRef.current = true;
2374
+ console.warn(message);
2375
+ }
2376
+ }, [condition, message]);
2377
+ }
2378
+ function useNotifyOnChange(value, toKey, callback) {
2379
+ const lastNotifiedRef = React.useRef(null);
2380
+ React.useEffect(() => {
2381
+ if (!callback) {
2382
+ return;
2383
+ }
2384
+ const key = toKey(value);
2385
+ if (lastNotifiedRef.current === key) {
2386
+ return;
2387
+ }
2388
+ lastNotifiedRef.current = key;
2389
+ callback(value);
2390
+ }, [value, toKey, callback]);
2391
+ }
2259
2392
 
2260
2393
  // src/lib/brand-theme.ts
2261
2394
  var TOKEN_TO_CSS_VAR = {
@@ -3053,7 +3186,7 @@ function LoadMessageDialog({
3053
3186
  loadRecentMessagesRef.current = loadRecentMessages;
3054
3187
  loadChannelsRef.current = loadChannels;
3055
3188
  });
3056
- const hasRecent = !!loadRecentMessages;
3189
+ const hasRecent = !!loadRecentMessages && !!loadChannels;
3057
3190
  React.useEffect(() => {
3058
3191
  if (!open) {
3059
3192
  return;
@@ -3064,7 +3197,10 @@ function LoadMessageDialog({
3064
3197
  setRecent(null);
3065
3198
  setRecentError(null);
3066
3199
  setSelectedRecent(null);
3067
- if (!loadRecentMessagesRef.current) {
3200
+ }, [open]);
3201
+ React.useEffect(() => {
3202
+ const load = loadChannelsRef.current;
3203
+ if (!open || !hasRecent || !load) {
3068
3204
  setChannels([]);
3069
3205
  setChannelsError(null);
3070
3206
  return;
@@ -3072,7 +3208,7 @@ function LoadMessageDialog({
3072
3208
  setChannels(null);
3073
3209
  setChannelsError(null);
3074
3210
  let cancelled = false;
3075
- loadChannelsRef.current().then((list) => {
3211
+ load().then((list) => {
3076
3212
  if (!cancelled) {
3077
3213
  setChannels(list);
3078
3214
  }
@@ -3084,7 +3220,7 @@ function LoadMessageDialog({
3084
3220
  return () => {
3085
3221
  cancelled = true;
3086
3222
  };
3087
- }, [open]);
3223
+ }, [open, hasRecent]);
3088
3224
  React.useEffect(() => {
3089
3225
  if (!open || !loadRecentMessagesRef.current || !channelId) {
3090
3226
  return;
@@ -8456,7 +8592,8 @@ function VideoEditor({ block, onChange }) {
8456
8592
  function BlockEditor({
8457
8593
  block,
8458
8594
  errors,
8459
- onChange
8595
+ onChange,
8596
+ onDone
8460
8597
  }) {
8461
8598
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
8462
8599
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-sm font-semibold text-foreground", children: [
@@ -8464,7 +8601,8 @@ function BlockEditor({
8464
8601
  labelForBlockType(block.type)
8465
8602
  ] }) }),
8466
8603
  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,
8467
- dispatch(block, onChange)
8604
+ dispatch(block, onChange),
8605
+ 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
8468
8606
  ] });
8469
8607
  }
8470
8608
  function dispatch(block, onChange) {
@@ -8681,7 +8819,8 @@ function BlockRow({
8681
8819
  {
8682
8820
  block: builderBlock.block,
8683
8821
  errors,
8684
- onChange: (next) => onUpdate(builderBlock.id, next)
8822
+ onChange: (next) => onUpdate(builderBlock.id, next),
8823
+ onDone: () => onOpenChange?.(false)
8685
8824
  }
8686
8825
  ) })
8687
8826
  ] }),
@@ -8945,7 +9084,15 @@ function ContainerRow({
8945
9084
  ]
8946
9085
  }
8947
9086
  ) }),
8948
- /* @__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) }) })
9087
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsxRuntime.jsx(
9088
+ BlockEditor,
9089
+ {
9090
+ block,
9091
+ errors,
9092
+ onChange: (next) => onUpdate(id, next),
9093
+ onDone: () => onOpenBlockChange?.(null)
9094
+ }
9095
+ ) })
8949
9096
  ] }),
8950
9097
  /* @__PURE__ */ jsxRuntime.jsx(
8951
9098
  "div",
@@ -9426,6 +9573,10 @@ function formatMessageTs(ts) {
9426
9573
  }
9427
9574
  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";
9428
9575
  function Toolbar2({
9576
+ canUndo = false,
9577
+ canRedo = false,
9578
+ onUndo,
9579
+ onRedo,
9429
9580
  onClear,
9430
9581
  onOpenJson,
9431
9582
  onOpenIssues,
@@ -9444,7 +9595,8 @@ function Toolbar2({
9444
9595
  showSend = true,
9445
9596
  primaryAction,
9446
9597
  sendButtonLabel = "Review & send",
9447
- editingEnabled = false,
9598
+ loadEnabled = false,
9599
+ updateEnabled = false,
9448
9600
  editBadge,
9449
9601
  onOpenLoad,
9450
9602
  onOpenUpdate,
@@ -9464,7 +9616,7 @@ function Toolbar2({
9464
9616
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9465
9617
  /* @__PURE__ */ jsxRuntime.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: [
9466
9618
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 sm:gap-2", children: [
9467
- editingEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9619
+ loadEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9468
9620
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MailSearch, { className: "h-3.5 w-3.5" }),
9469
9621
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: loadButtonLabel })
9470
9622
  ] }) : null,
@@ -9538,6 +9690,36 @@ function Toolbar2({
9538
9690
  ) : null
9539
9691
  ] }),
9540
9692
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 sm:gap-2", children: [
9693
+ onUndo && onRedo ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center", children: [
9694
+ /* @__PURE__ */ jsxRuntime.jsx(
9695
+ Button,
9696
+ {
9697
+ type: "button",
9698
+ variant: "ghost",
9699
+ size: "sm",
9700
+ onClick: onUndo,
9701
+ disabled: !canUndo,
9702
+ "aria-label": "Undo",
9703
+ title: "Undo (Ctrl+Z)",
9704
+ className: "px-2",
9705
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { className: "h-3.5 w-3.5" })
9706
+ }
9707
+ ),
9708
+ /* @__PURE__ */ jsxRuntime.jsx(
9709
+ Button,
9710
+ {
9711
+ type: "button",
9712
+ variant: "ghost",
9713
+ size: "sm",
9714
+ onClick: onRedo,
9715
+ disabled: !canRedo,
9716
+ "aria-label": "Redo",
9717
+ title: "Redo (Ctrl+Shift+Z)",
9718
+ className: "px-2",
9719
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { className: "h-3.5 w-3.5" })
9720
+ }
9721
+ )
9722
+ ] }) : null,
9541
9723
  errorCount > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
9542
9724
  Button,
9543
9725
  {
@@ -9594,9 +9776,12 @@ function Toolbar2({
9594
9776
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: primaryAction.label })
9595
9777
  }
9596
9778
  )
9597
- ) : null : editBadge ? (
9598
- // A message is loaded: split button. Main action updates it in
9599
- // place; the menu also offers posting the blocks as a new message.
9779
+ ) : null : editBadge && updateEnabled ? (
9780
+ // A message is loaded and update-in-place is wired: split button.
9781
+ // Main action updates it in place; the menu also offers posting
9782
+ // the blocks as a new message. Without `updateEnabled` (loading
9783
+ // configured but no `editing.onUpdate`), the plain send button
9784
+ // below stays — the loaded message can only be posted as new.
9600
9785
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-stretch", children: [
9601
9786
  /* @__PURE__ */ jsxRuntime.jsxs(
9602
9787
  Button,
@@ -9669,7 +9854,7 @@ function Toolbar2({
9669
9854
  ] })
9670
9855
  ] })
9671
9856
  ] }),
9672
- editingEnabled && editBadge ? /* @__PURE__ */ jsxRuntime.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: [
9857
+ loadEnabled && editBadge ? /* @__PURE__ */ jsxRuntime.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: [
9673
9858
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-4 w-4 shrink-0 text-primary" }),
9674
9859
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1 text-sm", children: [
9675
9860
  "References an existing message in ",
@@ -9832,6 +10017,19 @@ function UpdateDialog({
9832
10017
  ] })
9833
10018
  ] }) });
9834
10019
  }
10020
+ function toLoadedMessage({
10021
+ ok: _ok,
10022
+ blocks: _blocks,
10023
+ ...loadedMessage
10024
+ }) {
10025
+ return loadedMessage;
10026
+ }
10027
+ function loadedMessageKey(message) {
10028
+ return message ? JSON.stringify(message) : null;
10029
+ }
10030
+ function validationSummaryKey(summary) {
10031
+ return JSON.stringify([summary.valid, summary.errorCount, ...summary.errors]);
10032
+ }
9835
10033
  function BlockKitchen(props) {
9836
10034
  const {
9837
10035
  workspaceName,
@@ -9844,7 +10042,8 @@ function BlockKitchen(props) {
9844
10042
  loadSendAsUserStatus,
9845
10043
  onSend,
9846
10044
  renderSendExtras,
9847
- editing,
10045
+ loading,
10046
+ onUpdate: onUpdateProp,
9848
10047
  primaryAction,
9849
10048
  loadButtonLabel,
9850
10049
  updateButtonLabel,
@@ -9888,37 +10087,55 @@ function BlockKitchen(props) {
9888
10087
  const variantById = React.useMemo(() => buildVariantById(paletteSections), [paletteSections]);
9889
10088
  const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
9890
10089
  const sendEnabled = Boolean(loadChannels && loadSendAsUserStatus && onSend);
9891
- const editingConfig = sendEnabled ? editing : void 0;
9892
- const seededBlocks = editingConfig?.initialTarget?.blocks ?? initialBlocks;
9893
- React.useEffect(() => {
9894
- if (editingConfig?.initialTarget && initialBlocks) {
9895
- console.warn(
9896
- "[BlockKitchen] Both `initialBlocks` and `editing.initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
9897
- );
9898
- }
9899
- const sendPropsProvided = [loadChannels, loadSendAsUserStatus, onSend].filter(Boolean).length;
9900
- if (sendPropsProvided > 0 && sendPropsProvided < 3) {
9901
- console.warn(
9902
- "[BlockKitchen] Partial send wiring: `loadChannels`, `loadSendAsUserStatus`, and `onSend` are all-or-nothing. The send button is hidden until all three are provided."
9903
- );
9904
- }
9905
- if (editing && !sendEnabled) {
9906
- console.warn(
9907
- "[BlockKitchen] `editing` requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `editing`."
9908
- );
9909
- }
9910
- if (renderSendExtras && !sendEnabled) {
9911
- console.warn(
9912
- "[BlockKitchen] `renderSendExtras` extends the built-in send dialog, which requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `renderSendExtras`."
9913
- );
9914
- }
9915
- if (primaryAction && sendEnabled) {
9916
- console.warn(
9917
- "[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`."
9918
- );
9919
- }
9920
- }, []);
9921
- const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
10090
+ const loadEnabled = Boolean(loading);
10091
+ const recentChannelSource = loading?.loadChannels ?? (sendEnabled ? loadChannels : void 0);
10092
+ const onUpdate = sendEnabled ? onUpdateProp : void 0;
10093
+ const seededBlocks = loading?.initialTarget?.blocks ?? initialBlocks;
10094
+ const sendPropsProvided = [loadChannels, loadSendAsUserStatus, onSend].filter(Boolean).length;
10095
+ useDevWarning(
10096
+ Boolean(loading?.initialTarget && initialBlocks),
10097
+ "[BlockKitchen] Both `initialBlocks` and an `initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
10098
+ );
10099
+ useDevWarning(
10100
+ sendPropsProvided > 0 && sendPropsProvided < 3,
10101
+ "[BlockKitchen] Partial send wiring: `loadChannels`, `loadSendAsUserStatus`, and `onSend` are all-or-nothing. The send button is hidden until all three are provided."
10102
+ );
10103
+ useDevWarning(
10104
+ Boolean(onUpdateProp && !sendEnabled),
10105
+ "[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."
10106
+ );
10107
+ useDevWarning(
10108
+ Boolean(sendEnabled && onUpdateProp && !loading),
10109
+ "[BlockKitchen] `onUpdate` needs a loaded message, but no load source is configured \u2014 provide the `loading` prop. The update flow is unreachable."
10110
+ );
10111
+ useDevWarning(
10112
+ Boolean(loading?.loadRecentMessages && !recentChannelSource),
10113
+ "[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."
10114
+ );
10115
+ useDevWarning(
10116
+ Boolean(renderSendExtras && !sendEnabled),
10117
+ "[BlockKitchen] `renderSendExtras` extends the built-in send dialog, which requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `renderSendExtras`."
10118
+ );
10119
+ useDevWarning(
10120
+ Boolean(primaryAction && sendEnabled),
10121
+ "[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`."
10122
+ );
10123
+ const {
10124
+ blocks,
10125
+ addBlock,
10126
+ addChild,
10127
+ updateBlock,
10128
+ removeBlock,
10129
+ duplicateBlock,
10130
+ reorderBlock,
10131
+ moveBlock,
10132
+ replaceAll,
10133
+ resetAll,
10134
+ undo,
10135
+ redo,
10136
+ canUndo,
10137
+ canRedo
10138
+ } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
9922
10139
  const childParentById = React.useMemo(() => {
9923
10140
  const map = /* @__PURE__ */ new Map();
9924
10141
  for (const top of blocks) {
@@ -9934,8 +10151,11 @@ function BlockKitchen(props) {
9934
10151
  const [sendOpen, setSendOpen] = React.useState(false);
9935
10152
  const [updateOpen, setUpdateOpen] = React.useState(false);
9936
10153
  const [loadOpen, setLoadOpen] = React.useState(false);
9937
- const [editTarget, setEditTarget] = React.useState(() => editingConfig?.initialTarget ?? null);
9938
- const activeEditTarget = editingConfig ? editTarget : null;
10154
+ const [editTarget, setEditTarget] = React.useState(
10155
+ () => loading?.initialTarget ? toLoadedMessage(loading.initialTarget) : null
10156
+ );
10157
+ const activeEditTarget = loadEnabled ? editTarget : null;
10158
+ useNotifyOnChange(activeEditTarget, loadedMessageKey, loading?.onLoadedMessageChange);
9939
10159
  const [issuesOpen, setIssuesOpen] = React.useState(false);
9940
10160
  const [paletteOpen, setPaletteOpen] = React.useState(false);
9941
10161
  const [openBlockId, setOpenBlockId] = React.useState(null);
@@ -9957,18 +10177,7 @@ function BlockKitchen(props) {
9957
10177
  () => ({ valid: validation.valid, errorCount: validation.total, errors: validation.errors }),
9958
10178
  [validation]
9959
10179
  );
9960
- const lastNotifiedValidationRef = React.useRef(null);
9961
- React.useEffect(() => {
9962
- if (!onValidationChange) {
9963
- return;
9964
- }
9965
- const key = JSON.stringify([validationSummary.valid, validationSummary.errorCount, ...validationSummary.errors]);
9966
- if (lastNotifiedValidationRef.current === key) {
9967
- return;
9968
- }
9969
- lastNotifiedValidationRef.current = key;
9970
- onValidationChange(validationSummary);
9971
- }, [validationSummary, onValidationChange]);
10180
+ useNotifyOnChange(validationSummary, validationSummaryKey, onValidationChange);
9972
10181
  const sensors = core.useSensors(
9973
10182
  core.useSensor(core.PointerSensor, { activationConstraint: { distance: 4 } }),
9974
10183
  core.useSensor(core.TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
@@ -10042,6 +10251,30 @@ function BlockKitchen(props) {
10042
10251
  const handleDragCancel = React.useCallback(() => {
10043
10252
  setActivePaletteVariantId(null);
10044
10253
  }, []);
10254
+ const handleKeyDown = React.useCallback(
10255
+ (event) => {
10256
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) {
10257
+ return;
10258
+ }
10259
+ const key = event.key.toLowerCase();
10260
+ const isUndo = key === "z" && !event.shiftKey;
10261
+ const isRedo = key === "y" || key === "z" && event.shiftKey;
10262
+ if (!isUndo && !isRedo) {
10263
+ return;
10264
+ }
10265
+ const target = event.target;
10266
+ if (target && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))) {
10267
+ return;
10268
+ }
10269
+ event.preventDefault();
10270
+ if (isRedo) {
10271
+ if (canRedo) redo();
10272
+ } else if (canUndo) {
10273
+ undo();
10274
+ }
10275
+ },
10276
+ [canRedo, canUndo, redo, undo]
10277
+ );
10045
10278
  const activePaletteVariant = activePaletteVariantId ? variantById.get(activePaletteVariantId) : null;
10046
10279
  const blockPayloads = blocks.map((b) => b.block);
10047
10280
  return /* @__PURE__ */ jsxRuntime.jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsxRuntime.jsx(CustomEmojiProvider, { customEmojis, children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -10053,82 +10286,102 @@ function BlockKitchen(props) {
10053
10286
  onDragEnd: handleDragEnd,
10054
10287
  onDragCancel: handleDragCancel,
10055
10288
  children: [
10056
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground", children: [
10057
- /* @__PURE__ */ jsxRuntime.jsx(
10058
- Toolbar2,
10059
- {
10060
- onClear: () => replaceAll([]),
10061
- onOpenJson: () => setJsonOpen(true),
10062
- onOpenIssues: () => setIssuesOpen(true),
10063
- onOpenSend: () => setSendOpen(true),
10064
- onOpenPalette: () => setPaletteOpen(true),
10065
- canSend: blocks.length > 0,
10066
- canClear: blocks.length > 0,
10067
- errorCount: validation.total,
10068
- previewTheme,
10069
- onPreviewThemeChange: setUncontrolledPreviewTheme,
10070
- previewSurface,
10071
- onPreviewSurfaceChange: setPreviewSurface,
10072
- allowedSurfaces,
10073
- showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
10074
- docsLink,
10075
- showSend: sendEnabled,
10076
- primaryAction: !sendEnabled && primaryAction ? {
10077
- label: primaryAction.label,
10078
- onClick: () => primaryAction.onClick({ blocks: blockPayloads, validation: validationSummary }),
10079
- disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10080
- } : null,
10081
- sendButtonLabel,
10082
- editingEnabled: !!editingConfig,
10083
- editBadge: activeEditTarget ? {
10084
- channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
10085
- ts: activeEditTarget.ts
10086
- } : null,
10087
- onOpenLoad: () => setLoadOpen(true),
10088
- onOpenUpdate: () => setUpdateOpen(true),
10089
- onExitEdit: () => {
10090
- setEditTarget(null);
10091
- replaceAll([]);
10092
- setLoadOpen(true);
10093
- },
10094
- loadButtonLabel,
10095
- updateButtonLabel
10096
- }
10097
- ),
10098
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10099
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(
10100
- Palette,
10101
- {
10102
- onAddBlock: (block) => addBlock(block),
10103
- sections: paletteSections,
10104
- showSearch: showPaletteSearch,
10105
- searchPlaceholder: paletteSearchPlaceholder,
10106
- defaultOpenSections
10107
- }
10108
- ) }),
10109
- /* @__PURE__ */ jsxRuntime.jsx(
10110
- Surface,
10111
- {
10112
- blocks,
10113
- workspaceName,
10114
- authorName: activeEditTarget?.username,
10115
- authorIcon: activeEditTarget?.iconUrl,
10116
- previewHooks: mergedPreviewHooks,
10117
- previewTheme,
10118
- previewSurface,
10119
- errorsByBlockId: validation.byBlockId,
10120
- openBlockId,
10121
- onOpenBlockChange: setOpenBlockId,
10122
- onUpdate: updateBlock,
10123
- onDuplicate: duplicateBlock,
10124
- onDelete: removeBlock,
10125
- onReorder: reorderBlock,
10126
- isPaletteDrag: activePaletteVariant !== null,
10127
- onOpenPalette: () => setPaletteOpen(true)
10128
- }
10129
- )
10130
- ] })
10131
- ] }),
10289
+ /* @__PURE__ */ jsxRuntime.jsxs(
10290
+ "div",
10291
+ {
10292
+ className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground",
10293
+ onKeyDown: handleKeyDown,
10294
+ children: [
10295
+ /* @__PURE__ */ jsxRuntime.jsx(
10296
+ Toolbar2,
10297
+ {
10298
+ canUndo,
10299
+ canRedo,
10300
+ onUndo: undo,
10301
+ onRedo: redo,
10302
+ onClear: () => replaceAll([]),
10303
+ onOpenJson: () => setJsonOpen(true),
10304
+ onOpenIssues: () => setIssuesOpen(true),
10305
+ onOpenSend: () => setSendOpen(true),
10306
+ onOpenPalette: () => setPaletteOpen(true),
10307
+ canSend: blocks.length > 0,
10308
+ canClear: blocks.length > 0,
10309
+ errorCount: validation.total,
10310
+ previewTheme,
10311
+ onPreviewThemeChange: setUncontrolledPreviewTheme,
10312
+ previewSurface,
10313
+ onPreviewSurfaceChange: setPreviewSurface,
10314
+ allowedSurfaces,
10315
+ showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
10316
+ docsLink,
10317
+ showSend: sendEnabled,
10318
+ primaryAction: !sendEnabled && primaryAction ? {
10319
+ label: primaryAction.label,
10320
+ onClick: () => primaryAction.onClick({
10321
+ blocks: blockPayloads,
10322
+ validation: validationSummary,
10323
+ loadedMessage: activeEditTarget
10324
+ }),
10325
+ disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10326
+ } : null,
10327
+ sendButtonLabel,
10328
+ loadEnabled,
10329
+ updateEnabled: Boolean(onUpdate),
10330
+ editBadge: activeEditTarget ? {
10331
+ channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
10332
+ ts: activeEditTarget.ts
10333
+ } : null,
10334
+ onOpenLoad: () => setLoadOpen(true),
10335
+ onOpenUpdate: () => setUpdateOpen(true),
10336
+ onExitEdit: () => {
10337
+ if (sendEnabled) {
10338
+ setEditTarget(null);
10339
+ resetAll([]);
10340
+ setLoadOpen(true);
10341
+ return;
10342
+ }
10343
+ setEditTarget(null);
10344
+ },
10345
+ loadButtonLabel,
10346
+ updateButtonLabel
10347
+ }
10348
+ ),
10349
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10350
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(
10351
+ Palette,
10352
+ {
10353
+ onAddBlock: (block) => addBlock(block),
10354
+ sections: paletteSections,
10355
+ showSearch: showPaletteSearch,
10356
+ searchPlaceholder: paletteSearchPlaceholder,
10357
+ defaultOpenSections
10358
+ }
10359
+ ) }),
10360
+ /* @__PURE__ */ jsxRuntime.jsx(
10361
+ Surface,
10362
+ {
10363
+ blocks,
10364
+ workspaceName,
10365
+ authorName: activeEditTarget?.username,
10366
+ authorIcon: activeEditTarget?.iconUrl,
10367
+ previewHooks: mergedPreviewHooks,
10368
+ previewTheme,
10369
+ previewSurface,
10370
+ errorsByBlockId: validation.byBlockId,
10371
+ openBlockId,
10372
+ onOpenBlockChange: setOpenBlockId,
10373
+ onUpdate: updateBlock,
10374
+ onDuplicate: duplicateBlock,
10375
+ onDelete: removeBlock,
10376
+ onReorder: reorderBlock,
10377
+ isPaletteDrag: activePaletteVariant !== null,
10378
+ onOpenPalette: () => setPaletteOpen(true)
10379
+ }
10380
+ )
10381
+ ] })
10382
+ ]
10383
+ }
10384
+ ),
10132
10385
  /* @__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: [
10133
10386
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVertical, { className: "h-3 w-3 shrink-0" }),
10134
10387
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: activePaletteVariant.label })
@@ -10179,7 +10432,7 @@ function BlockKitchen(props) {
10179
10432
  }
10180
10433
  }
10181
10434
  ) : null,
10182
- activeEditTarget && editingConfig && loadSendAsUserStatus ? /* @__PURE__ */ jsxRuntime.jsx(
10435
+ activeEditTarget && onUpdate && loadSendAsUserStatus ? /* @__PURE__ */ jsxRuntime.jsx(
10183
10436
  UpdateDialog,
10184
10437
  {
10185
10438
  open: updateOpen,
@@ -10187,7 +10440,7 @@ function BlockKitchen(props) {
10187
10440
  target: activeEditTarget,
10188
10441
  blocks: blockPayloads,
10189
10442
  loadSendAsUserStatus,
10190
- onUpdate: editingConfig.onUpdate,
10443
+ onUpdate,
10191
10444
  confirmUpdateLabel,
10192
10445
  errorCount: validation.total,
10193
10446
  onShowIssues: () => {
@@ -10196,30 +10449,22 @@ function BlockKitchen(props) {
10196
10449
  }
10197
10450
  }
10198
10451
  ) : null,
10199
- editingConfig && loadChannels ? /* @__PURE__ */ jsxRuntime.jsx(
10452
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(
10200
10453
  LoadMessageDialog,
10201
10454
  {
10202
10455
  open: loadOpen,
10203
10456
  onOpenChange: setLoadOpen,
10204
- onLoadMessage: editingConfig.onLoadMessage,
10205
- loadRecentMessages: editingConfig.loadRecentMessages,
10206
- loadChannels,
10457
+ onLoadMessage: loading.onLoadMessage,
10458
+ loadRecentMessages: loading.loadRecentMessages,
10459
+ loadChannels: recentChannelSource,
10207
10460
  onLoaded: (result) => {
10208
- replaceAll(result.blocks);
10209
- setEditTarget({
10210
- channelId: result.channelId,
10211
- channelName: result.channelName,
10212
- ts: result.ts,
10213
- editableVia: result.editableVia,
10214
- workspaceName: result.workspaceName,
10215
- username: result.username,
10216
- iconUrl: result.iconUrl
10217
- });
10461
+ resetAll(result.blocks);
10462
+ setEditTarget(toLoadedMessage(result));
10218
10463
  setLoadOpen(false);
10219
10464
  },
10220
10465
  onOpenAsNew: (loadedBlocks) => {
10221
10466
  if (loadedBlocks) {
10222
- replaceAll(loadedBlocks);
10467
+ resetAll(loadedBlocks);
10223
10468
  }
10224
10469
  setEditTarget(null);
10225
10470
  setLoadOpen(false);