@tightknitai/block-kitchen 0.10.5 → 0.10.7

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
@@ -98,6 +98,27 @@ export function MyBuilderPage() {
98
98
  }
99
99
  ```
100
100
 
101
+ ### Sizing
102
+
103
+ The builder is an app shell: a fixed toolbar over a palette rail and a preview pane that scroll independently. It fills its container, so the height you give that container is the height it takes.
104
+
105
+ ```tsx
106
+ // Give it a definite height and it fills exactly that.
107
+ <div style={{ height: "100%" }}>
108
+ <BlockKitchen {...props} />
109
+ </div>
110
+ ```
111
+
112
+ Given no height to work with — a plain `<div>` in ordinary document flow — it bounds itself to `100svh` rather than growing to fit the palette's full block list, which would run a couple of thousand pixels down the page. Override that bound with `--bk-max-height` on any ancestor:
113
+
114
+ ```css
115
+ /* Room for a 4rem page header above the builder. */
116
+ .builder-host { --bk-max-height: calc(100svh - 4rem); }
117
+
118
+ /* Or opt out entirely and let it grow with its content. */
119
+ .builder-host { --bk-max-height: none; }
120
+ ```
121
+
101
122
  ## Props
102
123
 
103
124
  | Prop | Type | Required | Description |
@@ -120,8 +141,9 @@ export function MyBuilderPage() {
120
141
  | `customEmojis` | `CustomEmoji[]` | no | Workspace custom emoji (`{ name, url, alias }`) the preview resolves. Entries with a `url` render `:name:` as the workspace image; alias entries (`url: null`) fall back to their target emoji. Render-only — never serialized into the emitted Block Kit JSON. A caller-supplied `previewHooks.emoji` takes precedence. |
121
142
  | `palette` | `PaletteSection[]` | no | The left-hand palette of draggable variants. Defaults to `defaultPalette`. Spread it to filter, reorder, or add your own pre-configured variants — see [Customizing the palette](#customizing-the-palette). |
122
143
  | `disabledBlockTypes` | `SupportedBlockType[]` | no | Block types to hide from the palette without rebuilding it. Filters at the variant level — a section keeps any variants whose block types aren't disabled; sections that end up empty are dropped. Convenient when you want the default palette minus a few types (e.g. `['image', 'table']` for a text-only builder). |
144
+ | `paletteMode` | `'advanced' \| 'simple'` | no | How much of the palette the user meets first. `'advanced'` (default) renders the full sectioned palette with its search input. `'simple'` opens on a flat list of the variants flagged `basic` — in the built-in palette, just **Rich Text Section** — with no search and an "Advanced" link at the top that swaps in the full palette. Nothing is removed; it's a smaller first screen. See [Simple vs advanced palette](#simple-vs-advanced-palette). |
123
145
  | `defaultOpenSections` | `boolean \| string[]` | no | Which palette section headers are expanded on first paint. `true` (default) opens all sections; `false` collapses all (Slack-style); an array opens only sections whose `name` is in the list (e.g. `['Section', 'Actions']`). The palette also has a built-in search input that expands matching sections on demand. |
124
- | `showPaletteSearch` | `boolean` | no | Whether the palette renders the quick-search input above the section list. Defaults to `true`. Set `false` for compact palettes (e.g. when you've passed a small custom `palette`) where scanning by eye is faster than typing. |
146
+ | `showPaletteSearch` | `boolean` | no | Whether the palette renders the quick-search input above the section list. Defaults to `true`. Set `false` for compact palettes (e.g. when you've passed a small custom `palette`) where scanning by eye is faster than typing. Simple mode has no search either way — it appears with the rest of the palette behind "Advanced". |
125
147
  | `paletteSearchPlaceholder` | `string` | no | Placeholder text for the palette search input. Defaults to `'Search blocks…'`. Useful for localization. |
126
148
  | `allowedSurfaces` | `PreviewSurface[]` | no | Allowlist of preview surfaces (`'message'`, `'modal'`, `'app_home'`). Defaults to `['message']` — surface dropdown is hidden when only one surface is allowed. The first entry is the initial selection. |
127
149
  | `showThemeControl` | `boolean` | no | Defaults to `true`. When `false`, the toolbar's light/dark toggle is hidden and the theme stays at `defaultPreviewTheme`. Ignored when `previewTheme` is set (a controlled theme always hides the toggle). |
@@ -452,6 +474,107 @@ const PALETTE: readonly PaletteSection[] = [
452
474
 
453
475
  Variant `id`s must be unique across the array — the drag-drop lookup keys by id.
454
476
 
477
+ ### Simple vs advanced palette
478
+
479
+ By default the palette opens on everything it has: every section, plus the search input. For consumers whose users are writing ordinary messages rather than building interactive apps, `paletteMode="simple"` opens on a one-block starter list instead — no sections, no search — behind an **Advanced** link that swaps in the full palette (and back).
480
+
481
+ ```tsx
482
+ <BlockKitchen paletteMode="simple" {...rest} />
483
+ ```
484
+
485
+ Which variants the simple list holds is a property of the palette, not the flag: a variant opts in with `basic: true`. The built-in palette flags exactly one — **Rich Text Section** — so a custom palette that wants its own starter block says so:
486
+
487
+ ```tsx
488
+ const PALETTE: readonly PaletteSection[] = [
489
+ {
490
+ name: "Company presets",
491
+ icon: AlignLeft,
492
+ variants: [
493
+ { id: "help_footer", label: "Help footer", basic: true, factory: () => ({ ... }) },
494
+ { id: "company_divider", label: "Company divider", factory: () => ({ ... }) },
495
+ ],
496
+ },
497
+ ];
498
+ ```
499
+
500
+ A palette that flags none of its variants `basic` has no simple list to show, so it renders as `'advanced'` regardless — the link is withheld rather than leading to an empty rail.
501
+
502
+ ## Templates (`TemplatePicker`)
503
+
504
+ `TemplatePicker` is a standalone, page-sized picker that renders `Template`s as a grid of cards with live block previews, grouped into category sections — modeled on Slack's own Block Kit Builder templates page. It's pure UI: it owns no dialog and no layout, so you decide whether it's a route, a modal, a slide-over, or a sidebar next to the builder.
505
+
506
+ ```tsx
507
+ import {
508
+ BlockKitchen,
509
+ TemplatePicker,
510
+ type SupportedBlock,
511
+ type Template,
512
+ } from "@tightknitai/block-kitchen";
513
+ import { useState } from "react";
514
+
515
+ const TEMPLATES: Template[] = [
516
+ {
517
+ id: "standup",
518
+ name: "Daily standup",
519
+ description: "Yesterday / today / blockers",
520
+ surface: "message",
521
+ category: "Team", // optional — groups cards into sections
522
+ blocks: [
523
+ /* SupportedBlock[] */
524
+ ],
525
+ },
526
+ ];
527
+
528
+ function Builder() {
529
+ const [blocks, setBlocks] = useState<SupportedBlock[]>([]);
530
+ // `initialBlocks` is read once at mount, so bump a key to re-seed.
531
+ const [seedKey, setSeedKey] = useState(0);
532
+
533
+ return (
534
+ <div style={{ display: "flex", minHeight: 0 }}>
535
+ <div style={{ flex: 1, minWidth: 0 }}>
536
+ <BlockKitchen key={seedKey} initialBlocks={blocks} onChange={setBlocks} {...rest} />
537
+ </div>
538
+ {/* `.bk-root` is required — see below */}
539
+ <aside className="bk-root" style={{ width: 380, overflowY: "auto" }}>
540
+ <TemplatePicker
541
+ templates={TEMPLATES}
542
+ heading="Templates"
543
+ onSelect={(t) => {
544
+ setBlocks(t.blocks);
545
+ setSeedKey((n) => n + 1);
546
+ }}
547
+ />
548
+ </aside>
549
+ </div>
550
+ );
551
+ }
552
+ ```
553
+
554
+ | Prop | Type | Required | Description |
555
+ | --- | --- | --- | --- |
556
+ | `templates` | `Template[]` | yes | Templates to show. Order is preserved within each category section. |
557
+ | `onSelect` | `(template: Template) => void` | yes | Called with the whole template when a card is clicked. Nothing is applied to the builder for you — see [Applying a selection](#applying-a-selection). |
558
+ | `surface` | `'message' \| 'modal' \| 'app_home'` | no | Filter to templates whose `surface` matches. Omit to show all. |
559
+ | `theme` | `'light' \| 'dark'` | no | Preview theme used inside the card thumbnails. Defaults to `'light'`; pass the builder's `previewTheme` to keep them in step. |
560
+ | `heading` | `string` | no | Heading rendered above the grid. Omitted means no heading. |
561
+ | `emptyLabel` | `string` | no | Shown when the `surface` filter matches nothing. Defaults to `'No templates available.'`. |
562
+ | `className` | `string` | no | Merged onto the root element. |
563
+
564
+ A `Template` is `{ id, name, surface, blocks }` plus optional `description` and `category`. Templates without a `category` fall into a trailing **Other** section when at least one other template has one; with no categories at all, the grid renders flat without section headers.
565
+
566
+ ### Bring your own templates
567
+
568
+ The package ships **no** templates on purpose — they're use-case examples ("Expense approval" belongs to an approvals product, "Daily standup" to a team product), so they belong in your app's config rather than the library bundle. The live demo defines its own set in [`demo/src/templates.ts`](demo/src/templates.ts) if you want a starting point to crib from.
569
+
570
+ ### Applying a selection
571
+
572
+ `onSelect` hands you the template and stops there — the picker never reaches into the builder. Because [`initialBlocks`](#props) is read once at mount, replacing the current draft means re-seeding with a changed `key`, as above. Selecting a template this way discards the draft and its undo history, so gate it behind a confirmation if that's a surprise in your app.
573
+
574
+ ### It needs a `.bk-root` ancestor
575
+
576
+ Unlike `BlockKitchen`, the picker does not add the `bk-root` class itself. The stylesheet ships its utilities inside `@scope (.bk-root, .bk-portal-content)`, so a picker mounted outside both roots renders unstyled. Wrap it (or any ancestor) in `className="bk-root"`.
577
+
455
578
  ## Boundary
456
579
 
457
580
  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.
@@ -465,6 +588,7 @@ import {
465
588
  decodeBlocksFromString,
466
589
  defaultPalette, // the built-in palette — spread to customize
467
590
  SendDialog, // the built-in send dialog, standalone (bespoke send flows)
591
+ TemplatePicker, // the standalone templates grid — see Templates above
468
592
  useSlackSignIn, // the OAuth sign-in state machine behind the identity picker
469
593
  SlackSignInButton, // the "Sign in with Slack" button + polling spinner
470
594
  } from "@tightknitai/block-kitchen";
@@ -480,6 +604,7 @@ import type {
480
604
  BlockKitchenComposeOnlyProps, // the trio explicitly absent + `primaryAction`
481
605
  PaletteSection,
482
606
  PaletteVariant,
607
+ PaletteMode, // 'advanced' (default) | 'simple'
483
608
  SendPayload,
484
609
  SendResult,
485
610
  SendDialogProps,
@@ -488,6 +613,7 @@ import type {
488
613
  PrimaryActionContext,
489
614
  ChannelOption,
490
615
  SendAsUserStatus,
616
+ Template, // one entry in a TemplatePicker gallery
491
617
  ValidationSummary,
492
618
  PreviewHooks,
493
619
  } from "@tightknitai/block-kitchen";
@@ -1,5 +1,5 @@
1
- import { S as SupportedBlock } from './palette-Sz27861i.cjs';
2
- export { a0 as SupportedBlockType } from './palette-Sz27861i.cjs';
1
+ import { S as SupportedBlock } from './palette-QbOx7kAa.cjs';
2
+ export { a1 as SupportedBlockType } from './palette-QbOx7kAa.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 { S as SupportedBlock } from './palette-Sz27861i.js';
2
- export { a0 as SupportedBlockType } from './palette-Sz27861i.js';
1
+ import { S as SupportedBlock } from './palette-QbOx7kAa.js';
2
+ export { a1 as SupportedBlockType } from './palette-QbOx7kAa.js';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
package/dist/index.cjs CHANGED
@@ -848,7 +848,10 @@ var defaultPalette = [
848
848
  variants: [
849
849
  {
850
850
  id: "rich_text_section",
851
- label: "Section",
851
+ label: "Rich Text Section",
852
+ // The one variant simple mode offers, where it's listed without its
853
+ // section heading — hence the self-describing label.
854
+ basic: true,
852
855
  factory: () => ({
853
856
  type: "rich_text",
854
857
  elements: [
@@ -3500,7 +3503,8 @@ function LoadMessageDialog({
3500
3503
  }, POLL_INTERVAL_MS2);
3501
3504
  };
3502
3505
  const preview = activeTab === "recent" ? selectedRecent ? { kind: "ready", target: { ...selectedRecent, editableVia: selectedRecent.editableVia ?? "bot" } } : { kind: "empty" } : linkStatus.kind === "idle" ? { kind: "empty" } : linkStatus.kind === "ready" ? { kind: "ready", target: linkStatus.result } : linkStatus;
3503
- const canLoad = activeTab === "recent" ? !!selectedRecent : !!link.trim();
3506
+ const verdictOwnsAction = linkStatus.kind === "not-editable" && (isSafeHref(linkStatus.oauthUrl) || (linkStatus.blocks?.length ?? 0) > 0);
3507
+ const canLoad = activeTab === "recent" ? !!selectedRecent : !!link.trim() && !verdictOwnsAction;
3504
3508
  const panelProps = (id) => hasRecent ? { role: "tabpanel", id: panelDomId(id), "aria-labelledby": tabDomId(id) } : {};
3505
3509
  return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
3506
3510
  DialogContent,
@@ -3745,6 +3749,24 @@ function PreviewPane({
3745
3749
  editableLabel(target.editableVia)
3746
3750
  ] }) : null
3747
3751
  ] }),
3752
+ state.kind === "not-editable" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 flex-col gap-2 rounded-md border border-amber-200! bg-amber-50 p-3 text-xs text-amber-800", children: [
3753
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
3754
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
3755
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: state.reason })
3756
+ ] }),
3757
+ signInUrl && /* @__PURE__ */ jsxRuntime.jsx(SlackSignInButton, { onClick: () => onSignIn(signInUrl), polling: signInPolling }),
3758
+ verdictBlocks && verdictBlocks.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3759
+ Button,
3760
+ {
3761
+ type: "button",
3762
+ variant: "outline",
3763
+ size: "sm",
3764
+ className: "self-start",
3765
+ onClick: () => onOpenAsNew(verdictBlocks),
3766
+ children: "Open as a new message instead"
3767
+ }
3768
+ )
3769
+ ] }),
3748
3770
  /* @__PURE__ */ jsxRuntime.jsxs(
3749
3771
  "div",
3750
3772
  {
@@ -3753,30 +3775,13 @@ function PreviewPane({
3753
3775
  tabIndex: 0,
3754
3776
  className: "flex flex-1 flex-col gap-3 overflow-y-auto rounded-md border bg-muted/40 p-3 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring max-lg:max-h-72 max-lg:min-h-32 lg:min-h-0",
3755
3777
  children: [
3756
- state.kind === "not-editable" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 flex-col gap-2 rounded-md border border-amber-200! bg-amber-50 p-3 text-xs text-amber-800", children: [
3757
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
3758
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
3759
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: state.reason })
3760
- ] }),
3761
- signInUrl && /* @__PURE__ */ jsxRuntime.jsx(SlackSignInButton, { onClick: () => onSignIn(signInUrl), polling: signInPolling }),
3762
- verdictBlocks && verdictBlocks.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3763
- Button,
3764
- {
3765
- type: "button",
3766
- variant: "outline",
3767
- size: "sm",
3768
- className: "self-start",
3769
- onClick: () => onOpenAsNew(verdictBlocks),
3770
- children: "Open as a new message instead"
3771
- }
3772
- )
3773
- ] }),
3774
3778
  state.kind === "error" && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "shrink-0 rounded-md border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive", children: state.error }),
3775
3779
  state.kind === "loading" && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "m-auto flex items-center gap-2 text-sm text-muted-foreground", children: [
3776
3780
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 animate-spin" }),
3777
3781
  "Looking up that message\u2026"
3778
3782
  ] }),
3779
3783
  state.kind === "empty" && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-auto px-4 text-center text-sm text-muted-foreground", children: emptyHint }),
3784
+ state.kind === "not-editable" && !(verdictBlocks && verdictBlocks.length > 0) && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-auto px-4 text-center text-sm text-muted-foreground", children: "Nothing to preview." }),
3780
3785
  target && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
3781
3786
  SlackMessagePreview,
3782
3787
  {
@@ -3806,6 +3811,20 @@ function isDefaultOpen(sectionName, config) {
3806
3811
  if (typeof config === "boolean") return config;
3807
3812
  return config.includes(sectionName);
3808
3813
  }
3814
+ var PALETTE_TITLE = "Message Elements";
3815
+ var ADVANCED_HELP = "Browse every Slack block type \u2014 sections, images, buttons, inputs, tables and more \u2014 plus a search box.";
3816
+ var BASIC_HELP = "Go back to the short list: just the rich text block for writing a message.";
3817
+ function basicVariants(sections) {
3818
+ const out = [];
3819
+ for (const section of sections) {
3820
+ for (const variant of section.variants) {
3821
+ if (variant.basic) {
3822
+ out.push(variant);
3823
+ }
3824
+ }
3825
+ }
3826
+ return out;
3827
+ }
3809
3828
  function filterSections(sections, query) {
3810
3829
  const q = query.trim().toLowerCase();
3811
3830
  if (q.length === 0) return sections;
@@ -3828,14 +3847,26 @@ function Palette({
3828
3847
  defaultOpenSections = true,
3829
3848
  showSearch = true,
3830
3849
  searchPlaceholder = "Search blocks\u2026",
3850
+ mode = "advanced",
3831
3851
  variant = "aside"
3832
3852
  }) {
3833
3853
  const [query, setQuery] = React.useState("");
3854
+ const [advancedOpen, setAdvancedOpen] = React.useState(false);
3855
+ const [prevMode, setPrevMode] = React.useState(mode);
3856
+ if (prevMode !== mode) {
3857
+ setPrevMode(mode);
3858
+ setAdvancedOpen(false);
3859
+ setQuery("");
3860
+ }
3861
+ const basics = React.useMemo(() => mode === "simple" ? basicVariants(sections) : [], [mode, sections]);
3862
+ const offersSimple = mode === "simple" && basics.length > 0;
3863
+ const advanced = !offersSimple || advancedOpen;
3864
+ const searchVisible = showSearch && advanced;
3834
3865
  const visibleSections = React.useMemo(
3835
- () => showSearch ? filterSections(sections, query) : sections,
3836
- [sections, query, showSearch]
3866
+ () => searchVisible ? filterSections(sections, query) : sections,
3867
+ [sections, query, searchVisible]
3837
3868
  );
3838
- const queryActive = showSearch && query.trim().length > 0;
3869
+ const queryActive = searchVisible && query.trim().length > 0;
3839
3870
  const isSheet = variant === "sheet";
3840
3871
  return /* @__PURE__ */ jsxRuntime.jsxs(
3841
3872
  "aside",
@@ -3845,14 +3876,43 @@ function Palette({
3845
3876
  isSheet ? "w-full flex-1 bg-background" : "w-72 shrink-0 border-r bg-muted/20"
3846
3877
  ),
3847
3878
  children: [
3848
- showSearch ? /* @__PURE__ */ jsxRuntime.jsx(
3849
- "div",
3850
- {
3851
- className: cn(
3852
- "sticky top-0 z-10 border-b px-3 pt-3 pb-2 backdrop-blur",
3853
- isSheet ? "bg-background" : "bg-muted/20"
3854
- ),
3855
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
3879
+ offersSimple || searchVisible ? (
3880
+ // Two elements so the header is opaque *and* the exact color of the
3881
+ // rail. Painted with the rail's own `bg-muted/20`, it composited over
3882
+ // the scrolled list instead and headings read through the search box
3883
+ // (a `backdrop-blur` softened them, it didn't hide them). Restoring
3884
+ // the `bg-background` base under the tint rebuilds the rail's own two
3885
+ // layers, so the match holds under any theme.
3886
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10 shrink-0 bg-background", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-2 border-b px-3 pt-3 pb-2", isSheet ? "bg-background" : "bg-muted/20"), children: [
3887
+ offersSimple ? (
3888
+ // Titled row: with the sections gone, simple mode's header would
3889
+ // otherwise be a lone right-aligned link over empty space, and
3890
+ // nothing would name what the rail below it holds.
3891
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
3892
+ /* @__PURE__ */ jsxRuntime.jsx(
3893
+ "span",
3894
+ {
3895
+ className: cn(
3896
+ "min-w-0 truncate font-medium uppercase tracking-wide text-muted-foreground",
3897
+ isSheet ? "text-xs" : "text-[11px]"
3898
+ ),
3899
+ children: PALETTE_TITLE
3900
+ }
3901
+ ),
3902
+ /* @__PURE__ */ jsxRuntime.jsx(
3903
+ ModeLink,
3904
+ {
3905
+ advanced,
3906
+ isSheet,
3907
+ onToggle: () => {
3908
+ setQuery("");
3909
+ setAdvancedOpen((v) => !v);
3910
+ }
3911
+ }
3912
+ )
3913
+ ] })
3914
+ ) : null,
3915
+ searchVisible ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
3856
3916
  /* @__PURE__ */ jsxRuntime.jsx(
3857
3917
  lucideReact.Search,
3858
3918
  {
@@ -3873,10 +3933,14 @@ function Palette({
3873
3933
  className: cn(isSheet ? "h-10 pl-8 text-base" : "h-8 pl-7 text-sm")
3874
3934
  }
3875
3935
  )
3876
- ] })
3877
- }
3936
+ ] }) : null
3937
+ ] }) })
3878
3938
  ) : null,
3879
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex flex-col", isSheet ? "px-2 pb-6" : "p-3"), children: visibleSections.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-1 py-2 text-xs text-muted-foreground", children: "No blocks match." }) : visibleSections.map((section) => /* @__PURE__ */ jsxRuntime.jsx(
3939
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex flex-col", isSheet ? "px-2 pb-6" : "p-3"), children: !advanced ? (
3940
+ // Simple mode: a flat list, no section headings — with this few
3941
+ // rows the headings would outnumber the blocks under them.
3942
+ basics.map((v) => /* @__PURE__ */ jsxRuntime.jsx(PaletteItem, { variant: v, onAdd: () => onAddBlock(v.factory()), mode: variant }, v.id))
3943
+ ) : visibleSections.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-1 py-2 text-xs text-muted-foreground", children: "No blocks match." }) : visibleSections.map((section) => /* @__PURE__ */ jsxRuntime.jsx(
3880
3944
  PaletteSection,
3881
3945
  {
3882
3946
  section,
@@ -3891,6 +3955,24 @@ function Palette({
3891
3955
  }
3892
3956
  );
3893
3957
  }
3958
+ function ModeLink({ advanced, isSheet, onToggle }) {
3959
+ return /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
3960
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
3961
+ "button",
3962
+ {
3963
+ type: "button",
3964
+ onClick: onToggle,
3965
+ "aria-label": advanced ? "Basic block palette" : "Advanced block palette",
3966
+ className: cn(
3967
+ "shrink-0 cursor-pointer appearance-none rounded border-0 bg-transparent p-0 font-medium text-primary underline underline-offset-2 transition-colors hover:text-primary/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
3968
+ isSheet ? "min-h-8 text-sm" : "text-xs"
3969
+ ),
3970
+ children: advanced ? "Basic" : "Advanced"
3971
+ }
3972
+ ) }),
3973
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "bottom", align: "end", children: advanced ? BASIC_HELP : ADVANCED_HELP })
3974
+ ] }) });
3975
+ }
3894
3976
  function PaletteSection({
3895
3977
  section,
3896
3978
  defaultOpen,
@@ -9832,6 +9914,48 @@ function EmptyState({
9832
9914
  function DropIndicator2() {
9833
9915
  return /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", className: "pointer-events-none relative my-1 h-0.5 w-full rounded-full bg-primary", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "-left-1 -top-[3px] absolute h-2 w-2 rounded-full bg-primary shadow-[0_0_0_2px_var(--color-background)]" }) });
9834
9916
  }
9917
+ function SlackMark({ className }) {
9918
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9919
+ "svg",
9920
+ {
9921
+ className,
9922
+ viewBox: "0 0 122.8 122.8",
9923
+ xmlns: "http://www.w3.org/2000/svg",
9924
+ "aria-hidden": "true",
9925
+ focusable: "false",
9926
+ children: [
9927
+ /* @__PURE__ */ jsxRuntime.jsx(
9928
+ "path",
9929
+ {
9930
+ fill: "#e01e5a",
9931
+ d: "M25.8 77.6c0 7.1-5.8 12.9-12.9 12.9S0 84.7 0 77.6s5.8-12.9 12.9-12.9h12.9v12.9zm6.5 0c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v32.3c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V77.6z"
9932
+ }
9933
+ ),
9934
+ /* @__PURE__ */ jsxRuntime.jsx(
9935
+ "path",
9936
+ {
9937
+ fill: "#36c5f0",
9938
+ d: "M45.2 25.8c-7.1 0-12.9-5.8-12.9-12.9S38.1 0 45.2 0s12.9 5.8 12.9 12.9v12.9H45.2zm0 6.5c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H12.9C5.8 58.1 0 52.3 0 45.2s5.8-12.9 12.9-12.9h32.3z"
9939
+ }
9940
+ ),
9941
+ /* @__PURE__ */ jsxRuntime.jsx(
9942
+ "path",
9943
+ {
9944
+ fill: "#2eb67d",
9945
+ d: "M97 45.2c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9-5.8 12.9-12.9 12.9H97V45.2zm-6.5 0c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V12.9C64.7 5.8 70.5 0 77.6 0s12.9 5.8 12.9 12.9v32.3z"
9946
+ }
9947
+ ),
9948
+ /* @__PURE__ */ jsxRuntime.jsx(
9949
+ "path",
9950
+ {
9951
+ fill: "#ecb22e",
9952
+ d: "M77.6 97c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9-12.9-5.8-12.9-12.9V97h12.9zm0-6.5c-7.1 0-12.9-5.8-12.9-12.9s5.8-12.9 12.9-12.9h32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H77.6z"
9953
+ }
9954
+ )
9955
+ ]
9956
+ }
9957
+ );
9958
+ }
9835
9959
  var THEME_OPTIONS = [
9836
9960
  { value: "light", label: "Light", Icon: lucideReact.Sun },
9837
9961
  { value: "dark", label: "Dark", Icon: lucideReact.Moon }
@@ -9896,7 +10020,7 @@ function Toolbar2({
9896
10020
  /* @__PURE__ */ jsxRuntime.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: [
9897
10021
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 sm:gap-2", children: [
9898
10022
  loadEnabled ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: onOpenLoad, "aria-label": loadButtonLabel, children: [
9899
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MailSearch, { className: "h-3.5 w-3.5" }),
10023
+ /* @__PURE__ */ jsxRuntime.jsx(SlackMark, { className: "h-3.5 w-3.5" }),
9900
10024
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: loadButtonLabel })
9901
10025
  ] }) : null,
9902
10026
  onOpenPalette ? /* @__PURE__ */ jsxRuntime.jsxs(
@@ -10026,18 +10150,29 @@ function Toolbar2({
10026
10150
  size: "sm",
10027
10151
  onClick: onClear,
10028
10152
  disabled: !canClear,
10029
- className: "hover:bg-destructive/10 hover:text-destructive",
10153
+ className: "group gap-0 hover:bg-destructive/10 hover:text-destructive",
10030
10154
  "aria-label": "Clear all blocks",
10031
10155
  children: [
10032
10156
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" }),
10033
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: "Clear" })
10157
+ /* @__PURE__ */ jsxRuntime.jsx(ExpandingLabel, { children: "Clear" })
10158
+ ]
10159
+ }
10160
+ ),
10161
+ /* @__PURE__ */ jsxRuntime.jsxs(
10162
+ Button,
10163
+ {
10164
+ type: "button",
10165
+ variant: "ghost",
10166
+ size: "sm",
10167
+ onClick: onOpenJson,
10168
+ className: "group gap-0",
10169
+ "aria-label": "View JSON",
10170
+ children: [
10171
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code2, { className: "h-3.5 w-3.5" }),
10172
+ /* @__PURE__ */ jsxRuntime.jsx(ExpandingLabel, { children: "View JSON" })
10034
10173
  ]
10035
10174
  }
10036
10175
  ),
10037
- /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: onOpenJson, "aria-label": "View JSON", children: [
10038
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code2, { className: "h-3.5 w-3.5" }),
10039
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: "View JSON" })
10040
- ] }),
10041
10176
  !showSend ? primaryAction ? (
10042
10177
  // Compose-only mode with a host-owned primary action: same
10043
10178
  // placement and styling as the built-in send button. No icon —
@@ -10147,6 +10282,19 @@ function Toolbar2({
10147
10282
  ] }) : null
10148
10283
  ] });
10149
10284
  }
10285
+ function ExpandingLabel({ children }) {
10286
+ return /* @__PURE__ */ jsxRuntime.jsx(
10287
+ "span",
10288
+ {
10289
+ className: cn(
10290
+ "grid grid-cols-[0fr] transition-[grid-template-columns] duration-200 ease-out",
10291
+ "group-hover:grid-cols-[1fr] group-focus-visible:grid-cols-[1fr]",
10292
+ "motion-reduce:transition-none"
10293
+ ),
10294
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block whitespace-nowrap pl-1.5", children }) })
10295
+ }
10296
+ );
10297
+ }
10150
10298
  function Menu({
10151
10299
  ariaLabel,
10152
10300
  options,
@@ -10329,6 +10477,7 @@ function BlockKitchen(props) {
10329
10477
  confirmUpdateLabel,
10330
10478
  palette,
10331
10479
  disabledBlockTypes,
10480
+ paletteMode,
10332
10481
  showPaletteSearch,
10333
10482
  paletteSearchPlaceholder,
10334
10483
  defaultOpenSections,
@@ -10568,7 +10717,7 @@ function BlockKitchen(props) {
10568
10717
  /* @__PURE__ */ jsxRuntime.jsxs(
10569
10718
  "div",
10570
10719
  {
10571
- className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground",
10720
+ className: "bk-root flex h-full max-h-[var(--bk-max-height,100svh)] w-full flex-col overflow-hidden rounded-md border bg-background text-foreground",
10572
10721
  onKeyDown: handleKeyDown,
10573
10722
  children: [
10574
10723
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -10631,6 +10780,7 @@ function BlockKitchen(props) {
10631
10780
  {
10632
10781
  onAddBlock: (block) => addBlock(block),
10633
10782
  sections: paletteSections,
10783
+ mode: paletteMode,
10634
10784
  showSearch: showPaletteSearch,
10635
10785
  searchPlaceholder: paletteSearchPlaceholder,
10636
10786
  defaultOpenSections
@@ -10683,6 +10833,7 @@ function BlockKitchen(props) {
10683
10833
  setPaletteOpen(false);
10684
10834
  },
10685
10835
  sections: paletteSections,
10836
+ mode: paletteMode,
10686
10837
  showSearch: showPaletteSearch,
10687
10838
  searchPlaceholder: paletteSearchPlaceholder,
10688
10839
  defaultOpenSections,