@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 +117 -28
- package/dist/helpers.d.cts +2 -2
- package/dist/helpers.d.ts +2 -2
- package/dist/index.cjs +505 -260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +506 -261
- package/dist/index.js.map +1 -1
- package/dist/{palette-Duj901T6.d.cts → palette-C8sPJuXx.d.cts} +133 -60
- package/dist/{palette-Duj901T6.d.ts → palette-C8sPJuXx.d.ts} +133 -60
- package/dist/palette.d.cts +1 -1
- package/dist/palette.d.ts +1 -1
- package/package.json +2 -2
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
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
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. `
|
|
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
|
-
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
##
|
|
298
|
+
## Loading an existing message (opt-in)
|
|
291
299
|
|
|
292
|
-
By default the builder
|
|
293
|
-
already-posted message
|
|
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
|
|
300
|
-
|
|
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
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
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 + `
|
|
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.
|
package/dist/helpers.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
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 {
|
|
2
|
-
export {
|
|
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
|
|