@tightknitai/block-kitchen 0.10.6 → 0.10.8

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,7 +141,7 @@ 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). |
123
- | `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). |
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: **Header**, **Divider**, **Rich Text Section** and **Image** — 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). |
124
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. |
125
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". |
126
147
  | `paletteSearchPlaceholder` | `string` | no | Placeholder text for the palette search input. Defaults to `'Search blocks…'`. Useful for localization. |
@@ -421,6 +442,8 @@ update against the target from `loadedMessage` / `onLoadedMessageChange`.
421
442
 
422
443
  ## Customizing the palette
423
444
 
445
+ On desktop the palette is a rail down the left-hand side, with a chevron handle on its outer border that hides and restores it — collapsed, the preview gets the full width, and the rail comes back with its open sections and search exactly as they were. Below the `md` breakpoint there's no rail: the toolbar's **Blocks** button opens the palette as a bottom sheet instead, and picking a block closes it.
446
+
424
447
  The default palette ships with curated presets for every supported block type. To narrow what's available, or add your own pre-configured variants (e.g. a "Help footer" section), pass a `palette` array. Define it at module scope (or wrap in `useMemo`) so it stays referentially stable across renders.
425
448
 
426
449
  ```tsx
@@ -455,13 +478,13 @@ Variant `id`s must be unique across the array — the drag-drop lookup keys by i
455
478
 
456
479
  ### Simple vs advanced palette
457
480
 
458
- 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).
481
+ 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 short starter list instead — no sections, no search — behind an **Advanced** link that swaps in the full palette (and back).
459
482
 
460
483
  ```tsx
461
484
  <BlockKitchen paletteMode="simple" {...rest} />
462
485
  ```
463
486
 
464
- 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:
487
+ 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 four — **Header**, **Divider**, **Rich Text Section** and **Image** — listed in palette order. A custom palette that wants its own starter blocks says so:
465
488
 
466
489
  ```tsx
467
490
  const PALETTE: readonly PaletteSection[] = [
@@ -476,8 +499,12 @@ const PALETTE: readonly PaletteSection[] = [
476
499
  ];
477
500
  ```
478
501
 
502
+ The flat list has no section headings to lean on, so a variant whose label only reads in context can give simple mode a self-describing one with `basicLabel` — that's how the built-in image variant (**No title**, under **Image**) shows up as **Image** there.
503
+
479
504
  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.
480
505
 
506
+ The builder remembers which side of that switch the user is on, so the mobile sheet — which is torn down every time it closes — reopens on the full palette rather than sending them back through the **Advanced** link on every block.
507
+
481
508
  ## Templates (`TemplatePicker`)
482
509
 
483
510
  `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.
@@ -1,5 +1,5 @@
1
- import { S as SupportedBlock } from './palette-QbOx7kAa.cjs';
2
- export { a1 as SupportedBlockType } from './palette-QbOx7kAa.cjs';
1
+ import { S as SupportedBlock } from './palette-CvSGn2z6.cjs';
2
+ export { a1 as SupportedBlockType } from './palette-CvSGn2z6.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-QbOx7kAa.js';
2
- export { a1 as SupportedBlockType } from './palette-QbOx7kAa.js';
1
+ import { S as SupportedBlock } from './palette-CvSGn2z6.js';
2
+ export { a1 as SupportedBlockType } from './palette-CvSGn2z6.js';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
package/dist/index.cjs CHANGED
@@ -4,10 +4,10 @@ var core = require('@dnd-kit/core');
4
4
  var sortable = require('@dnd-kit/sortable');
5
5
  var lucideReact = require('lucide-react');
6
6
  var React = require('react');
7
- var jsxRuntime = require('react/jsx-runtime');
8
- var DialogPrimitive = require('@radix-ui/react-dialog');
9
7
  var clsx = require('clsx');
10
8
  var tailwindMerge = require('tailwind-merge');
9
+ var jsxRuntime = require('react/jsx-runtime');
10
+ var DialogPrimitive = require('@radix-ui/react-dialog');
11
11
  var TooltipPrimitive = require('@radix-ui/react-tooltip');
12
12
  var slackBlockKitValidator = require('@tightknitai/slack-block-kit-validator');
13
13
  var nanoid = require('nanoid');
@@ -53,6 +53,9 @@ var Link__default = /*#__PURE__*/_interopDefault(Link);
53
53
  var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
54
54
 
55
55
  // src/components/block-kitchen.tsx
56
+ function cn(...inputs) {
57
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
58
+ }
56
59
 
57
60
  // src/lib/container-blocks.ts
58
61
  var CONTAINER_CHILD_TYPES = /* @__PURE__ */ new Set([
@@ -734,6 +737,7 @@ var defaultPalette = [
734
737
  {
735
738
  id: "structure_header",
736
739
  label: "Header",
740
+ basic: true,
737
741
  factory: () => ({
738
742
  type: "header",
739
743
  text: { type: "plain_text", text: "Heading text", emoji: true }
@@ -742,6 +746,7 @@ var defaultPalette = [
742
746
  {
743
747
  id: "structure_divider",
744
748
  label: "Divider",
749
+ basic: true,
745
750
  factory: () => ({ type: "divider" })
746
751
  },
747
752
  {
@@ -849,8 +854,9 @@ var defaultPalette = [
849
854
  {
850
855
  id: "rich_text_section",
851
856
  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.
857
+ // The block a message is mostly made of, and so one of the handful
858
+ // simple mode offers — listed there without its section heading,
859
+ // hence the self-describing label.
854
860
  basic: true,
855
861
  factory: () => ({
856
862
  type: "rich_text",
@@ -887,6 +893,11 @@ var defaultPalette = [
887
893
  {
888
894
  id: "image_no_title",
889
895
  label: "No title",
896
+ // The plain image is the one an ordinary message reaches for, so it
897
+ // carries simple mode's image slot — under its own name there, with
898
+ // no "Image" heading overhead to lend "No title" its meaning.
899
+ basic: true,
900
+ basicLabel: "Image",
890
901
  factory: () => ({
891
902
  type: "image",
892
903
  image_url: "https://placehold.co/600x300?text=Image",
@@ -1709,9 +1720,6 @@ function labelForBlockType(type) {
1709
1720
  return "Container";
1710
1721
  }
1711
1722
  }
1712
- function cn(...inputs) {
1713
- return tailwindMerge.twMerge(clsx.clsx(inputs));
1714
- }
1715
1723
  var Sheet = DialogPrimitive__namespace.Root;
1716
1724
  var SheetOverlay = React__namespace.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
1717
1725
  DialogPrimitive__namespace.Overlay,
@@ -3812,14 +3820,14 @@ function isDefaultOpen(sectionName, config) {
3812
3820
  return config.includes(sectionName);
3813
3821
  }
3814
3822
  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.";
3823
+ var ADVANCED_HELP = "Browse every Slack block type \u2014 sections, images, buttons, inputs, tables and more.";
3824
+ var BASIC_HELP = "Go back to the short list: the few blocks an ordinary message is made of.";
3817
3825
  function basicVariants(sections) {
3818
3826
  const out = [];
3819
3827
  for (const section of sections) {
3820
3828
  for (const variant of section.variants) {
3821
3829
  if (variant.basic) {
3822
- out.push(variant);
3830
+ out.push(variant.basicLabel ? { ...variant, label: variant.basicLabel } : variant);
3823
3831
  }
3824
3832
  }
3825
3833
  }
@@ -3848,10 +3856,12 @@ function Palette({
3848
3856
  showSearch = true,
3849
3857
  searchPlaceholder = "Search blocks\u2026",
3850
3858
  mode = "advanced",
3859
+ defaultAdvanced = false,
3860
+ onAdvancedChange,
3851
3861
  variant = "aside"
3852
3862
  }) {
3853
3863
  const [query, setQuery] = React.useState("");
3854
- const [advancedOpen, setAdvancedOpen] = React.useState(false);
3864
+ const [advancedOpen, setAdvancedOpen] = React.useState(defaultAdvanced);
3855
3865
  const [prevMode, setPrevMode] = React.useState(mode);
3856
3866
  if (prevMode !== mode) {
3857
3867
  setPrevMode(mode);
@@ -3873,69 +3883,70 @@ function Palette({
3873
3883
  {
3874
3884
  className: cn(
3875
3885
  "flex min-h-0 flex-col overflow-x-hidden overflow-y-auto",
3876
- isSheet ? "w-full flex-1 bg-background" : "w-72 shrink-0 border-r bg-muted/20"
3886
+ isSheet ? "w-full flex-1 bg-background" : "w-56 shrink-0 border-r bg-muted/20"
3877
3887
  ),
3878
3888
  children: [
3879
- offersSimple || searchVisible ? /* @__PURE__ */ jsxRuntime.jsxs(
3880
- "div",
3881
- {
3882
- className: cn(
3883
- "sticky top-0 z-10 flex flex-col gap-2 border-b px-3 pt-3 pb-2 backdrop-blur",
3884
- isSheet ? "bg-background" : "bg-muted/20"
3885
- ),
3886
- 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: [
3889
+ offersSimple || searchVisible ? (
3890
+ // Two elements so the header is opaque *and* the exact color of the
3891
+ // rail. Painted with the rail's own `bg-muted/20`, it composited over
3892
+ // the scrolled list instead and headings read through the search box
3893
+ // (a `backdrop-blur` softened them, it didn't hide them). Restoring
3894
+ // the `bg-background` base under the tint rebuilds the rail's own two
3895
+ // layers, so the match holds under any theme.
3896
+ /* @__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: [
3897
+ offersSimple ? (
3898
+ // Titled row: with the sections gone, simple mode's header would
3899
+ // otherwise be a lone right-aligned link over empty space, and
3900
+ // nothing would name what the rail below it holds.
3901
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
3916
3902
  /* @__PURE__ */ jsxRuntime.jsx(
3917
- lucideReact.Search,
3903
+ "span",
3918
3904
  {
3919
3905
  className: cn(
3920
- "pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground",
3921
- isSheet ? "h-4 w-4" : "h-3.5 w-3.5"
3922
- )
3906
+ "min-w-0 truncate font-medium uppercase tracking-wide text-muted-foreground",
3907
+ isSheet ? "text-xs" : "text-[11px]"
3908
+ ),
3909
+ children: PALETTE_TITLE
3923
3910
  }
3924
3911
  ),
3925
3912
  /* @__PURE__ */ jsxRuntime.jsx(
3926
- Input,
3913
+ ModeLink,
3927
3914
  {
3928
- type: "search",
3929
- value: query,
3930
- onChange: (e) => setQuery(e.target.value),
3931
- placeholder: searchPlaceholder,
3932
- "aria-label": searchPlaceholder,
3933
- className: cn(isSheet ? "h-10 pl-8 text-base" : "h-8 pl-7 text-sm")
3915
+ advanced,
3916
+ isSheet,
3917
+ onToggle: () => {
3918
+ setQuery("");
3919
+ const next = !advancedOpen;
3920
+ setAdvancedOpen(next);
3921
+ onAdvancedChange?.(next);
3922
+ }
3934
3923
  }
3935
3924
  )
3936
- ] }) : null
3937
- ]
3938
- }
3925
+ ] })
3926
+ ) : null,
3927
+ searchVisible ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
3928
+ /* @__PURE__ */ jsxRuntime.jsx(
3929
+ lucideReact.Search,
3930
+ {
3931
+ className: cn(
3932
+ "pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground",
3933
+ isSheet ? "h-4 w-4" : "h-3.5 w-3.5"
3934
+ )
3935
+ }
3936
+ ),
3937
+ /* @__PURE__ */ jsxRuntime.jsx(
3938
+ Input,
3939
+ {
3940
+ type: "search",
3941
+ value: query,
3942
+ onChange: (e) => setQuery(e.target.value),
3943
+ placeholder: searchPlaceholder,
3944
+ "aria-label": searchPlaceholder,
3945
+ className: cn(isSheet ? "h-10 pl-8 text-base" : "h-8 pl-7 text-sm")
3946
+ }
3947
+ )
3948
+ ] }) : null
3949
+ ] }) })
3939
3950
  ) : null,
3940
3951
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex flex-col", isSheet ? "px-2 pb-6" : "p-3"), children: !advanced ? (
3941
3952
  // Simple mode: a flat list, no section headings — with this few
@@ -4035,8 +4046,8 @@ function PaletteItem({
4035
4046
  isDragging && "opacity-50"
4036
4047
  ),
4037
4048
  children: [
4038
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:bg-primary/10 group-hover:text-primary", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" }) }),
4039
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: variant.label })
4049
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: variant.label }),
4050
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:bg-primary/10 group-hover:text-primary", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" }) })
4040
4051
  ]
4041
4052
  }
4042
4053
  );
@@ -4046,7 +4057,7 @@ function PaletteItem({
4046
4057
  {
4047
4058
  ref: setNodeRef,
4048
4059
  className: cn(
4049
- "group flex items-center gap-2 rounded px-2 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground has-[:focus-visible]:bg-accent has-[:focus-visible]:text-foreground",
4060
+ "group flex items-center gap-2 rounded px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground has-[:focus-visible]:bg-accent has-[:focus-visible]:text-foreground",
4050
4061
  isDragging && "opacity-50"
4051
4062
  ),
4052
4063
  children: [
@@ -4054,7 +4065,7 @@ function PaletteItem({
4054
4065
  "div",
4055
4066
  {
4056
4067
  ref: setActivatorNodeRef,
4057
- className: "-my-1.5 flex min-w-0 flex-1 cursor-grab items-center gap-2 rounded py-1.5 active:cursor-grabbing focus-visible:outline-none",
4068
+ className: "-my-1 flex min-w-0 flex-1 cursor-grab items-center gap-2 rounded py-1 active:cursor-grabbing focus-visible:outline-none",
4058
4069
  ...attributes,
4059
4070
  ...listeners,
4060
4071
  children: [
@@ -10031,7 +10042,7 @@ function Toolbar2({
10031
10042
  variant: "ghost",
10032
10043
  size: "sm",
10033
10044
  onClick: onOpenPalette,
10034
- className: "md:hidden",
10045
+ className: "gap-1 md:hidden",
10035
10046
  "aria-label": "Add a block",
10036
10047
  children: [
10037
10048
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }),
@@ -10151,18 +10162,29 @@ function Toolbar2({
10151
10162
  size: "sm",
10152
10163
  onClick: onClear,
10153
10164
  disabled: !canClear,
10154
- className: "hover:bg-destructive/10 hover:text-destructive",
10165
+ className: "group gap-0 hover:bg-destructive/10 hover:text-destructive",
10155
10166
  "aria-label": "Clear all blocks",
10156
10167
  children: [
10157
10168
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" }),
10158
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: "Clear" })
10169
+ /* @__PURE__ */ jsxRuntime.jsx(ExpandingLabel, { children: "Clear" })
10170
+ ]
10171
+ }
10172
+ ),
10173
+ /* @__PURE__ */ jsxRuntime.jsxs(
10174
+ Button,
10175
+ {
10176
+ type: "button",
10177
+ variant: "ghost",
10178
+ size: "sm",
10179
+ onClick: onOpenJson,
10180
+ className: "group gap-0",
10181
+ "aria-label": "View JSON",
10182
+ children: [
10183
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code2, { className: "h-3.5 w-3.5" }),
10184
+ /* @__PURE__ */ jsxRuntime.jsx(ExpandingLabel, { children: "View JSON" })
10159
10185
  ]
10160
10186
  }
10161
10187
  ),
10162
- /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: onOpenJson, "aria-label": "View JSON", children: [
10163
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code2, { className: "h-3.5 w-3.5" }),
10164
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden md:inline", children: "View JSON" })
10165
- ] }),
10166
10188
  !showSend ? primaryAction ? (
10167
10189
  // Compose-only mode with a host-owned primary action: same
10168
10190
  // placement and styling as the built-in send button. No icon —
@@ -10272,6 +10294,19 @@ function Toolbar2({
10272
10294
  ] }) : null
10273
10295
  ] });
10274
10296
  }
10297
+ function ExpandingLabel({ children }) {
10298
+ return /* @__PURE__ */ jsxRuntime.jsx(
10299
+ "span",
10300
+ {
10301
+ className: cn(
10302
+ "grid grid-cols-[0fr] transition-[grid-template-columns] duration-200 ease-out",
10303
+ "group-hover:grid-cols-[1fr] group-focus-visible:grid-cols-[1fr]",
10304
+ "motion-reduce:transition-none"
10305
+ ),
10306
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block whitespace-nowrap pl-1.5", children }) })
10307
+ }
10308
+ );
10309
+ }
10275
10310
  function Menu({
10276
10311
  ariaLabel,
10277
10312
  options,
@@ -10563,12 +10598,20 @@ function BlockKitchen(props) {
10563
10598
  useNotifyOnChange(activeEditTarget, loadedMessageKey, loading?.onLoadedMessageChange);
10564
10599
  const [issuesOpen, setIssuesOpen] = React.useState(false);
10565
10600
  const [paletteOpen, setPaletteOpen] = React.useState(false);
10601
+ const [paletteCollapsed, setPaletteCollapsed] = React.useState(false);
10602
+ const [paletteAdvanced, setPaletteAdvanced] = React.useState(false);
10603
+ const [prevPaletteMode, setPrevPaletteMode] = React.useState(paletteMode);
10604
+ if (prevPaletteMode !== paletteMode) {
10605
+ setPrevPaletteMode(paletteMode);
10606
+ setPaletteAdvanced(false);
10607
+ }
10566
10608
  const [openBlockId, setOpenBlockId] = React.useState(null);
10567
10609
  const [uncontrolledPreviewTheme, setUncontrolledPreviewTheme] = React.useState(defaultPreviewTheme);
10568
10610
  const isPreviewThemeControlled = controlledPreviewTheme !== void 0;
10569
10611
  const previewTheme = controlledPreviewTheme ?? uncontrolledPreviewTheme;
10570
10612
  const [previewSurface, setPreviewSurface] = React.useState(allowedSurfaces[0]);
10571
10613
  const [activePaletteVariantId, setActivePaletteVariantId] = React.useState(null);
10614
+ const paletteSheetRef = React.useRef(null);
10572
10615
  const isMobile = useIsMobile();
10573
10616
  const mergedPreviewHooks = React.useMemo(() => {
10574
10617
  if (!customEmojis || customEmojis.length === 0) {
@@ -10694,7 +10737,7 @@ function BlockKitchen(props) {
10694
10737
  /* @__PURE__ */ jsxRuntime.jsxs(
10695
10738
  "div",
10696
10739
  {
10697
- className: "bk-root flex h-full w-full flex-col overflow-hidden rounded-md border bg-background text-foreground",
10740
+ 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",
10698
10741
  onKeyDown: handleKeyDown,
10699
10742
  children: [
10700
10743
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -10752,17 +10795,48 @@ function BlockKitchen(props) {
10752
10795
  }
10753
10796
  ),
10754
10797
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
10755
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden min-h-0 md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(
10756
- Palette,
10757
- {
10758
- onAddBlock: (block) => addBlock(block),
10759
- sections: paletteSections,
10760
- mode: paletteMode,
10761
- showSearch: showPaletteSearch,
10762
- searchPlaceholder: paletteSearchPlaceholder,
10763
- defaultOpenSections
10764
- }
10765
- ) }),
10798
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative hidden min-h-0 shrink-0 md:flex", children: [
10799
+ /* @__PURE__ */ jsxRuntime.jsx("div", { hidden: paletteCollapsed, className: cn("min-h-0", !paletteCollapsed && "flex"), children: /* @__PURE__ */ jsxRuntime.jsx(
10800
+ Palette,
10801
+ {
10802
+ onAddBlock: (block) => addBlock(block),
10803
+ sections: paletteSections,
10804
+ mode: paletteMode,
10805
+ showSearch: showPaletteSearch,
10806
+ searchPlaceholder: paletteSearchPlaceholder,
10807
+ defaultOpenSections,
10808
+ defaultAdvanced: paletteAdvanced,
10809
+ onAdvancedChange: setPaletteAdvanced
10810
+ }
10811
+ ) }),
10812
+ paletteCollapsed ? (
10813
+ // In a host that hands the builder no height of its own,
10814
+ // the rail is what the shell takes its height from (see
10815
+ // the shell's `max-h` note above) — so hiding it would
10816
+ // shrink the whole frame to the height of the message and
10817
+ // grow it back on the way out. This stands in for the rail
10818
+ // at no width. Inert to everything but layout.
10819
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, className: "w-0 shrink-0 md:h-[var(--bk-max-height,100svh)]" })
10820
+ ) : null,
10821
+ /* @__PURE__ */ jsxRuntime.jsx(
10822
+ "button",
10823
+ {
10824
+ type: "button",
10825
+ onClick: () => setPaletteCollapsed((v) => !v),
10826
+ "aria-expanded": !paletteCollapsed,
10827
+ "aria-label": paletteCollapsed ? "Show block palette" : "Hide block palette",
10828
+ title: paletteCollapsed ? "Show block palette" : "Hide block palette",
10829
+ className: cn(
10830
+ // A hairline outline in the rail's own border color, so
10831
+ // the handle reads as part of the edge it rides rather
10832
+ // than a chevron floating loose over the border line.
10833
+ "absolute top-1/2 z-10 flex h-10 w-4 -translate-y-1/2 cursor-pointer appearance-none items-center justify-center rounded-sm border bg-background p-0 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
10834
+ paletteCollapsed ? "left-0" : "right-0 translate-x-1/2"
10835
+ ),
10836
+ children: paletteCollapsed ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4", strokeWidth: 1.5 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-4 w-4", strokeWidth: 1.5 })
10837
+ }
10838
+ )
10839
+ ] }),
10766
10840
  /* @__PURE__ */ jsxRuntime.jsx(
10767
10841
  Surface,
10768
10842
  {
@@ -10795,8 +10869,13 @@ function BlockKitchen(props) {
10795
10869
  /* @__PURE__ */ jsxRuntime.jsx(Sheet, { open: paletteOpen && isMobile, onOpenChange: setPaletteOpen, children: /* @__PURE__ */ jsxRuntime.jsxs(
10796
10870
  SheetContent,
10797
10871
  {
10872
+ ref: paletteSheetRef,
10798
10873
  side: "bottom",
10799
10874
  className: "bk-portal-content flex h-[85svh] max-h-[85svh] flex-col gap-3 p-0 sm:max-w-none",
10875
+ onOpenAutoFocus: (e) => {
10876
+ e.preventDefault();
10877
+ paletteSheetRef.current?.focus();
10878
+ },
10800
10879
  children: [
10801
10880
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 px-4 pt-5", children: [
10802
10881
  /* @__PURE__ */ jsxRuntime.jsx(SheetTitle, { children: "Add a block" }),
@@ -10814,6 +10893,8 @@ function BlockKitchen(props) {
10814
10893
  showSearch: showPaletteSearch,
10815
10894
  searchPlaceholder: paletteSearchPlaceholder,
10816
10895
  defaultOpenSections,
10896
+ defaultAdvanced: paletteAdvanced,
10897
+ onAdvancedChange: setPaletteAdvanced,
10817
10898
  variant: "sheet"
10818
10899
  }
10819
10900
  ) })