Skip to content

feat: caller-customized send — compose-only mode, send-dialog extras, exported primitives - #138

Merged
StephenTangCook merged 3 commits into
mainfrom
claude/slack-editor-audience-boundary-uykjuy
Jul 17, 2026
Merged

feat: caller-customized send — compose-only mode, send-dialog extras, exported primitives#138
StephenTangCook merged 3 commits into
mainfrom
claude/slack-editor-audience-boundary-uykjuy

Conversation

@StephenTangCook

@StephenTangCook StephenTangCook commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes the send integration customizable at every grain, layered on the original compose-only change:

  1. Compose-only mode (original scope): provide loadChannels / loadSendAsUserStatus / onSend for the built-in send flow (unchanged), or omit all three and the builder renders no send button — a pure Block Kit editor whose host app owns the send flow. onValidationChange lets a host-rendered CTA gate on the exact validation verdict the issues sheet shows.
  2. primaryAction (compose-only mode): a host-owned button in the toolbar slot where "Review & send" normally sits — e.g. "Save template" for a message drafter. onClick receives the current draft (builder-native format) plus the validation verdict; disableWhenInvalid opts into Send-style gating (default off — a drafter usually allows committing a WIP draft).
  3. renderSendExtras (send mode): host-defined fields rendered inside the built-in send dialog below the channel/identity pickers. Values collected via setExtras arrive on onSend as payload.extras — for "the built-in flow plus a cross-post toggle / custom sender name" without rebuilding the dialog.
  4. Exported send-flow primitives: SendDialog (+ SendDialogProps), useSlackSignIn, SlackSignInButton — so a bespoke send flow built on compose-only mode doesn't rebuild channel loading, OAuth polling, and sending states from scratch.

Why

When composing is one step in a flow the host app owns (audience selection, scheduling, multi-channel fan-out), the moment of commitment belongs outside the builder — but previously the send trio was required and the "Review & send" button couldn't be removed, forcing either a dual-CTA UX or CSS hacks. The host also had no access to the builder's validation verdict, so an external CTA would have to re-run the validator and risk drifting from what the issues sheet displays.

The follow-up layer closes the gap between "all" (built-in flow, fixed fields) and "nothing" (compose-only): customizing which identity sends, custom sender names, or extra toggles like cross-posting previously meant abandoning the entire built-in dialog. Now each need maps to a grain: extras slot → built-in UX + host fields; primaryAction → compose-only with a toolbar-native CTA; exported primitives → fully bespoke flows without rebuilding the machinery.

Design choices:

  • All-or-nothing, not individually optional. BlockKitchenProps is BlockKitchenBaseProps & (BlockKitchenSendProps | BlockKitchenComposeOnlyProps); the compose-only branch pins the trio (and editing) to undefined. Wiring only some of the three is a type error rather than a silently dead send button. Runtime console.warns cover untyped JS consumers. Fully backward compatible for existing consumers.
  • Branch discipline extends to the new props. renderSendExtras lives on the send branch, primaryAction on the compose-only branch, each pinned ?: undefined on the other — wrong-branch use is a type error, with mount-time warnings for untyped consumers.
  • payload.extras only exists when the slot is wired, so existing onSend handlers never see a new key. The extras object is dialog-owned: it resets on open alongside the channel/identity pickers, and setExtras(patch) shallow-merges — onSend stays a pure data-in callback instead of relying on host closures.
  • editing requires the trio. chat.update is inherently bound to a channel + timestamp, so the update flow only exists alongside the send integration.
  • onValidationChange fires on verdict change only (piggybacking the builder's debounced validation pass, deduped against the last-notified verdict), always scoped to the message surface — the same verdict the issues chip/sheet displays. The same ValidationSummary is what primaryAction.onClick receives.

New exports: SendDialog, useSlackSignIn, SlackSignInButton; types ValidationSummary, BlockKitchenBaseProps, BlockKitchenSendProps, BlockKitchenComposeOnlyProps, SendDialogProps, SendExtrasContext, PrimaryActionConfig, PrimaryActionContext. README gains "Compose-only mode", "Keeping the CTA in the toolbar", "Building a bespoke send flow from the exported primitives", and "Extending the send dialog" sections.

Test plan

  • pnpm typecheck
  • pnpm lint
  • pnpm test (390 tests, 31 files — includes browser-mode Storybook tests)
  • pnpm build
  • pnpm demo:build (the demo consumes ../src directly, so this is a real-consumer compile check)

Tests in test/block-kitchen-compose-only.test.tsx:

  • compose-only render hides the send button, rest of toolbar intact; send trio wired → send button renders as before
  • onValidationChange reports { valid: true, errorCount: 0, errors: [] } for a valid draft and a non-empty error list for an invalid one
  • primaryAction renders in the toolbar; click receives { blocks, validation }; enabled for an invalid draft by default; disableWhenInvalid disables it
  • partial wiring warns and hides the send button; editing without the trio warns and is ignored; primaryAction alongside the trio warns and the built-in send button wins; renderSendExtras without the trio warns
  • @ts-expect-error assertions (enforced by pnpm typecheck) pin the all-or-nothing union, including both new wrong-branch combinations

New test/send-dialog-extras.test.tsx:

  • the slot renders inside the dialog with live channelId / sendAsUser context (null before channels load)
  • setExtras values arrive on payload.extras; {} when wired but unused; the key is absent when the slot isn't wired
  • extras reset each time the dialog reopens

test/public-api.test.ts pins the entry-point exports (components + send-flow primitives) so a rename/dropped export fails in CI, not in a consumer's build.

Notes for reviewer

  • BlockKitchen destructures through a widening cast: TS's correlated-union narrowing otherwise flags the runtime guards as "always true" (TS2774), and the guards must exist for untyped consumers anyway. The cast pulls primaryAction from the compose-only branch (Pick) rather than the send branch's undefined pin, which would collapse the intersection to undefined.
  • The Storybook meta pins the send-enabled branch (Meta) because arg inference collapses the props union to never.
  • Toolbar gains a showSend prop (default true) and a resolved primaryAction slot; both are no-ops for every existing usage.
  • SendDialogProps is a new exported interface (previously an inline type) since the dialog is now public API.

🤖 Generated with Claude Code

https://claude.ai/code/session_019Av2NwEDuB6sw2Q6ab35bH

Split BlockKitchenProps into a shared base plus an all-or-nothing send
integration union: provide loadChannels/loadSendAsUserStatus/onSend for
the built-in send flow, or omit all three and the builder renders no send
button, becoming a pure editor whose host owns the send flow. Partial
wiring is a type error (and a runtime console warning for JS consumers);
`editing` requires the trio since chat.update is channel-bound.

Add onValidationChange, reporting { valid, errorCount, errors } whenever
the draft's verdict changes — the exact verdict the issues sheet shows,
validated against the message surface — so a host-rendered CTA can gate
on validity without re-running the validator and risking drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2JGjyg1LC9RxdjBFR8oeJ
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
block-kitchen ae21df3 Commit Preview URL

Branch Preview URL
Jul 17 2026, 03:39 AM

…end primitives

Three extension points layered on compose-only mode, so callers can
customize the moment of commitment at any grain:

- renderSendExtras (send mode): host-defined fields rendered inside the
  built-in send dialog below the channel/identity pickers; values collected
  via setExtras arrive on onSend as payload.extras. The key is only present
  when the slot is wired, so existing onSend handlers are unaffected. The
  extras object is dialog-owned and resets on open.
- primaryAction (compose-only mode): host-owned button rendered in the
  toolbar slot where "Review & send" normally sits (e.g. "Save template"
  for a message drafter). onClick receives the current draft plus the same
  validation verdict onValidationChange reports; disableWhenInvalid opts
  into Send-style gating (default off).
- exported send-flow primitives: SendDialog (+ SendDialogProps),
  useSlackSignIn, SlackSignInButton — so a bespoke send flow built on
  compose-only mode doesn't rebuild channel loading, OAuth polling, and
  sending states from scratch.

Both new props follow the union's all-or-nothing discipline: wrong-branch
use is a type error, with mount-time console.warn guards for untyped
consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Av2NwEDuB6sw2Q6ab35bH
@StephenTangCook StephenTangCook changed the title feat: compose-only mode — optional send integration + onValidationChange feat: caller-customized send — compose-only mode, send-dialog extras, exported primitives Jul 16, 2026
@StephenTangCook
StephenTangCook marked this pull request as ready for review July 17, 2026 03:26
…udience-boundary-uykjuy

Brings the branch up to date with main (0.9.5 → 0.9.9 line): dependency
bumps, the JSON-drawer Done button, the data_visualization preview-image
sanitize fix, and the bk-utilities style-scoping step. No conflicts.

Applies Biome's it.each formatting to three test files: main's Biome
bump (2.4.16 → 2.5.4, #155) reformats multi-line it.each arrays onto one
line. The files are otherwise untouched by this branch; the reformat just
keeps `biome check` green under the newer version.
@StephenTangCook
StephenTangCook merged commit 5c8b33e into main Jul 17, 2026
13 checks passed
@StephenTangCook
StephenTangCook deleted the claude/slack-editor-audience-boundary-uykjuy branch July 17, 2026 04:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants