@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 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
 
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
- editBadge ? (
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 seededBlocks = editing?.initialTarget?.blocks ?? initialBlocks;
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 (editing?.initialTarget && initialBlocks) {
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(() => editing?.initialTarget ?? null);
9870
- const activeEditTarget = editing ? editTarget : null;
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: !!editing,
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 && editing ? /* @__PURE__ */ jsxRuntime.jsx(
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: editing.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
- editing ? /* @__PURE__ */ jsxRuntime.jsx(
10199
+ editingConfig && loadChannels ? /* @__PURE__ */ jsxRuntime.jsx(
10109
10200
  LoadMessageDialog,
10110
10201
  {
10111
10202
  open: loadOpen,
10112
10203
  onOpenChange: setLoadOpen,
10113
- onLoadMessage: editing.onLoadMessage,
10114
- loadRecentMessages: editing.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