@tightknitai/block-kitchen 0.9.9 → 0.10.0

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/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as react from 'react';
2
- import { B as BlockKitchenProps, $ as Template, K as PreviewSurface, N as PreviewTheme } from './palette-CKP8CK5Z.cjs';
3
- export { A as AlertBlock, a as AlertLevel, b as BrandPreset, c as BrandTheme, d as BrandTokens, e as BuilderBlock, C as CardBlock, f as CarouselBlock, g as CartesianChart, h as ChannelOption, i as Chart, j as ChartAxisConfig, k as ChartDataPoint, l as ChartSeries, m as ChartType, n as ContainerBlock, o as ContainerChildBlock, p as ContextActionsBlock, q as ContextActionsElement, r as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, s as EditingConfig, F as FeedbackButtonSubobject, t as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, u as IconButtonIcon, v as InputBlock, L as LoadMessageInput, w as LoadResult, M as MarkdownBlock, P as PaletteSection, x as PaletteVariant, y as PieChart, z as PieChartSegment, G as PlanBlock, J as PreviewHooks, R as RecentMessage, S as SendAsUserStatus, O as SendPayload, Q as SendResult, T as SupportedBlock, U as SupportedBlockType, V as SupportedHeaderBlock, W as TableBlock, X as TableCell, Y as TableColumnSetting, Z as TaskCardBlock, _ as TaskCardStatus, a0 as UpdatePayload, a1 as UpdateResult, a2 as UrlSourceElement, a3 as VideoBlock, a5 as defaultPalette, a6 as extraAlertVariant, a7 as legacyInputVariants } from './palette-CKP8CK5Z.cjs';
2
+ import { ReactNode } from 'react';
3
+ import { c as BlockKitchenProps, _ as SupportedBlock, k as ChannelOption, W as SendAsUserStatus, Y as SendPayload, X as SendExtrasContext, a6 as Template, R as PreviewSurface, S as PreviewTheme } from './palette-C8sPJuXx.cjs';
4
+ export { A as AlertBlock, a as AlertLevel, B as BlockKitchenBaseProps, b as BlockKitchenComposeOnlyProps, d as BlockKitchenSendProps, e as BrandPreset, f as BrandTheme, g as BrandTokens, h as BuilderBlock, C as CardBlock, i as CarouselBlock, j as CartesianChart, l as Chart, m as ChartAxisConfig, n as ChartDataPoint, o as ChartSeries, p as ChartType, q as ContainerBlock, r as ContainerChildBlock, s as ContextActionsBlock, t as ContextActionsElement, u as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, F as FeedbackButtonSubobject, v as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, w as IconButtonIcon, x as InputBlock, L as LoadMessageInput, y as LoadResult, z as LoadedMessage, G as LoadingConfig, M as MarkdownBlock, P as PaletteSection, J as PaletteVariant, K as PieChart, N as PieChartSegment, O as PlanBlock, Q as PreviewHooks, T as PrimaryActionConfig, U as PrimaryActionContext, V as RecentMessage, Z as SendResult, $ as SupportedBlockType, a0 as SupportedHeaderBlock, a1 as TableBlock, a2 as TableCell, a3 as TableColumnSetting, a4 as TaskCardBlock, a5 as TaskCardStatus, a7 as UpdatePayload, a8 as UpdateResult, a9 as UrlSourceElement, aa as ValidationSummary, ab as VideoBlock, ad as defaultPalette, ae as extraAlertVariant, af as legacyInputVariants } from './palette-C8sPJuXx.cjs';
4
5
  export { decodeBlocksFromString, encodeBlocksToString, toSlackBlocks } from './helpers.cjs';
5
6
  import 'slack-web-api-client';
6
7
 
@@ -14,6 +15,85 @@ import 'slack-web-api-client';
14
15
  */
15
16
  declare function BlockKitchen(props: BlockKitchenProps): react.JSX.Element;
16
17
 
18
+ /**
19
+ * Props for {@link SendDialog}. Exported so hosts building a bespoke send
20
+ * flow (compose-only mode) can reuse the built-in dialog standalone instead
21
+ * of rebuilding channel loading, identity, and sending states from scratch.
22
+ */
23
+ interface SendDialogProps {
24
+ /** Whether the dialog is open. */
25
+ open: boolean;
26
+ /** Notified when the user closes the dialog. */
27
+ onOpenChange: (open: boolean) => void;
28
+ /** The draft blocks to send (builder-native format). */
29
+ blocks: SupportedBlock[];
30
+ /** Returns channels available to send to. Called when the dialog opens. */
31
+ loadChannels: () => Promise<ChannelOption[]>;
32
+ /** Returns user-token status + OAuth URL. */
33
+ loadSendAsUserStatus: () => Promise<SendAsUserStatus>;
34
+ /** Terminal action; should return `{ ok }` or `{ ok: false, error }`. */
35
+ onSend: (payload: SendPayload) => Promise<{
36
+ ok: boolean;
37
+ error?: string;
38
+ }>;
39
+ /**
40
+ * Renders host-defined fields below the built-in pickers; collected
41
+ * values are delivered on {@link SendPayload.extras}.
42
+ */
43
+ renderSendExtras?: (context: SendExtrasContext) => ReactNode;
44
+ /** Label for the final confirm button. Defaults to `'Send'`. */
45
+ confirmSendLabel?: string;
46
+ /** Total validation errors against the current draft. */
47
+ errorCount: number;
48
+ /** Asks the parent to open the global issues panel. */
49
+ onShowIssues?: () => void;
50
+ }
51
+ /**
52
+ * Modal dialog that collects the destination channel + send-as identity
53
+ * (plus any host-defined extras), then calls the consumer's `onSend`.
54
+ *
55
+ * Channels and user-token status are loaded async via callback props on open.
56
+ * The consumer brokers all I/O; the dialog never makes a network call.
57
+ * @param props - {@link SendDialogProps}
58
+ * @returns the rendered send dialog
59
+ */
60
+ declare function SendDialog({ open, onOpenChange, blocks, loadChannels, loadSendAsUserStatus, onSend, renderSendExtras, confirmSendLabel, errorCount, onShowIssues }: SendDialogProps): react.JSX.Element;
61
+
62
+ /**
63
+ * Tracks the "act as the user" Slack token status for a dialog and drives the
64
+ * OAuth sign-in flow. Shared by {@link SendDialog} and {@link UpdateDialog}.
65
+ *
66
+ * While `open && enabled` it fetches status on open and refreshes on window
67
+ * focus. Once `startSignIn` opens the OAuth tab it also background-polls until
68
+ * the token appears or the retry cap is hit, so the dialog unlocks itself
69
+ * without a manual reload. All work stops when the dialog closes, the token
70
+ * path doesn't apply, or the component unmounts.
71
+ *
72
+ * @param loadStatus - consumer callback returning user-token status + OAuth URL
73
+ * @param options.open - whether the host dialog is open
74
+ * @param options.enabled - whether the user-token path applies (idle when false)
75
+ * @returns `{ userStatus, polling, startSignIn }`
76
+ */
77
+ declare function useSlackSignIn(loadStatus: () => Promise<SendAsUserStatus>, { open, enabled }: {
78
+ open: boolean;
79
+ enabled: boolean;
80
+ }): {
81
+ userStatus: SendAsUserStatus | null;
82
+ polling: boolean;
83
+ startSignIn: () => void;
84
+ };
85
+ /**
86
+ * The "Sign in with Slack" button used by both dialogs. Opens OAuth and shows a
87
+ * spinner while {@link useSlackSignIn} polls for the completed round-trip. The
88
+ * surrounding explanatory copy stays with each caller since it differs.
89
+ * @param props.onClick - starts the sign-in flow (typically `startSignIn`)
90
+ * @param props.polling - whether a background poll is in flight
91
+ */
92
+ declare function SlackSignInButton({ onClick, polling }: {
93
+ onClick: () => void;
94
+ polling: boolean;
95
+ }): react.JSX.Element;
96
+
17
97
  /**
18
98
  * Standalone, page-sized picker that displays {@link Template}s as a
19
99
  * grid of cards with live block previews, organized into category
@@ -51,4 +131,4 @@ declare function TemplatePicker({ templates, onSelect, surface, theme, heading,
51
131
  className?: string;
52
132
  }): react.JSX.Element;
53
133
 
54
- export { BlockKitchen, BlockKitchenProps, PreviewSurface, PreviewTheme, Template, TemplatePicker };
134
+ export { BlockKitchen, BlockKitchenProps, ChannelOption, PreviewSurface, PreviewTheme, SendAsUserStatus, SendDialog, type SendDialogProps, SendExtrasContext, SendPayload, SlackSignInButton, SupportedBlock, Template, TemplatePicker, useSlackSignIn };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as react from 'react';
2
- import { B as BlockKitchenProps, $ as Template, K as PreviewSurface, N as PreviewTheme } from './palette-CKP8CK5Z.js';
3
- export { A as AlertBlock, a as AlertLevel, b as BrandPreset, c as BrandTheme, d as BrandTokens, e as BuilderBlock, C as CardBlock, f as CarouselBlock, g as CartesianChart, h as ChannelOption, i as Chart, j as ChartAxisConfig, k as ChartDataPoint, l as ChartSeries, m as ChartType, n as ContainerBlock, o as ContainerChildBlock, p as ContextActionsBlock, q as ContextActionsElement, r as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, s as EditingConfig, F as FeedbackButtonSubobject, t as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, u as IconButtonIcon, v as InputBlock, L as LoadMessageInput, w as LoadResult, M as MarkdownBlock, P as PaletteSection, x as PaletteVariant, y as PieChart, z as PieChartSegment, G as PlanBlock, J as PreviewHooks, R as RecentMessage, S as SendAsUserStatus, O as SendPayload, Q as SendResult, T as SupportedBlock, U as SupportedBlockType, V as SupportedHeaderBlock, W as TableBlock, X as TableCell, Y as TableColumnSetting, Z as TaskCardBlock, _ as TaskCardStatus, a0 as UpdatePayload, a1 as UpdateResult, a2 as UrlSourceElement, a3 as VideoBlock, a5 as defaultPalette, a6 as extraAlertVariant, a7 as legacyInputVariants } from './palette-CKP8CK5Z.js';
2
+ import { ReactNode } from 'react';
3
+ import { c as BlockKitchenProps, _ as SupportedBlock, k as ChannelOption, W as SendAsUserStatus, Y as SendPayload, X as SendExtrasContext, a6 as Template, R as PreviewSurface, S as PreviewTheme } from './palette-C8sPJuXx.js';
4
+ export { A as AlertBlock, a as AlertLevel, B as BlockKitchenBaseProps, b as BlockKitchenComposeOnlyProps, d as BlockKitchenSendProps, e as BrandPreset, f as BrandTheme, g as BrandTokens, h as BuilderBlock, C as CardBlock, i as CarouselBlock, j as CartesianChart, l as Chart, m as ChartAxisConfig, n as ChartDataPoint, o as ChartSeries, p as ChartType, q as ContainerBlock, r as ContainerChildBlock, s as ContextActionsBlock, t as ContextActionsElement, u as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, F as FeedbackButtonSubobject, v as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, w as IconButtonIcon, x as InputBlock, L as LoadMessageInput, y as LoadResult, z as LoadedMessage, G as LoadingConfig, M as MarkdownBlock, P as PaletteSection, J as PaletteVariant, K as PieChart, N as PieChartSegment, O as PlanBlock, Q as PreviewHooks, T as PrimaryActionConfig, U as PrimaryActionContext, V as RecentMessage, Z as SendResult, $ as SupportedBlockType, a0 as SupportedHeaderBlock, a1 as TableBlock, a2 as TableCell, a3 as TableColumnSetting, a4 as TaskCardBlock, a5 as TaskCardStatus, a7 as UpdatePayload, a8 as UpdateResult, a9 as UrlSourceElement, aa as ValidationSummary, ab as VideoBlock, ad as defaultPalette, ae as extraAlertVariant, af as legacyInputVariants } from './palette-C8sPJuXx.js';
4
5
  export { decodeBlocksFromString, encodeBlocksToString, toSlackBlocks } from './helpers.js';
5
6
  import 'slack-web-api-client';
6
7
 
@@ -14,6 +15,85 @@ import 'slack-web-api-client';
14
15
  */
15
16
  declare function BlockKitchen(props: BlockKitchenProps): react.JSX.Element;
16
17
 
18
+ /**
19
+ * Props for {@link SendDialog}. Exported so hosts building a bespoke send
20
+ * flow (compose-only mode) can reuse the built-in dialog standalone instead
21
+ * of rebuilding channel loading, identity, and sending states from scratch.
22
+ */
23
+ interface SendDialogProps {
24
+ /** Whether the dialog is open. */
25
+ open: boolean;
26
+ /** Notified when the user closes the dialog. */
27
+ onOpenChange: (open: boolean) => void;
28
+ /** The draft blocks to send (builder-native format). */
29
+ blocks: SupportedBlock[];
30
+ /** Returns channels available to send to. Called when the dialog opens. */
31
+ loadChannels: () => Promise<ChannelOption[]>;
32
+ /** Returns user-token status + OAuth URL. */
33
+ loadSendAsUserStatus: () => Promise<SendAsUserStatus>;
34
+ /** Terminal action; should return `{ ok }` or `{ ok: false, error }`. */
35
+ onSend: (payload: SendPayload) => Promise<{
36
+ ok: boolean;
37
+ error?: string;
38
+ }>;
39
+ /**
40
+ * Renders host-defined fields below the built-in pickers; collected
41
+ * values are delivered on {@link SendPayload.extras}.
42
+ */
43
+ renderSendExtras?: (context: SendExtrasContext) => ReactNode;
44
+ /** Label for the final confirm button. Defaults to `'Send'`. */
45
+ confirmSendLabel?: string;
46
+ /** Total validation errors against the current draft. */
47
+ errorCount: number;
48
+ /** Asks the parent to open the global issues panel. */
49
+ onShowIssues?: () => void;
50
+ }
51
+ /**
52
+ * Modal dialog that collects the destination channel + send-as identity
53
+ * (plus any host-defined extras), then calls the consumer's `onSend`.
54
+ *
55
+ * Channels and user-token status are loaded async via callback props on open.
56
+ * The consumer brokers all I/O; the dialog never makes a network call.
57
+ * @param props - {@link SendDialogProps}
58
+ * @returns the rendered send dialog
59
+ */
60
+ declare function SendDialog({ open, onOpenChange, blocks, loadChannels, loadSendAsUserStatus, onSend, renderSendExtras, confirmSendLabel, errorCount, onShowIssues }: SendDialogProps): react.JSX.Element;
61
+
62
+ /**
63
+ * Tracks the "act as the user" Slack token status for a dialog and drives the
64
+ * OAuth sign-in flow. Shared by {@link SendDialog} and {@link UpdateDialog}.
65
+ *
66
+ * While `open && enabled` it fetches status on open and refreshes on window
67
+ * focus. Once `startSignIn` opens the OAuth tab it also background-polls until
68
+ * the token appears or the retry cap is hit, so the dialog unlocks itself
69
+ * without a manual reload. All work stops when the dialog closes, the token
70
+ * path doesn't apply, or the component unmounts.
71
+ *
72
+ * @param loadStatus - consumer callback returning user-token status + OAuth URL
73
+ * @param options.open - whether the host dialog is open
74
+ * @param options.enabled - whether the user-token path applies (idle when false)
75
+ * @returns `{ userStatus, polling, startSignIn }`
76
+ */
77
+ declare function useSlackSignIn(loadStatus: () => Promise<SendAsUserStatus>, { open, enabled }: {
78
+ open: boolean;
79
+ enabled: boolean;
80
+ }): {
81
+ userStatus: SendAsUserStatus | null;
82
+ polling: boolean;
83
+ startSignIn: () => void;
84
+ };
85
+ /**
86
+ * The "Sign in with Slack" button used by both dialogs. Opens OAuth and shows a
87
+ * spinner while {@link useSlackSignIn} polls for the completed round-trip. The
88
+ * surrounding explanatory copy stays with each caller since it differs.
89
+ * @param props.onClick - starts the sign-in flow (typically `startSignIn`)
90
+ * @param props.polling - whether a background poll is in flight
91
+ */
92
+ declare function SlackSignInButton({ onClick, polling }: {
93
+ onClick: () => void;
94
+ polling: boolean;
95
+ }): react.JSX.Element;
96
+
17
97
  /**
18
98
  * Standalone, page-sized picker that displays {@link Template}s as a
19
99
  * grid of cards with live block previews, organized into category
@@ -51,4 +131,4 @@ declare function TemplatePicker({ templates, onSelect, surface, theme, heading,
51
131
  className?: string;
52
132
  }): react.JSX.Element;
53
133
 
54
- export { BlockKitchen, BlockKitchenProps, PreviewSurface, PreviewTheme, Template, TemplatePicker };
134
+ export { BlockKitchen, BlockKitchenProps, ChannelOption, PreviewSurface, PreviewTheme, SendAsUserStatus, SendDialog, type SendDialogProps, SendExtrasContext, SendPayload, SlackSignInButton, SupportedBlock, Template, TemplatePicker, useSlackSignIn };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, pointerWithin, closestCenter, DndContext, DragOverlay, useDroppable, useDraggable } from '@dnd-kit/core';
2
2
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
3
- import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, MailSearch, Plus, ChevronDown, BookOpen, ExternalLink, AlertTriangle, Trash2, Code2, Pencil, Send, Search, Check, Copy, Info, CheckCircle2, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Loader2, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Undo2, Redo2, Smile, Link as Link$1 } from 'lucide-react';
3
+ import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, Loader2, ExternalLink, AlertTriangle, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, MailSearch, Plus, ChevronDown, BookOpen, Trash2, Code2, Pencil, Send, Search, Check, Copy, Info, CheckCircle2, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Undo2, Redo2, Smile, Link as Link$1 } from 'lucide-react';
4
4
  import * as React from 'react';
5
- import { createContext, useMemo, useEffect, useState, useCallback, useRef, useId, useContext } from 'react';
5
+ import { createContext, useState, useRef, useEffect, useCallback, useMemo, useId, useContext } from 'react';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
  import * as DialogPrimitive from '@radix-ui/react-dialog';
8
8
  import { clsx } from 'clsx';
@@ -2015,6 +2015,7 @@ function computeValidation(blocks, surface) {
2015
2015
  const grouped = groupValidatorErrors(result.errors, blocks);
2016
2016
  return {
2017
2017
  valid: result.valid,
2018
+ errors: result.errors,
2018
2019
  byBlockId: grouped.byBlockId,
2019
2020
  general: grouped.general,
2020
2021
  total: grouped.total
@@ -2225,6 +2226,29 @@ function useBlockKitchenState({
2225
2226
  replaceAll
2226
2227
  };
2227
2228
  }
2229
+ function useDevWarning(condition, message) {
2230
+ const warnedRef = useRef(false);
2231
+ useEffect(() => {
2232
+ if (condition && !warnedRef.current) {
2233
+ warnedRef.current = true;
2234
+ console.warn(message);
2235
+ }
2236
+ }, [condition, message]);
2237
+ }
2238
+ function useNotifyOnChange(value, toKey, callback) {
2239
+ const lastNotifiedRef = useRef(null);
2240
+ useEffect(() => {
2241
+ if (!callback) {
2242
+ return;
2243
+ }
2244
+ const key = toKey(value);
2245
+ if (lastNotifiedRef.current === key) {
2246
+ return;
2247
+ }
2248
+ lastNotifiedRef.current = key;
2249
+ callback(value);
2250
+ }, [value, toKey, callback]);
2251
+ }
2228
2252
 
2229
2253
  // src/lib/brand-theme.ts
2230
2254
  var TOKEN_TO_CSS_VAR = {
@@ -3022,7 +3046,7 @@ function LoadMessageDialog({
3022
3046
  loadRecentMessagesRef.current = loadRecentMessages;
3023
3047
  loadChannelsRef.current = loadChannels;
3024
3048
  });
3025
- const hasRecent = !!loadRecentMessages;
3049
+ const hasRecent = !!loadRecentMessages && !!loadChannels;
3026
3050
  useEffect(() => {
3027
3051
  if (!open) {
3028
3052
  return;
@@ -3033,7 +3057,10 @@ function LoadMessageDialog({
3033
3057
  setRecent(null);
3034
3058
  setRecentError(null);
3035
3059
  setSelectedRecent(null);
3036
- if (!loadRecentMessagesRef.current) {
3060
+ }, [open]);
3061
+ useEffect(() => {
3062
+ const load = loadChannelsRef.current;
3063
+ if (!open || !hasRecent || !load) {
3037
3064
  setChannels([]);
3038
3065
  setChannelsError(null);
3039
3066
  return;
@@ -3041,7 +3068,7 @@ function LoadMessageDialog({
3041
3068
  setChannels(null);
3042
3069
  setChannelsError(null);
3043
3070
  let cancelled = false;
3044
- loadChannelsRef.current().then((list) => {
3071
+ load().then((list) => {
3045
3072
  if (!cancelled) {
3046
3073
  setChannels(list);
3047
3074
  }
@@ -3053,7 +3080,7 @@ function LoadMessageDialog({
3053
3080
  return () => {
3054
3081
  cancelled = true;
3055
3082
  };
3056
- }, [open]);
3083
+ }, [open, hasRecent]);
3057
3084
  useEffect(() => {
3058
3085
  if (!open || !loadRecentMessagesRef.current || !channelId) {
3059
3086
  return;
@@ -3532,6 +3559,7 @@ function SendDialog({
3532
3559
  loadChannels,
3533
3560
  loadSendAsUserStatus,
3534
3561
  onSend,
3562
+ renderSendExtras,
3535
3563
  confirmSendLabel = "Send",
3536
3564
  errorCount,
3537
3565
  onShowIssues
@@ -3541,6 +3569,10 @@ function SendDialog({
3541
3569
  const [channelId, setChannelId] = useState("");
3542
3570
  const [sendAs, setSendAs] = useState("bot");
3543
3571
  const [status, setStatus] = useState({ kind: "idle" });
3572
+ const [extras, setExtrasState] = useState({});
3573
+ const setExtras = useCallback((patch) => {
3574
+ setExtrasState((prev) => ({ ...prev, ...patch }));
3575
+ }, []);
3544
3576
  const { userStatus, polling, startSignIn } = useSlackSignIn(loadSendAsUserStatus, { open, enabled: true });
3545
3577
  const loadChannelsRef = useRef(loadChannels);
3546
3578
  useEffect(() => {
@@ -3555,6 +3587,7 @@ function SendDialog({
3555
3587
  setChannelsError(null);
3556
3588
  setChannelId("");
3557
3589
  setSendAs("bot");
3590
+ setExtrasState({});
3558
3591
  let cancelled = false;
3559
3592
  loadChannelsRef.current().then((list) => {
3560
3593
  if (cancelled) {
@@ -3582,7 +3615,10 @@ function SendDialog({
3582
3615
  const result = await onSend({
3583
3616
  channelId,
3584
3617
  blocks: toSlackBlocks(blocks),
3585
- sendAsUser: sendAs === "user"
3618
+ sendAsUser: sendAs === "user",
3619
+ // Only introduce the key when the extras slot is wired, so existing
3620
+ // consumers never see a new payload field.
3621
+ ...renderSendExtras ? { extras } : {}
3586
3622
  });
3587
3623
  if (result.ok) {
3588
3624
  setStatus({ kind: "idle" });
@@ -3649,6 +3685,12 @@ function SendDialog({
3649
3685
  }
3650
3686
  )
3651
3687
  ] }),
3688
+ renderSendExtras ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1.5", children: renderSendExtras({
3689
+ channelId: channelId || null,
3690
+ sendAsUser: sendAs === "user",
3691
+ extras,
3692
+ setExtras
3693
+ }) }) : null,
3652
3694
  errorCount > 0 ? /* @__PURE__ */ jsxs(
3653
3695
  "button",
3654
3696
  {
@@ -9395,8 +9437,11 @@ function Toolbar2({
9395
9437
  showThemeControl = true,
9396
9438
  docsLink,
9397
9439
  errorCount,
9440
+ showSend = true,
9441
+ primaryAction,
9398
9442
  sendButtonLabel = "Review & send",
9399
- editingEnabled = false,
9443
+ loadEnabled = false,
9444
+ updateEnabled = false,
9400
9445
  editBadge,
9401
9446
  onOpenLoad,
9402
9447
  onOpenUpdate,
@@ -9416,7 +9461,7 @@ function Toolbar2({
9416
9461
  return /* @__PURE__ */ jsxs(Fragment, { children: [
9417
9462
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-x-2 gap-y-1 border-b bg-background px-2 py-1.5 sm:px-3 sm:py-2", children: [
9418
9463
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 sm:gap-2", children: [
9419
- editingEnabled ? /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9464
+ loadEnabled ? /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9420
9465
  /* @__PURE__ */ jsx(MailSearch, { className: "h-3.5 w-3.5" }),
9421
9466
  /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: loadButtonLabel })
9422
9467
  ] }) : null,
@@ -9529,9 +9574,29 @@ function Toolbar2({
9529
9574
  /* @__PURE__ */ jsx(Code2, { className: "h-3.5 w-3.5" }),
9530
9575
  /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: "View JSON" })
9531
9576
  ] }),
9532
- editBadge ? (
9533
- // A message is loaded: split button. Main action updates it in
9534
- // place; the menu also offers posting the blocks as a new message.
9577
+ !showSend ? primaryAction ? (
9578
+ // Compose-only mode with a host-owned primary action: same
9579
+ // placement and styling as the built-in send button. No icon —
9580
+ // the action's meaning is the host's, so the label always shows
9581
+ // (the send button can collapse to its icon on small screens;
9582
+ // an icon-less button can't).
9583
+ /* @__PURE__ */ jsx(
9584
+ Button,
9585
+ {
9586
+ type: "button",
9587
+ size: "sm",
9588
+ onClick: primaryAction.onClick,
9589
+ disabled: primaryAction.disabled,
9590
+ "aria-label": primaryAction.label,
9591
+ children: /* @__PURE__ */ jsx("span", { children: primaryAction.label })
9592
+ }
9593
+ )
9594
+ ) : null : editBadge && updateEnabled ? (
9595
+ // A message is loaded and update-in-place is wired: split button.
9596
+ // Main action updates it in place; the menu also offers posting
9597
+ // the blocks as a new message. Without `updateEnabled` (loading
9598
+ // configured but no `editing.onUpdate`), the plain send button
9599
+ // below stays — the loaded message can only be posted as new.
9535
9600
  /* @__PURE__ */ jsxs("div", { className: "flex items-stretch", children: [
9536
9601
  /* @__PURE__ */ jsxs(
9537
9602
  Button,
@@ -9604,7 +9669,7 @@ function Toolbar2({
9604
9669
  ] })
9605
9670
  ] })
9606
9671
  ] }),
9607
- editingEnabled && editBadge ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b bg-muted px-2 py-2 text-foreground sm:px-3", children: [
9672
+ loadEnabled && editBadge ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 border-b bg-muted px-2 py-2 text-foreground sm:px-3", children: [
9608
9673
  /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 shrink-0 text-primary" }),
9609
9674
  /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 text-sm", children: [
9610
9675
  "References an existing message in ",
@@ -9767,17 +9832,34 @@ function UpdateDialog({
9767
9832
  ] })
9768
9833
  ] }) });
9769
9834
  }
9835
+ function toLoadedMessage({
9836
+ ok: _ok,
9837
+ blocks: _blocks,
9838
+ ...loadedMessage
9839
+ }) {
9840
+ return loadedMessage;
9841
+ }
9842
+ function loadedMessageKey(message) {
9843
+ return message ? JSON.stringify(message) : null;
9844
+ }
9845
+ function validationSummaryKey(summary) {
9846
+ return JSON.stringify([summary.valid, summary.errorCount, ...summary.errors]);
9847
+ }
9770
9848
  function BlockKitchen(props) {
9771
9849
  const {
9772
9850
  workspaceName,
9773
9851
  initialBlocks,
9774
9852
  onChange,
9853
+ onValidationChange,
9775
9854
  previewHooks,
9776
9855
  customEmojis,
9777
9856
  loadChannels,
9778
9857
  loadSendAsUserStatus,
9779
9858
  onSend,
9780
- editing,
9859
+ renderSendExtras,
9860
+ loading,
9861
+ onUpdate: onUpdateProp,
9862
+ primaryAction,
9781
9863
  loadButtonLabel,
9782
9864
  updateButtonLabel,
9783
9865
  confirmUpdateLabel,
@@ -9794,6 +9876,13 @@ function BlockKitchen(props) {
9794
9876
  sendButtonLabel,
9795
9877
  confirmSendLabel,
9796
9878
  theme
9879
+ // Widen the all-or-nothing union so the branch-specific props
9880
+ // destructure as independent optionals: untyped JS consumers can still
9881
+ // pass partial wiring (handled by the runtime guards below), and the
9882
+ // correlated-union narrowing would otherwise flag those guards as
9883
+ // "always true". `primaryAction` is pulled from the compose-only branch
9884
+ // (where it carries a real type) rather than the send branch's
9885
+ // `undefined` pin, which would collapse the intersection to `undefined`.
9797
9886
  } = props;
9798
9887
  const paletteSections = useMemo(() => {
9799
9888
  const sections = palette ?? defaultPalette;
@@ -9812,14 +9901,40 @@ function BlockKitchen(props) {
9812
9901
  }, [palette, disabledBlockTypes]);
9813
9902
  const variantById = useMemo(() => buildVariantById(paletteSections), [paletteSections]);
9814
9903
  const allowedSurfaces = allowedSurfacesProp && allowedSurfacesProp.length > 0 ? allowedSurfacesProp : ["message"];
9815
- const seededBlocks = editing?.initialTarget?.blocks ?? initialBlocks;
9816
- useEffect(() => {
9817
- if (editing?.initialTarget && initialBlocks) {
9818
- console.warn(
9819
- "[BlockKitchen] Both `initialBlocks` and `editing.initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
9820
- );
9821
- }
9822
- }, []);
9904
+ const sendEnabled = Boolean(loadChannels && loadSendAsUserStatus && onSend);
9905
+ const loadEnabled = Boolean(loading);
9906
+ const recentChannelSource = loading?.loadChannels ?? (sendEnabled ? loadChannels : void 0);
9907
+ const onUpdate = sendEnabled ? onUpdateProp : void 0;
9908
+ const seededBlocks = loading?.initialTarget?.blocks ?? initialBlocks;
9909
+ const sendPropsProvided = [loadChannels, loadSendAsUserStatus, onSend].filter(Boolean).length;
9910
+ useDevWarning(
9911
+ Boolean(loading?.initialTarget && initialBlocks),
9912
+ "[BlockKitchen] Both `initialBlocks` and an `initialTarget` were provided; using the target\u2019s blocks and ignoring `initialBlocks`."
9913
+ );
9914
+ useDevWarning(
9915
+ sendPropsProvided > 0 && sendPropsProvided < 3,
9916
+ "[BlockKitchen] Partial send wiring: `loadChannels`, `loadSendAsUserStatus`, and `onSend` are all-or-nothing. The send button is hidden until all three are provided."
9917
+ );
9918
+ useDevWarning(
9919
+ Boolean(onUpdateProp && !sendEnabled),
9920
+ "[BlockKitchen] `onUpdate` requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `onUpdate`. To load an existing message without the send integration, use the `loading` prop instead."
9921
+ );
9922
+ useDevWarning(
9923
+ Boolean(sendEnabled && onUpdateProp && !loading),
9924
+ "[BlockKitchen] `onUpdate` needs a loaded message, but no load source is configured \u2014 provide the `loading` prop. The update flow is unreachable."
9925
+ );
9926
+ useDevWarning(
9927
+ Boolean(loading?.loadRecentMessages && !recentChannelSource),
9928
+ "[BlockKitchen] `loading.loadRecentMessages` needs a channel list to scope its lookup \u2014 provide `loading.loadChannels` (compose-only mode has no send integration to reuse). Hiding the recent-messages picker."
9929
+ );
9930
+ useDevWarning(
9931
+ Boolean(renderSendExtras && !sendEnabled),
9932
+ "[BlockKitchen] `renderSendExtras` extends the built-in send dialog, which requires the send integration (`loadChannels`, `loadSendAsUserStatus`, `onSend`); ignoring `renderSendExtras`."
9933
+ );
9934
+ useDevWarning(
9935
+ Boolean(primaryAction && sendEnabled),
9936
+ "[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`."
9937
+ );
9823
9938
  const { blocks, addBlock, addChild, updateBlock, removeBlock, duplicateBlock, reorderBlock, moveBlock, replaceAll } = useBlockKitchenState({ initialBlocks: seededBlocks, onChange });
9824
9939
  const childParentById = useMemo(() => {
9825
9940
  const map = /* @__PURE__ */ new Map();
@@ -9836,8 +9951,11 @@ function BlockKitchen(props) {
9836
9951
  const [sendOpen, setSendOpen] = useState(false);
9837
9952
  const [updateOpen, setUpdateOpen] = useState(false);
9838
9953
  const [loadOpen, setLoadOpen] = useState(false);
9839
- const [editTarget, setEditTarget] = useState(() => editing?.initialTarget ?? null);
9840
- const activeEditTarget = editing ? editTarget : null;
9954
+ const [editTarget, setEditTarget] = useState(
9955
+ () => loading?.initialTarget ? toLoadedMessage(loading.initialTarget) : null
9956
+ );
9957
+ const activeEditTarget = loadEnabled ? editTarget : null;
9958
+ useNotifyOnChange(activeEditTarget, loadedMessageKey, loading?.onLoadedMessageChange);
9841
9959
  const [issuesOpen, setIssuesOpen] = useState(false);
9842
9960
  const [paletteOpen, setPaletteOpen] = useState(false);
9843
9961
  const [openBlockId, setOpenBlockId] = useState(null);
@@ -9855,6 +9973,11 @@ function BlockKitchen(props) {
9855
9973
  return { emoji: makeEmojiHook(byName), ...previewHooks };
9856
9974
  }, [customEmojis, previewHooks]);
9857
9975
  const validation = useBlockKitValidation(blocks, "message");
9976
+ const validationSummary = useMemo(
9977
+ () => ({ valid: validation.valid, errorCount: validation.total, errors: validation.errors }),
9978
+ [validation]
9979
+ );
9980
+ useNotifyOnChange(validationSummary, validationSummaryKey, onValidationChange);
9858
9981
  const sensors = useSensors(
9859
9982
  useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
9860
9983
  useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 } }),
@@ -9958,8 +10081,19 @@ function BlockKitchen(props) {
9958
10081
  allowedSurfaces,
9959
10082
  showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
9960
10083
  docsLink,
10084
+ showSend: sendEnabled,
10085
+ primaryAction: !sendEnabled && primaryAction ? {
10086
+ label: primaryAction.label,
10087
+ onClick: () => primaryAction.onClick({
10088
+ blocks: blockPayloads,
10089
+ validation: validationSummary,
10090
+ loadedMessage: activeEditTarget
10091
+ }),
10092
+ disabled: primaryAction.disableWhenInvalid ? !validationSummary.valid : false
10093
+ } : null,
9961
10094
  sendButtonLabel,
9962
- editingEnabled: !!editing,
10095
+ loadEnabled,
10096
+ updateEnabled: Boolean(onUpdate),
9963
10097
  editBadge: activeEditTarget ? {
9964
10098
  channelLabel: activeEditTarget.channelName ? `#${activeEditTarget.channelName}` : activeEditTarget.channelId,
9965
10099
  ts: activeEditTarget.ts
@@ -9967,9 +10101,13 @@ function BlockKitchen(props) {
9967
10101
  onOpenLoad: () => setLoadOpen(true),
9968
10102
  onOpenUpdate: () => setUpdateOpen(true),
9969
10103
  onExitEdit: () => {
10104
+ if (sendEnabled) {
10105
+ setEditTarget(null);
10106
+ replaceAll([]);
10107
+ setLoadOpen(true);
10108
+ return;
10109
+ }
9970
10110
  setEditTarget(null);
9971
- replaceAll([]);
9972
- setLoadOpen(true);
9973
10111
  },
9974
10112
  loadButtonLabel,
9975
10113
  updateButtonLabel
@@ -10041,7 +10179,7 @@ function BlockKitchen(props) {
10041
10179
  }
10042
10180
  ) }),
10043
10181
  /* @__PURE__ */ jsx(JsonDrawer, { open: jsonOpen, onOpenChange: setJsonOpen, blocks: blockPayloads, onApply: replaceAll }),
10044
- /* @__PURE__ */ jsx(
10182
+ loadChannels && loadSendAsUserStatus && onSend ? /* @__PURE__ */ jsx(
10045
10183
  SendDialog,
10046
10184
  {
10047
10185
  open: sendOpen,
@@ -10050,6 +10188,7 @@ function BlockKitchen(props) {
10050
10188
  loadChannels,
10051
10189
  loadSendAsUserStatus,
10052
10190
  onSend,
10191
+ renderSendExtras,
10053
10192
  confirmSendLabel,
10054
10193
  errorCount: validation.total,
10055
10194
  onShowIssues: () => {
@@ -10057,8 +10196,8 @@ function BlockKitchen(props) {
10057
10196
  setIssuesOpen(true);
10058
10197
  }
10059
10198
  }
10060
- ),
10061
- activeEditTarget && editing ? /* @__PURE__ */ jsx(
10199
+ ) : null,
10200
+ activeEditTarget && onUpdate && loadSendAsUserStatus ? /* @__PURE__ */ jsx(
10062
10201
  UpdateDialog,
10063
10202
  {
10064
10203
  open: updateOpen,
@@ -10066,7 +10205,7 @@ function BlockKitchen(props) {
10066
10205
  target: activeEditTarget,
10067
10206
  blocks: blockPayloads,
10068
10207
  loadSendAsUserStatus,
10069
- onUpdate: editing.onUpdate,
10208
+ onUpdate,
10070
10209
  confirmUpdateLabel,
10071
10210
  errorCount: validation.total,
10072
10211
  onShowIssues: () => {
@@ -10075,25 +10214,17 @@ function BlockKitchen(props) {
10075
10214
  }
10076
10215
  }
10077
10216
  ) : null,
10078
- editing ? /* @__PURE__ */ jsx(
10217
+ loading ? /* @__PURE__ */ jsx(
10079
10218
  LoadMessageDialog,
10080
10219
  {
10081
10220
  open: loadOpen,
10082
10221
  onOpenChange: setLoadOpen,
10083
- onLoadMessage: editing.onLoadMessage,
10084
- loadRecentMessages: editing.loadRecentMessages,
10085
- loadChannels,
10222
+ onLoadMessage: loading.onLoadMessage,
10223
+ loadRecentMessages: loading.loadRecentMessages,
10224
+ loadChannels: recentChannelSource,
10086
10225
  onLoaded: (result) => {
10087
10226
  replaceAll(result.blocks);
10088
- setEditTarget({
10089
- channelId: result.channelId,
10090
- channelName: result.channelName,
10091
- ts: result.ts,
10092
- editableVia: result.editableVia,
10093
- workspaceName: result.workspaceName,
10094
- username: result.username,
10095
- iconUrl: result.iconUrl
10096
- });
10227
+ setEditTarget(toLoadedMessage(result));
10097
10228
  setLoadOpen(false);
10098
10229
  },
10099
10230
  onOpenAsNew: (loadedBlocks) => {
@@ -10275,6 +10406,6 @@ function fromBase64Url(b64url) {
10275
10406
  return b64 + "=".repeat(padLen);
10276
10407
  }
10277
10408
 
10278
- export { BlockKitchen, TemplatePicker, decodeBlocksFromString, defaultPalette, encodeBlocksToString, extraAlertVariant, legacyInputVariants, toSlackBlocks };
10409
+ export { BlockKitchen, SendDialog, SlackSignInButton, TemplatePicker, decodeBlocksFromString, defaultPalette, encodeBlocksToString, extraAlertVariant, legacyInputVariants, toSlackBlocks, useSlackSignIn };
10279
10410
  //# sourceMappingURL=index.js.map
10280
10411
  //# sourceMappingURL=index.js.map