@tightknitai/block-kitchen 0.9.9 → 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 +108 -14
- 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 +108 -17
- 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 +1 -1
- 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
|
|
package/dist/index.cjs
CHANGED
|
@@ -2045,6 +2045,7 @@ function computeValidation(blocks, surface) {
|
|
|
2045
2045
|
const grouped = groupValidatorErrors(result.errors, blocks);
|
|
2046
2046
|
return {
|
|
2047
2047
|
valid: result.valid,
|
|
2048
|
+
errors: result.errors,
|
|
2048
2049
|
byBlockId: grouped.byBlockId,
|
|
2049
2050
|
general: grouped.general,
|
|
2050
2051
|
total: grouped.total
|
|
@@ -3562,6 +3563,7 @@ function SendDialog({
|
|
|
3562
3563
|
loadChannels,
|
|
3563
3564
|
loadSendAsUserStatus,
|
|
3564
3565
|
onSend,
|
|
3566
|
+
renderSendExtras,
|
|
3565
3567
|
confirmSendLabel = "Send",
|
|
3566
3568
|
errorCount,
|
|
3567
3569
|
onShowIssues
|
|
@@ -3571,6 +3573,10 @@ function SendDialog({
|
|
|
3571
3573
|
const [channelId, setChannelId] = React.useState("");
|
|
3572
3574
|
const [sendAs, setSendAs] = React.useState("bot");
|
|
3573
3575
|
const [status, setStatus] = React.useState({ kind: "idle" });
|
|
3576
|
+
const [extras, setExtrasState] = React.useState({});
|
|
3577
|
+
const setExtras = React.useCallback((patch) => {
|
|
3578
|
+
setExtrasState((prev) => ({ ...prev, ...patch }));
|
|
3579
|
+
}, []);
|
|
3574
3580
|
const { userStatus, polling, startSignIn } = useSlackSignIn(loadSendAsUserStatus, { open, enabled: true });
|
|
3575
3581
|
const loadChannelsRef = React.useRef(loadChannels);
|
|
3576
3582
|
React.useEffect(() => {
|
|
@@ -3585,6 +3591,7 @@ function SendDialog({
|
|
|
3585
3591
|
setChannelsError(null);
|
|
3586
3592
|
setChannelId("");
|
|
3587
3593
|
setSendAs("bot");
|
|
3594
|
+
setExtrasState({});
|
|
3588
3595
|
let cancelled = false;
|
|
3589
3596
|
loadChannelsRef.current().then((list) => {
|
|
3590
3597
|
if (cancelled) {
|
|
@@ -3612,7 +3619,10 @@ function SendDialog({
|
|
|
3612
3619
|
const result = await onSend({
|
|
3613
3620
|
channelId,
|
|
3614
3621
|
blocks: toSlackBlocks(blocks),
|
|
3615
|
-
sendAsUser: sendAs === "user"
|
|
3622
|
+
sendAsUser: sendAs === "user",
|
|
3623
|
+
// Only introduce the key when the extras slot is wired, so existing
|
|
3624
|
+
// consumers never see a new payload field.
|
|
3625
|
+
...renderSendExtras ? { extras } : {}
|
|
3616
3626
|
});
|
|
3617
3627
|
if (result.ok) {
|
|
3618
3628
|
setStatus({ kind: "idle" });
|
|
@@ -3679,6 +3689,12 @@ function SendDialog({
|
|
|
3679
3689
|
}
|
|
3680
3690
|
)
|
|
3681
3691
|
] }),
|
|
3692
|
+
renderSendExtras ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1.5", children: renderSendExtras({
|
|
3693
|
+
channelId: channelId || null,
|
|
3694
|
+
sendAsUser: sendAs === "user",
|
|
3695
|
+
extras,
|
|
3696
|
+
setExtras
|
|
3697
|
+
}) }) : null,
|
|
3682
3698
|
errorCount > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3683
3699
|
"button",
|
|
3684
3700
|
{
|
|
@@ -9425,6 +9441,8 @@ function Toolbar2({
|
|
|
9425
9441
|
showThemeControl = true,
|
|
9426
9442
|
docsLink,
|
|
9427
9443
|
errorCount,
|
|
9444
|
+
showSend = true,
|
|
9445
|
+
primaryAction,
|
|
9428
9446
|
sendButtonLabel = "Review & send",
|
|
9429
9447
|
editingEnabled = false,
|
|
9430
9448
|
editBadge,
|
|
@@ -9559,7 +9577,24 @@ function Toolbar2({
|
|
|
9559
9577
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code2, { className: "h-3.5 w-3.5" }),
|
|
9560
9578
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: "View JSON" })
|
|
9561
9579
|
] }),
|
|
9562
|
-
|
|
9580
|
+
!showSend ? primaryAction ? (
|
|
9581
|
+
// Compose-only mode with a host-owned primary action: same
|
|
9582
|
+
// placement and styling as the built-in send button. No icon —
|
|
9583
|
+
// the action's meaning is the host's, so the label always shows
|
|
9584
|
+
// (the send button can collapse to its icon on small screens;
|
|
9585
|
+
// an icon-less button can't).
|
|
9586
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9587
|
+
Button,
|
|
9588
|
+
{
|
|
9589
|
+
type: "button",
|
|
9590
|
+
size: "sm",
|
|
9591
|
+
onClick: primaryAction.onClick,
|
|
9592
|
+
disabled: primaryAction.disabled,
|
|
9593
|
+
"aria-label": primaryAction.label,
|
|
9594
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: primaryAction.label })
|
|
9595
|
+
}
|
|
9596
|
+
)
|
|
9597
|
+
) : null : editBadge ? (
|
|
9563
9598
|
// A message is loaded: split button. Main action updates it in
|
|
9564
9599
|
// place; the menu also offers posting the blocks as a new message.
|
|
9565
9600
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-stretch", children: [
|
|
@@ -9802,12 +9837,15 @@ function BlockKitchen(props) {
|
|
|
9802
9837
|
workspaceName,
|
|
9803
9838
|
initialBlocks,
|
|
9804
9839
|
onChange,
|
|
9840
|
+
onValidationChange,
|
|
9805
9841
|
previewHooks,
|
|
9806
9842
|
customEmojis,
|
|
9807
9843
|
loadChannels,
|
|
9808
9844
|
loadSendAsUserStatus,
|
|
9809
9845
|
onSend,
|
|
9846
|
+
renderSendExtras,
|
|
9810
9847
|
editing,
|
|
9848
|
+
primaryAction,
|
|
9811
9849
|
loadButtonLabel,
|
|
9812
9850
|
updateButtonLabel,
|
|
9813
9851
|
confirmUpdateLabel,
|
|
@@ -9824,6 +9862,13 @@ function BlockKitchen(props) {
|
|
|
9824
9862
|
sendButtonLabel,
|
|
9825
9863
|
confirmSendLabel,
|
|
9826
9864
|
theme
|
|
9865
|
+
// Widen the all-or-nothing union so the branch-specific props
|
|
9866
|
+
// destructure as independent optionals: untyped JS consumers can still
|
|
9867
|
+
// pass partial wiring (handled by the runtime guards below), and the
|
|
9868
|
+
// correlated-union narrowing would otherwise flag those guards as
|
|
9869
|
+
// "always true". `primaryAction` is pulled from the compose-only branch
|
|
9870
|
+
// (where it carries a real type) rather than the send branch's
|
|
9871
|
+
// `undefined` pin, which would collapse the intersection to `undefined`.
|
|
9827
9872
|
} = props;
|
|
9828
9873
|
const paletteSections = React.useMemo(() => {
|
|
9829
9874
|
const sections = palette ?? defaultPalette;
|
|
@@ -9842,13 +9887,36 @@ function BlockKitchen(props) {
|
|
|
9842
9887
|
}, [palette, disabledBlockTypes]);
|
|
9843
9888
|
const variantById = React.useMemo(() => buildVariantById(paletteSections), [paletteSections]);
|
|
9844
9889
|
const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
|
|
9845
|
-
const
|
|
9890
|
+
const sendEnabled = Boolean(loadChannels && loadSendAsUserStatus && onSend);
|
|
9891
|
+
const editingConfig = sendEnabled ? editing : void 0;
|
|
9892
|
+
const seededBlocks = editingConfig?.initialTarget?.blocks ?? initialBlocks;
|
|
9846
9893
|
React.useEffect(() => {
|
|
9847
|
-
if (
|
|
9894
|
+
if (editingConfig?.initialTarget && initialBlocks) {
|
|
9848
9895
|
console.warn(
|
|
9849
9896
|
"[BlockKitchen] Both `initialBlocks` and `editing.initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
|
|
9850
9897
|
);
|
|
9851
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
|
+
}
|
|
9852
9920
|
}, []);
|
|
9853
9921
|
const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
|
|
9854
9922
|
const childParentById = React.useMemo(() => {
|
|
@@ -9866,8 +9934,8 @@ function BlockKitchen(props) {
|
|
|
9866
9934
|
const [sendOpen, setSendOpen] = React.useState(false);
|
|
9867
9935
|
const [updateOpen, setUpdateOpen] = React.useState(false);
|
|
9868
9936
|
const [loadOpen, setLoadOpen] = React.useState(false);
|
|
9869
|
-
const [editTarget, setEditTarget] = React.useState(() =>
|
|
9870
|
-
const activeEditTarget =
|
|
9937
|
+
const [editTarget, setEditTarget] = React.useState(() => editingConfig?.initialTarget ?? null);
|
|
9938
|
+
const activeEditTarget = editingConfig ? editTarget : null;
|
|
9871
9939
|
const [issuesOpen, setIssuesOpen] = React.useState(false);
|
|
9872
9940
|
const [paletteOpen, setPaletteOpen] = React.useState(false);
|
|
9873
9941
|
const [openBlockId, setOpenBlockId] = React.useState(null);
|
|
@@ -9885,6 +9953,22 @@ function BlockKitchen(props) {
|
|
|
9885
9953
|
return { emoji: makeEmojiHook(byName), ...previewHooks };
|
|
9886
9954
|
}, [customEmojis, previewHooks]);
|
|
9887
9955
|
const validation = useBlockKitValidation(blocks, "message");
|
|
9956
|
+
const validationSummary = React.useMemo(
|
|
9957
|
+
() => ({ valid: validation.valid, errorCount: validation.total, errors: validation.errors }),
|
|
9958
|
+
[validation]
|
|
9959
|
+
);
|
|
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]);
|
|
9888
9972
|
const sensors = core.useSensors(
|
|
9889
9973
|
core.useSensor(core.PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
9890
9974
|
core.useSensor(core.TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
|
|
@@ -9988,8 +10072,14 @@ function BlockKitchen(props) {
|
|
|
9988
10072
|
allowedSurfaces,
|
|
9989
10073
|
showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
|
|
9990
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,
|
|
9991
10081
|
sendButtonLabel,
|
|
9992
|
-
editingEnabled: !!
|
|
10082
|
+
editingEnabled: !!editingConfig,
|
|
9993
10083
|
editBadge: activeEditTarget ? {
|
|
9994
10084
|
channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
|
|
9995
10085
|
ts: activeEditTarget.ts
|
|
@@ -10071,7 +10161,7 @@ function BlockKitchen(props) {
|
|
|
10071
10161
|
}
|
|
10072
10162
|
) }),
|
|
10073
10163
|
/* @__PURE__ */ jsxRuntime.jsx(JsonDrawer, { open: jsonOpen, onOpenChange: setJsonOpen, blocks: blockPayloads, onApply: replaceAll }),
|
|
10074
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10164
|
+
loadChannels && loadSendAsUserStatus && onSend ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
10075
10165
|
SendDialog,
|
|
10076
10166
|
{
|
|
10077
10167
|
open: sendOpen,
|
|
@@ -10080,6 +10170,7 @@ function BlockKitchen(props) {
|
|
|
10080
10170
|
loadChannels,
|
|
10081
10171
|
loadSendAsUserStatus,
|
|
10082
10172
|
onSend,
|
|
10173
|
+
renderSendExtras,
|
|
10083
10174
|
confirmSendLabel,
|
|
10084
10175
|
errorCount: validation.total,
|
|
10085
10176
|
onShowIssues: () => {
|
|
@@ -10087,8 +10178,8 @@ function BlockKitchen(props) {
|
|
|
10087
10178
|
setIssuesOpen(true);
|
|
10088
10179
|
}
|
|
10089
10180
|
}
|
|
10090
|
-
),
|
|
10091
|
-
activeEditTarget &&
|
|
10181
|
+
) : null,
|
|
10182
|
+
activeEditTarget && editingConfig && loadSendAsUserStatus ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
10092
10183
|
UpdateDialog,
|
|
10093
10184
|
{
|
|
10094
10185
|
open: updateOpen,
|
|
@@ -10096,7 +10187,7 @@ function BlockKitchen(props) {
|
|
|
10096
10187
|
target: activeEditTarget,
|
|
10097
10188
|
blocks: blockPayloads,
|
|
10098
10189
|
loadSendAsUserStatus,
|
|
10099
|
-
onUpdate:
|
|
10190
|
+
onUpdate: editingConfig.onUpdate,
|
|
10100
10191
|
confirmUpdateLabel,
|
|
10101
10192
|
errorCount: validation.total,
|
|
10102
10193
|
onShowIssues: () => {
|
|
@@ -10105,13 +10196,13 @@ function BlockKitchen(props) {
|
|
|
10105
10196
|
}
|
|
10106
10197
|
}
|
|
10107
10198
|
) : null,
|
|
10108
|
-
|
|
10199
|
+
editingConfig && loadChannels ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
10109
10200
|
LoadMessageDialog,
|
|
10110
10201
|
{
|
|
10111
10202
|
open: loadOpen,
|
|
10112
10203
|
onOpenChange: setLoadOpen,
|
|
10113
|
-
onLoadMessage:
|
|
10114
|
-
loadRecentMessages:
|
|
10204
|
+
onLoadMessage: editingConfig.onLoadMessage,
|
|
10205
|
+
loadRecentMessages: editingConfig.loadRecentMessages,
|
|
10115
10206
|
loadChannels,
|
|
10116
10207
|
onLoaded: (result) => {
|
|
10117
10208
|
replaceAll(result.blocks);
|
|
@@ -10306,6 +10397,8 @@ function fromBase64Url(b64url) {
|
|
|
10306
10397
|
}
|
|
10307
10398
|
|
|
10308
10399
|
exports.BlockKitchen = BlockKitchen;
|
|
10400
|
+
exports.SendDialog = SendDialog;
|
|
10401
|
+
exports.SlackSignInButton = SlackSignInButton;
|
|
10309
10402
|
exports.TemplatePicker = TemplatePicker;
|
|
10310
10403
|
exports.decodeBlocksFromString = decodeBlocksFromString;
|
|
10311
10404
|
exports.defaultPalette = defaultPalette;
|
|
@@ -10313,5 +10406,6 @@ exports.encodeBlocksToString = encodeBlocksToString;
|
|
|
10313
10406
|
exports.extraAlertVariant = extraAlertVariant;
|
|
10314
10407
|
exports.legacyInputVariants = legacyInputVariants;
|
|
10315
10408
|
exports.toSlackBlocks = toSlackBlocks;
|
|
10409
|
+
exports.useSlackSignIn = useSlackSignIn;
|
|
10316
10410
|
//# sourceMappingURL=index.cjs.map
|
|
10317
10411
|
//# sourceMappingURL=index.cjs.map
|