@tightknitai/block-kitchen 0.9.8 → 0.9.10

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
@@ -105,9 +105,12 @@ export function MyBuilderPage() {
105
105
  | `workspaceName` | `string` | no | Shown in the preview chrome to mimic a real Slack message header. |
106
106
  | `initialBlocks` | `SupportedBlock[]` | no | Starting draft. If omitted, the builder starts empty. |
107
107
  | `onChange` | `(blocks: SupportedBlock[]) => void` | no | Fires on every state change. Use this to persist the draft (URL, localStorage, etc). |
108
- | `loadChannels` | `() => Promise<{ id: string; name: string }[]>` | yes | Returns channels available to send to. The package never makes Slack API calls itself. |
109
- | `loadSendAsUserStatus` | `() => Promise<{ canSendAsUser: boolean; oauthUrl?: string }>` | yes | 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. |
110
- | `onSend` | `(payload) => Promise<{ ok: boolean; error?: string }>` | yes | Called when the user submits the send dialog. Payload is `{ channelId, blocks, sendAsUser }`. |
108
+ | `onValidationChange` | `(summary: ValidationSummary) => void` | no | Fires when the draft's validation verdict changes (debounced alongside the builder's own validation pass — not on every keystroke). `summary` is `{ valid, errorCount, errors }`: the exact verdict the issues chip/sheet shows, always scoped to the `message` surface. Lets a host-owned CTA gate on validity in [compose-only mode](#compose-only-mode-bring-your-own-send-flow) without re-running the validator. |
109
+ | `loadChannels` | `() => Promise<{ id: string; name: string }[]>` | grouped\* | Returns channels available to send to. The package never makes Slack API calls itself. |
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
+ | `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
+ | `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). |
111
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. |
112
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
116
  | `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'`. |
@@ -127,6 +130,163 @@ export function MyBuilderPage() {
127
130
  | `confirmSendLabel` | `string` | no | Label for the send dialog's final confirm button. Defaults to `'Send'` (shows `'Sending…'` while in flight). |
128
131
  | `theme` | `BrandTheme \| BrandPreset` | no | Branding tokens applied to the builder chrome (toolbar, palette, popovers, dialogs). Accepts a `Partial<BrandTokens>` map and optional `light`/`dark` overrides. See [Styling](#styling) below. |
129
132
 
133
+ \* The send trio — `loadChannels`, `loadSendAsUserStatus`, `onSend` — is
134
+ all-or-nothing: provide all three for the built-in send flow, or omit all
135
+ three for [compose-only mode](#compose-only-mode-bring-your-own-send-flow)
136
+ (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
+ `primaryAction` requires its absence (the built-in Send/Update flow owns the
139
+ toolbar's primary slot otherwise).
140
+
141
+ ## Compose-only mode (bring your own send flow)
142
+
143
+ Omit the send trio entirely and the builder renders no send button — it
144
+ becomes a pure Block Kit editor. This is the right shape when composing is
145
+ one step in a flow your app owns (a wizard, an audience picker, a scheduler):
146
+ the package owns *composition* (authoring, preview, validation UX) and your
147
+ app owns *distribution* (audiences, identity, scheduling, delivery). The
148
+ only thing that crosses the boundary is the blocks array.
149
+
150
+ Mirror the draft with `onChange`, gate your own CTA with
151
+ `onValidationChange`, and hand `toSlackBlocks(draft)` to whatever comes next:
152
+
153
+ ```tsx
154
+ import { useState } from "react";
155
+ import {
156
+ BlockKitchen,
157
+ toSlackBlocks,
158
+ type SupportedBlock,
159
+ type ValidationSummary,
160
+ } from "@tightknitai/block-kitchen";
161
+
162
+ function ComposeStep({ onNext }: { onNext: (blocks: SupportedBlock[]) => void }) {
163
+ const [draft, setDraft] = useState<SupportedBlock[]>([]);
164
+ const [validation, setValidation] = useState<ValidationSummary | null>(null);
165
+
166
+ return (
167
+ <>
168
+ <BlockKitchen onChange={setDraft} onValidationChange={setValidation} />
169
+ {/* Your CTA, your copy, your placement — gated on the builder's own verdict. */}
170
+ <button
171
+ disabled={draft.length === 0 || validation?.valid === false}
172
+ onClick={() => onNext(toSlackBlocks(draft))}
173
+ >
174
+ Next: choose audience
175
+ </button>
176
+ </>
177
+ );
178
+ }
179
+ ```
180
+
181
+ Notes:
182
+
183
+ - The rest of the toolbar (Clear, View JSON, the issues chip, theme/surface
184
+ controls) is unchanged — users can still inspect problems in the issues
185
+ sheet; only the moment of commitment moves into your app.
186
+ - `onValidationChange` reports the same verdict the issues sheet displays
187
+ (validated against the `message` surface, like Send), so your CTA and the
188
+ 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.
192
+
193
+ ### Keeping the CTA in the toolbar (`primaryAction`)
194
+
195
+ The example above renders its CTA outside the builder. If you want the button
196
+ to sit where "Review & send" normally sits — say a message drafter whose
197
+ primary action is "Save template" — pass `primaryAction`:
198
+
199
+ ```tsx
200
+ <BlockKitchen
201
+ primaryAction={{
202
+ label: "Save template",
203
+ onClick: ({ blocks, validation }) => openSaveModal(blocks, validation),
204
+ // Send-style gating is opt-in. Default is enabled-while-invalid: a
205
+ // drafter usually allows committing a work-in-progress draft and
206
+ // surfaces the verdict in its own flow instead.
207
+ disableWhenInvalid: false,
208
+ }}
209
+ />
210
+ ```
211
+
212
+ `onClick` receives the current draft in the builder's native format (the same
213
+ shape `onChange` reports — persist that to re-open the draft later; run it
214
+ through `toSlackBlocks` for the Slack-ready payload) plus the validation
215
+ verdict `onValidationChange` would report. The button is never disabled for
216
+ an empty draft; gate on `blocks.length` in `onClick` if you need that.
217
+
218
+ ### Building a bespoke send flow from the exported primitives
219
+
220
+ If your custom flow ends in something send-shaped, you don't have to rebuild
221
+ the built-in dialog's machinery — the send-flow primitives are exported:
222
+
223
+ - `SendDialog` — the built-in dialog as a standalone component
224
+ (`SendDialogProps`): channel loading/error states, bot-vs-user identity
225
+ picker, OAuth hand-off, in-flight and failure states. Mount it against your
226
+ own trigger.
227
+ - `useSlackSignIn(loadStatus, { open, enabled })` — the OAuth sign-in state
228
+ machine behind the identity picker: fetches user-token status on open,
229
+ refreshes on window focus, and background-polls after the OAuth tab opens
230
+ until the token appears.
231
+ - `SlackSignInButton` — the "Sign in with Slack" button with its polling
232
+ spinner, for wiring `useSlackSignIn` into your own dialog.
233
+
234
+ ## Extending the send dialog (custom fields)
235
+
236
+ When the built-in send flow is *almost* right — you want the channel picker,
237
+ identity picker, and sending states, plus a few controls of your own (a
238
+ cross-post toggle, a custom sender name) — pass `renderSendExtras` instead of
239
+ rebuilding the dialog. It renders below the built-in fields, and whatever you
240
+ collect via `setExtras` arrives on `onSend` as `payload.extras`:
241
+
242
+ ```tsx
243
+ <BlockKitchen
244
+ loadChannels={loadChannels}
245
+ loadSendAsUserStatus={loadSendAsUserStatus}
246
+ renderSendExtras={({ extras, setExtras }) => (
247
+ <>
248
+ <label>
249
+ <input
250
+ type="checkbox"
251
+ checked={Boolean(extras.alsoPostToIntranet)}
252
+ onChange={(e) => setExtras({ alsoPostToIntranet: e.target.checked })}
253
+ />
254
+ Also publish to the intranet
255
+ </label>
256
+ <label>
257
+ Custom sender name
258
+ <input
259
+ type="text"
260
+ value={String(extras.senderName ?? "")}
261
+ onChange={(e) => setExtras({ senderName: e.target.value })}
262
+ />
263
+ </label>
264
+ </>
265
+ )}
266
+ onSend={async ({ channelId, blocks, sendAsUser, extras }) => {
267
+ await api.send({ channelId, blocks, sendAsUser, ...extras }); // your code
268
+ return { ok: true };
269
+ }}
270
+ />
271
+ ```
272
+
273
+ Notes:
274
+
275
+ - The `extras` object is owned by the dialog: it resets every time the dialog
276
+ opens (alongside the channel and identity pickers), and `setExtras(patch)`
277
+ shallow-merges. Render controlled inputs from `extras`.
278
+ - The context also carries the currently selected `channelId` (`null` until
279
+ channels load) and `sendAsUser`, so extras can react to them — e.g. only
280
+ offer cross-posting for certain channels.
281
+ - `payload.extras` is present iff `renderSendExtras` is wired, so existing
282
+ `onSend` handlers never see a new key.
283
+ - The dialog renders in a portal attached to `document.body`, so CSS that
284
+ relies on ancestor selectors from your app's DOM won't reach slot content —
285
+ style it directly (inline, CSS modules, utility classes).
286
+ - For customizations that generalize something the dialog already models
287
+ (identities, channels), prefer proposing a first-class prop over routing
288
+ them through extras; the slot is for genuinely host-specific concerns.
289
+
130
290
  ## Editing an existing message (opt-in)
131
291
 
132
292
  By default the builder is send-only. Pass `editing` to let users load an
@@ -225,9 +385,9 @@ Variant `id`s must be unique across the array — the drag-drop lookup keys by i
225
385
 
226
386
  ## Boundary
227
387
 
228
- The package is deliberately decoupled from any Slack SDK or backend. It does not import HTTP clients, OAuth libraries, or workspace-state systems. Everything I/O-shaped is brokered through props.
388
+ The package is deliberately decoupled from any Slack SDK or backend. It does not import HTTP clients, OAuth libraries, or workspace-state systems. Everything I/O-shaped is brokered through props — and the send flow itself is optional: see [compose-only mode](#compose-only-mode-bring-your-own-send-flow) when your app owns the moment of commitment.
229
389
 
230
- Helpers also exported:
390
+ Helpers and send-flow primitives also exported:
231
391
 
232
392
  ```ts
233
393
  import {
@@ -235,18 +395,29 @@ import {
235
395
  encodeBlocksToString, // base64url-encode a blocks array (for URL state)
236
396
  decodeBlocksFromString,
237
397
  defaultPalette, // the built-in palette — spread to customize
398
+ SendDialog, // the built-in send dialog, standalone (bespoke send flows)
399
+ useSlackSignIn, // the OAuth sign-in state machine behind the identity picker
400
+ SlackSignInButton, // the "Sign in with Slack" button + polling spinner
238
401
  } from "@tightknitai/block-kitchen";
239
402
 
240
403
  import type {
241
404
  SupportedBlock,
242
405
  SupportedBlockType,
243
406
  BlockKitchenProps,
407
+ BlockKitchenBaseProps, // shared props
408
+ BlockKitchenSendProps, // the send trio + `editing` + `renderSendExtras`
409
+ BlockKitchenComposeOnlyProps, // the trio explicitly absent + `primaryAction`
244
410
  PaletteSection,
245
411
  PaletteVariant,
246
412
  SendPayload,
247
413
  SendResult,
414
+ SendDialogProps,
415
+ SendExtrasContext,
416
+ PrimaryActionConfig,
417
+ PrimaryActionContext,
248
418
  ChannelOption,
249
419
  SendAsUserStatus,
420
+ ValidationSummary,
250
421
  PreviewHooks,
251
422
  } from "@tightknitai/block-kitchen";
252
423
  ```
@@ -1,5 +1,5 @@
1
- import { T as SupportedBlock } from './palette-CKP8CK5Z.cjs';
2
- export { U as SupportedBlockType } from './palette-CKP8CK5Z.cjs';
1
+ import { Z as SupportedBlock } from './palette-Duj901T6.cjs';
2
+ export { _ as SupportedBlockType } from './palette-Duj901T6.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 { T as SupportedBlock } from './palette-CKP8CK5Z.js';
2
- export { U as SupportedBlockType } from './palette-CKP8CK5Z.js';
1
+ import { Z as SupportedBlock } from './palette-Duj901T6.js';
2
+ export { _ as SupportedBlockType } from './palette-Duj901T6.js';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5