@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/README.md CHANGED
@@ -110,9 +110,10 @@ export function MyBuilderPage() {
110
110
  | `loadSendAsUserStatus` | `() => Promise<{ canSendAsUser: boolean; oauthUrl?: string }>` | grouped\* | Whether the current user has a Slack user-token and can post as themselves. If `canSendAsUser` is false, `oauthUrl` is shown as a "Sign in with Slack" link. |
111
111
  | `onSend` | `(payload) => Promise<{ ok: boolean; error?: string }>` | grouped\* | Called when the user submits the send dialog. Payload is `{ channelId, blocks, sendAsUser, extras? }` (`extras` only when `renderSendExtras` is wired). |
112
112
  | `renderSendExtras` | `(ctx: SendExtrasContext) => ReactNode` | no | Renders host-defined fields inside the built-in send dialog, below the channel and identity pickers. Collect values with `ctx.setExtras(patch)`; they arrive on `onSend` as `payload.extras`. Requires the send trio. See [Extending the send dialog](#extending-the-send-dialog-custom-fields). |
113
- | `primaryAction` | `{ label, onClick, disableWhenInvalid? }` | no | Compose-only mode only: a host-owned button in the toolbar slot where "Review & send" normally sits. `onClick` receives `{ blocks, validation }` — the current draft plus the same verdict `onValidationChange` reports. See [Keeping the CTA in the toolbar](#keeping-the-cta-in-the-toolbar-primaryaction). |
114
- | `editing` | `{ onLoadMessage, onUpdate, loadRecentMessages? }` | no | Opt-in edit mode. When present, the toolbar exposes "Edit message": the user pastes a Slack message link, `onLoadMessage({ link })` returns a host-computed [editability verdict](#editing-an-existing-message-opt-in), and a successful load flips the primary action to "Update message" wired to `onUpdate`. Pass `loadRecentMessages` to add a "recent messages from this app" picker beside the paste input; the user picks a channel first (reusing `loadChannels`) and the lookup is scoped to it. Omit `editing` to keep send-only behavior. |
115
- | `loadButtonLabel` | `string` | no | Label + accessible name for the toolbar button that opens the load-message dialog (the edit-mode entry point). Defaults to `'Load message'`. Only shown when `editing` is set and no message is loaded. |
113
+ | `primaryAction` | `{ label, onClick, disableWhenInvalid? }` | no | Compose-only mode only: a host-owned button in the toolbar slot where "Review & send" normally sits. `onClick` receives `{ blocks, validation, loadedMessage }` — the current draft, the same verdict `onValidationChange` reports, and the currently [loaded message](#loading-an-existing-message-opt-in) (`null` unless `loading` is configured and one is loaded). See [Keeping the CTA in the toolbar](#keeping-the-cta-in-the-toolbar-primaryaction). |
114
+ | `loading` | `{ onLoadMessage, loadRecentMessages?, loadChannels?, initialTarget?, onLoadedMessageChange? }` | no | Opt-in: [load an existing message](#loading-an-existing-message-opt-in) into the editor. Adds a "Find message" toolbar entry — the user pastes a Slack permalink (or picks a recent message) and the draft is hydrated with its blocks. Loading is a *composition* concern, so it works in **both** send mode and compose-only mode. `onLoadedMessageChange` reports the loaded target (`null` on exit) so a host-owned commitment step can stay in sync. |
115
+ | `onUpdate` | `(payload) => Promise<{ ok: boolean; error?: string }>` | no | Opt-in update-in-place mode (send mode only); sibling to `onSend`, but the payload carries the source `channel + ts`. With `loading` configured, a loaded message flips the primary action to a "Review & update" split button wired to it (`chat.update`). |
116
+ | `loadButtonLabel` | `string` | no | Label + accessible name for the toolbar button that opens the load-message dialog. Defaults to `'Find message'`. Only shown when `loading` is configured. |
116
117
  | `updateButtonLabel` | `string` | no | Label for the toolbar's primary button while a message is loaded for editing. It's a split button: clicking it updates the message in place; the menu beside it also offers "Send as a new message" (post the current blocks as new). Defaults to `'Review & update'`. |
117
118
  | `confirmUpdateLabel` | `string` | no | Label for the update dialog's final confirm button. Defaults to `'Update message'` (shows `'Updating…'` while in flight). |
118
119
  | `previewHooks` | `PreviewHooks` | no | Hooks forwarded to `slack-blocks-to-jsx`'s `<Message>` for resolving user / channel / emoji directives. |
@@ -134,9 +135,10 @@ export function MyBuilderPage() {
134
135
  all-or-nothing: provide all three for the built-in send flow, or omit all
135
136
  three for [compose-only mode](#compose-only-mode-bring-your-own-send-flow)
136
137
  (no send button; your app owns the send flow). Wiring only some of the three
137
- is a type error. `editing` and `renderSendExtras` require the trio;
138
+ is a type error. `onUpdate` and `renderSendExtras` require the trio;
138
139
  `primaryAction` requires its absence (the built-in Send/Update flow owns the
139
- toolbar's primary slot otherwise).
140
+ toolbar's primary slot otherwise). `loading` is mode-agnostic — it works
141
+ with or without the trio.
140
142
 
141
143
  ## Compose-only mode (bring your own send flow)
142
144
 
@@ -186,9 +188,15 @@ Notes:
186
188
  - `onValidationChange` reports the same verdict the issues sheet displays
187
189
  (validated against the `message` surface, like Send), so your CTA and the
188
190
  in-builder issue count can never disagree.
189
- - `editing` is unavailable in compose-only mode: updating an existing message
190
- is inherently bound to its channel + timestamp, so the update flow only
191
- exists alongside the send integration.
191
+ - [Loading an existing message](#loading-an-existing-message-opt-in) works
192
+ here too — pass `loading` and the "Find message" entry appears. The loaded
193
+ target (channel + ts + editability verdict) reaches your app via
194
+ `primaryAction`'s `loadedMessage` context and
195
+ `loading.onLoadedMessageChange`, so your own commitment step can offer
196
+ "update in place" alongside "post as new".
197
+ - The built-in *update* flow (`onUpdate`) is unavailable in compose-only
198
+ mode: dispatching a `chat.update` is inherently bound to a channel +
199
+ timestamp + token, so it only exists alongside the send integration.
192
200
 
193
201
  ### Keeping the CTA in the toolbar (`primaryAction`)
194
202
 
@@ -287,17 +295,20 @@ Notes:
287
295
  (identities, channels), prefer proposing a first-class prop over routing
288
296
  them through extras; the slot is for genuinely host-specific concerns.
289
297
 
290
- ## Editing an existing message (opt-in)
298
+ ## Loading an existing message (opt-in)
291
299
 
292
- By default the builder is send-only. Pass `editing` to let users load an
293
- already-posted message, edit its blocks, and dispatch a `chat.update`. The
300
+ By default the builder starts from a blank canvas. Pass `loading` to let
301
+ users load an already-posted message's blocks into the editor — paste a
302
+ permalink, or pick from a "recent messages from this app" list. Loading is a
303
+ *composition* concern, so it works in **both** send mode and
304
+ [compose-only mode](#compose-only-mode-bring-your-own-send-flow). The
294
305
  package stays integration-agnostic: it makes no Slack calls and computes
295
306
  nothing about who can edit; the host does both.
296
307
 
297
308
  ```tsx
298
309
  <BlockKitchen
299
- /* …send-only props… */
300
- editing={{
310
+ /* …send trio, or nothing (compose-only)… */
311
+ loading={{
301
312
  // Host parses the pasted permalink, fetches the message, and returns a
302
313
  // verdict. `chat.update` only edits a message authored by the calling
303
314
  // token, so the host decides: bot message → 'bot', the user's own
@@ -315,17 +326,11 @@ nothing about who can edit; the host does both.
315
326
  return { ok: true, channelId: msg.channel, channelName: msg.channelName, ts: msg.ts, blocks: msg.blocks, editableVia: 'user', ...author };
316
327
  return { ok: false, reason: 'Only messages your app or you posted can be edited.', blocks: msg.blocks };
317
328
  },
318
- // Sibling to onSend; carries the source channel + ts. `asUser` follows
319
- // the verdict's `editableVia`.
320
- onUpdate: async ({ channelId, ts, blocks, asUser }) => {
321
- await chatUpdate({ channel: channelId, ts, blocks, asUser }); // your code
322
- return { ok: true };
323
- },
324
329
  // Optional: adds a "recent messages from this app" picker beside the paste
325
- // input. The user first picks a channel (reusing `loadChannels`), then this
326
- // is called with that `channelId` so the lookup scans only one channel.
327
- // These are editable-by-construction (the app authored them), so picking one
328
- // loads it straight into edit mode (no verdict needed).
330
+ // input. The user first picks a channel, then this is called with that
331
+ // `channelId` so the lookup scans only one channel. These are
332
+ // editable-by-construction (the app authored them), so picking one loads
333
+ // it directly (no verdict needed).
329
334
  loadRecentMessages: async (channelId) => {
330
335
  const msgs = await fetchRecentAppMessages(channelId); // your code
331
336
  return msgs.map((m) => ({
@@ -337,17 +342,68 @@ nothing about who can edit; the host does both.
337
342
  label: m.preview // one-line preview shown in the picker row
338
343
  }));
339
344
  }
345
+ // In send mode the recent picker's channel list reuses the send trio's
346
+ // `loadChannels`. In compose-only mode there is no trio — pass
347
+ // `loadChannels` here too, or the picker is hidden.
340
348
  }}
341
349
  />
342
350
  ```
343
351
 
344
- - On `ok`, the builder hydrates with `blocks`, shows an edit-mode badge, locks
345
- the destination to the source channel, and fixes the post-as identity to
346
- `editableVia` (the `'user'` path reuses `loadSendAsUserStatus` for the "Sign
347
- in with Slack" gate).
352
+ - On `ok`, the builder hydrates with `blocks` and shows a loaded banner
353
+ ("References an existing message in #channel"), with the author's
354
+ name/avatar in the preview header.
348
355
  - On `{ ok: false, reason }`, the load dialog renders the reason inline and
349
356
  offers **Open as a new message instead**. Pass `blocks` on the failure
350
357
  result to hydrate the draft for that fallback.
358
+ - `loading.onLoadedMessageChange` reports the loaded target — and `null`
359
+ when the user exits it — so host state stays in sync (most useful in
360
+ compose-only mode, where your app owns what happens next). It can
361
+ **re-fire for the same `channelId` + `ts`** when the host's verdict or
362
+ metadata changed (e.g. `editableVia` flipping after a Slack sign-in), so
363
+ key host state on the full target, not `ts` alone.
364
+ - The banner's **"Switch to a new message"** exit is mode-aware: in send
365
+ mode it clears the canvas and reopens the load dialog (the edit-centric
366
+ "pick a different message" flow); in compose-only mode it only detaches
367
+ the reference and **keeps the draft** — loading seeded a composition, and
368
+ a misclick must not destroy it.
369
+
370
+ ### Updating it in place (send mode)
371
+
372
+ In send mode, pair `loading` with `onUpdate` to dispatch a `chat.update` for
373
+ the loaded message. The primary action flips to a "Review & update" split
374
+ button (the menu also offers "Send as a new message"), the destination is
375
+ locked to the source channel, and the post-as identity is fixed to the
376
+ verdict's `editableVia` (the `'user'` path reuses `loadSendAsUserStatus` for
377
+ the "Sign in with Slack" gate).
378
+
379
+ ```tsx
380
+ <BlockKitchen
381
+ /* …send trio… */
382
+ loading={{ onLoadMessage }}
383
+ // Sibling to onSend; carries the source channel + ts. `asUser` follows
384
+ // the verdict's `editableVia`.
385
+ onUpdate={async ({ channelId, ts, blocks, asUser }) => {
386
+ await chatUpdate({ channel: channelId, ts, blocks, asUser }); // your code
387
+ return { ok: true };
388
+ }}
389
+ />
390
+ ```
391
+
392
+ With `loading` alone (no `onUpdate`), a loaded message can still be posted
393
+ as a brand-new message via the regular Send flow — there's just no in-place
394
+ update. In compose-only mode `onUpdate` doesn't exist; implement your own
395
+ update against the target from `loadedMessage` / `onLoadedMessageChange`.
396
+
397
+ > **Migrating from the pre-split `editing` bundle (≤ 0.9.x):** the load
398
+ > fields move to `loading` unchanged, and `onUpdate` becomes a top-level
399
+ > prop (the `editing` wrapper is gone).
400
+ >
401
+ > ```tsx
402
+ > // Before
403
+ > <BlockKitchen editing={{ onLoadMessage, onUpdate, loadRecentMessages }} />
404
+ > // After
405
+ > <BlockKitchen loading={{ onLoadMessage, loadRecentMessages }} onUpdate={onUpdate} />
406
+ > ```
351
407
 
352
408
  ## Customizing the palette
353
409
 
@@ -405,7 +461,9 @@ import type {
405
461
  SupportedBlockType,
406
462
  BlockKitchenProps,
407
463
  BlockKitchenBaseProps, // shared props
408
- BlockKitchenSendProps, // the send trio + `editing` + `renderSendExtras`
464
+ BlockKitchenSendProps, // the send trio + `onUpdate` + `renderSendExtras`
465
+ LoadingConfig, // load an existing message (works in both modes)
466
+ LoadedMessage, // the loaded target: channel + ts + verdict
409
467
  BlockKitchenComposeOnlyProps, // the trio explicitly absent + `primaryAction`
410
468
  PaletteSection,
411
469
  PaletteVariant,
@@ -436,6 +494,37 @@ Pass [`customEmojis`](#props) to add your workspace's custom emoji: image entrie
436
494
 
437
495
  Defense-in-depth: blocks are validated against [slack-block-kit-validator](https://github.com/TightknitAI/slack-block-kit-validator) before send. Issues are surfaced in the issues sheet with line numbers — users can fix them inline before posting.
438
496
 
497
+ ## Undo & redo
498
+
499
+ Every edit to the draft is undoable — adding, deleting, duplicating,
500
+ reordering, dragging in and out of containers, editing a block's fields, and
501
+ the toolbar's **Clear** and **View JSON → apply**. The toolbar shows **Undo**
502
+ and **Redo** buttons (disabled when there's nothing to step to), and the
503
+ keyboard shortcuts work whenever focus is inside the builder:
504
+
505
+ | Shortcut | Action |
506
+ |---|---|
507
+ | `Ctrl`/`Cmd` + `Z` | Undo |
508
+ | `Ctrl`/`Cmd` + `Shift` + `Z`, or `Ctrl` + `Y` | Redo |
509
+
510
+ Notes:
511
+
512
+ - **Typing coalesces.** A run of edits to the *same* field collapses into a
513
+ single undo step, so one `Ctrl+Z` rewinds the whole edit rather than one
514
+ character at a time. Editing a different block, or any structural change,
515
+ starts a new step.
516
+ - **Native text undo is preserved.** While a text input, textarea, or the
517
+ inline rich-text editor is focused, `Ctrl/Cmd+Z` performs the browser's
518
+ own per-character undo — the builder only takes over when focus is on the
519
+ canvas or toolbar. The shortcut is scoped to the builder, so it never
520
+ hijacks the host app's own `Ctrl/Cmd+Z`.
521
+ - **Loading is a fresh baseline.** [Loading an existing
522
+ message](#loading-an-existing-message-opt-in) (or "open as new") starts a
523
+ new document with an empty history — undo won't step back across the load
524
+ into the previous draft. **Clear**, by contrast, is undoable.
525
+ - Undo and redo are ordinary draft changes, so they flow through `onChange`
526
+ (and re-run validation) like any other edit.
527
+
439
528
  ## Styling
440
529
 
441
530
  Ships a compiled stylesheet at `@tightknitai/block-kitchen/styles.css`. The styles use CSS custom properties (`--background`, `--primary`, `--border`, etc.) for theming. Consumers must provide values for these vars — the standard shadcn/ui token set works as-is.
@@ -1,5 +1,5 @@
1
- import { Z as SupportedBlock } from './palette-Duj901T6.cjs';
2
- export { _ as SupportedBlockType } from './palette-Duj901T6.cjs';
1
+ import { _ as SupportedBlock } from './palette-C8sPJuXx.cjs';
2
+ export { $ as SupportedBlockType } from './palette-C8sPJuXx.cjs';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
package/dist/helpers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Z as SupportedBlock } from './palette-Duj901T6.js';
2
- export { _ as SupportedBlockType } from './palette-Duj901T6.js';
1
+ import { _ as SupportedBlock } from './palette-C8sPJuXx.js';
2
+ export { $ as SupportedBlockType } from './palette-C8sPJuXx.js';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5