@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 +176 -5
- package/dist/helpers.d.cts +2 -2
- package/dist/helpers.d.ts +2 -2
- package/dist/index.cjs +190 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -3
- package/dist/index.d.ts +83 -3
- package/dist/index.js +190 -61
- package/dist/index.js.map +1 -1
- package/dist/{palette-CKP8CK5Z.d.cts → palette-Duj901T6.d.cts} +188 -27
- package/dist/{palette-CKP8CK5Z.d.ts → palette-Duj901T6.d.ts} +188 -27
- package/dist/palette.d.cts +1 -1
- package/dist/palette.d.ts +1 -1
- package/dist/styles.css +2 -2
- package/package.json +1 -1
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
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
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
|
```
|
package/dist/helpers.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
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 {
|
|
2
|
-
export {
|
|
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
|
|