@tightknitai/block-kitchen 0.10.26 → 0.10.27

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.cjs CHANGED
@@ -1958,6 +1958,36 @@ function CustomEmojiProvider({ customEmojis, children }) {
1958
1958
  function useCustomEmojis() {
1959
1959
  return React.useContext(CustomEmojiContext);
1960
1960
  }
1961
+ var DEFAULT_IMAGE_UPLOAD_ACCEPT = ["image/png", "image/jpeg", "image/gif"];
1962
+ var ImageUploadContext = React.createContext(null);
1963
+ function ImageUploadProvider({
1964
+ imageUpload,
1965
+ children
1966
+ }) {
1967
+ const uploadImage = imageUpload?.uploadImage;
1968
+ const accept = imageUpload?.accept;
1969
+ const value = React.useMemo(
1970
+ () => uploadImage ? { uploadImage, accept: accept ?? DEFAULT_IMAGE_UPLOAD_ACCEPT } : null,
1971
+ [uploadImage, accept]
1972
+ );
1973
+ return /* @__PURE__ */ jsxRuntime.jsx(ImageUploadContext.Provider, { value, children });
1974
+ }
1975
+ function useImageUpload() {
1976
+ return React.useContext(ImageUploadContext);
1977
+ }
1978
+ function isAcceptedImage(file, accept) {
1979
+ return accept.includes(file.type);
1980
+ }
1981
+ function uploadErrorMessage(error) {
1982
+ return error instanceof Error && error.message ? error.message : "Upload failed. Try again.";
1983
+ }
1984
+ function isFileDrag(dataTransfer) {
1985
+ return Boolean(dataTransfer && Array.from(dataTransfer.types).includes("Files"));
1986
+ }
1987
+ function formatList(accept) {
1988
+ const names = accept.map((type) => type.replace(/^image\//, "").toUpperCase());
1989
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "";
1990
+ }
1961
1991
  var MentionsContext = React.createContext(null);
1962
1992
  function MentionsProvider({ mentions, children }) {
1963
1993
  return /* @__PURE__ */ jsxRuntime.jsx(MentionsContext.Provider, { value: mentions ?? null, children });
@@ -3269,6 +3299,13 @@ var PopoverContent = React__namespace.forwardRef(({ className, align = "start",
3269
3299
  }
3270
3300
  ) }));
3271
3301
  PopoverContent.displayName = PopoverPrimitive__namespace.Content.displayName;
3302
+ function returnFocusUnlessMoved(event) {
3303
+ const active = document.activeElement;
3304
+ const closing = event.target instanceof Node ? event.target : null;
3305
+ if (active && active !== document.body && !closing?.contains(active)) {
3306
+ event.preventDefault();
3307
+ }
3308
+ }
3272
3309
  function matchesQuery(name, query) {
3273
3310
  const needle = query.replace(/^\s*#\s*/, "").trim().toLowerCase();
3274
3311
  return needle === "" || name.toLowerCase().includes(needle);
@@ -4760,6 +4797,73 @@ function SendDialog({
4760
4797
  ] })
4761
4798
  ] }) });
4762
4799
  }
4800
+ var ALT_TEXT_MAX_LENGTH = 2e3;
4801
+ function altTextFromFileName(file) {
4802
+ const name = file.name.replace(/\.[^.]+$/, "").trim();
4803
+ return (name || "Image").slice(0, ALT_TEXT_MAX_LENGTH);
4804
+ }
4805
+ function useCanvasImageDrop({
4806
+ upload,
4807
+ enabled,
4808
+ onAddBlock
4809
+ }) {
4810
+ const [dragging, setDragging] = React.useState(false);
4811
+ const [pending, setPending] = React.useState(0);
4812
+ const [error, setError] = React.useState(null);
4813
+ const onAddBlockRef = React.useRef(onAddBlock);
4814
+ onAddBlockRef.current = onAddBlock;
4815
+ const addFiles = async (activeUpload, files) => {
4816
+ const images = files.filter((file) => isAcceptedImage(file, activeUpload.accept));
4817
+ const skipped = files.length - images.length;
4818
+ setError(
4819
+ skipped > 0 ? `${skipped === 1 ? "1 file was" : `${skipped} files were`} skipped. Only ${formatList(activeUpload.accept)} images can be added.` : null
4820
+ );
4821
+ if (images.length === 0) {
4822
+ return;
4823
+ }
4824
+ setPending((count) => count + images.length);
4825
+ const results = await Promise.allSettled(images.map((file) => activeUpload.uploadImage(file)));
4826
+ setPending((count) => count - images.length);
4827
+ const failures = [];
4828
+ results.forEach((result, idx) => {
4829
+ const file = images[idx];
4830
+ if (result.status === "fulfilled") {
4831
+ onAddBlockRef.current?.({ type: "image", image_url: result.value.url, alt_text: altTextFromFileName(file) });
4832
+ } else {
4833
+ failures.push(`Couldn't upload "${file.name}": ${uploadErrorMessage(result.reason)}`);
4834
+ }
4835
+ });
4836
+ if (failures.length > 0) {
4837
+ setError(failures.join(" "));
4838
+ }
4839
+ };
4840
+ const isOwnEvent = (e) => isFileDrag(e.dataTransfer) && e.currentTarget.contains(e.target);
4841
+ const handlers = upload && enabled && onAddBlock ? {
4842
+ onDragOver: (e) => {
4843
+ if (!isOwnEvent(e)) {
4844
+ return;
4845
+ }
4846
+ e.preventDefault();
4847
+ e.dataTransfer.dropEffect = "copy";
4848
+ setDragging(true);
4849
+ },
4850
+ onDragLeave: (e) => {
4851
+ const next = e.relatedTarget;
4852
+ if (!next || !e.currentTarget.contains(next)) {
4853
+ setDragging(false);
4854
+ }
4855
+ },
4856
+ onDrop: (e) => {
4857
+ if (!isOwnEvent(e)) {
4858
+ return;
4859
+ }
4860
+ e.preventDefault();
4861
+ setDragging(false);
4862
+ void addFiles(upload, Array.from(e.dataTransfer.files));
4863
+ }
4864
+ } : void 0;
4865
+ return { handlers, dragging, pending, error, dismissError: () => setError(null) };
4866
+ }
4763
4867
  function BlockActionButtons({
4764
4868
  actions,
4765
4869
  position,
@@ -5014,6 +5118,129 @@ function AlertEditor({ block, onChange }) {
5014
5118
  ) })
5015
5119
  ] });
5016
5120
  }
5121
+ function ImageUrlInput({
5122
+ id,
5123
+ value,
5124
+ placeholder,
5125
+ onChange
5126
+ }) {
5127
+ const upload = useImageUpload();
5128
+ const input = /* @__PURE__ */ jsxRuntime.jsx(Input, { id, type: "url", value, placeholder, onChange: (e) => onChange(e.target.value) });
5129
+ if (!upload) {
5130
+ return input;
5131
+ }
5132
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5", children: [
5133
+ /* @__PURE__ */ jsxRuntime.jsx(UploadDropZone, { upload, inputId: id, onUploaded: onChange, variant: "inline" }),
5134
+ input
5135
+ ] });
5136
+ }
5137
+ function UploadDropZone({
5138
+ upload,
5139
+ inputId,
5140
+ onUploaded,
5141
+ variant
5142
+ }) {
5143
+ const [uploading, setUploading] = React.useState(false);
5144
+ const [dragging, setDragging] = React.useState(false);
5145
+ const [error, setError] = React.useState(null);
5146
+ const fileInputRef = React.useRef(null);
5147
+ const onUploadedRef = React.useRef(onUploaded);
5148
+ onUploadedRef.current = onUploaded;
5149
+ const uploadFile = async (file) => {
5150
+ if (!isAcceptedImage(file, upload.accept)) {
5151
+ setError(`"${file.name}" isn't a supported image. Use ${formatList(upload.accept)}.`);
5152
+ return;
5153
+ }
5154
+ setError(null);
5155
+ setUploading(true);
5156
+ try {
5157
+ const { url } = await upload.uploadImage(file);
5158
+ onUploadedRef.current(url);
5159
+ } catch (err) {
5160
+ setError(uploadErrorMessage(err));
5161
+ } finally {
5162
+ setUploading(false);
5163
+ }
5164
+ };
5165
+ const onDragOver = (e) => {
5166
+ if (!isFileDrag(e.dataTransfer)) {
5167
+ return;
5168
+ }
5169
+ e.preventDefault();
5170
+ e.stopPropagation();
5171
+ setDragging(true);
5172
+ };
5173
+ const onDrop = (e) => {
5174
+ if (!isFileDrag(e.dataTransfer)) {
5175
+ return;
5176
+ }
5177
+ e.preventDefault();
5178
+ e.stopPropagation();
5179
+ setDragging(false);
5180
+ const [file] = Array.from(e.dataTransfer.files);
5181
+ if (file && !uploading) {
5182
+ void uploadFile(file);
5183
+ }
5184
+ };
5185
+ const fileInput = /* @__PURE__ */ jsxRuntime.jsx(
5186
+ "input",
5187
+ {
5188
+ ref: fileInputRef,
5189
+ id: `${inputId}-file`,
5190
+ type: "file",
5191
+ accept: upload.accept.join(","),
5192
+ className: "hidden",
5193
+ tabIndex: -1,
5194
+ "aria-hidden": "true",
5195
+ onChange: (e) => {
5196
+ const file = e.target.files?.[0];
5197
+ e.target.value = "";
5198
+ if (file) {
5199
+ void uploadFile(file);
5200
+ }
5201
+ }
5202
+ }
5203
+ );
5204
+ const pickButton = (label) => /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", size: "sm", className: "h-7", disabled: uploading, onClick: () => fileInputRef.current?.click(), children: label });
5205
+ const dropHandlers = { onDragOver, onDragLeave: () => setDragging(false), onDrop };
5206
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5207
+ variant === "panel" ? /* @__PURE__ */ jsxRuntime.jsxs(
5208
+ "section",
5209
+ {
5210
+ "aria-label": "Upload an image",
5211
+ ...dropHandlers,
5212
+ className: cn(
5213
+ "flex flex-col items-center justify-center gap-2 rounded-md border border-dashed px-4 py-6 text-center text-xs text-muted-foreground transition-colors",
5214
+ dragging && "border-primary bg-primary/5 text-primary"
5215
+ ),
5216
+ children: [
5217
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { className: cn("h-5 w-5", uploading && "animate-pulse"), "aria-hidden": "true" }),
5218
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-live": "polite", children: uploading ? "Uploading\u2026" : dragging ? "Drop to upload" : "Drag an image here, or" }),
5219
+ pickButton("Choose file"),
5220
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px]", children: formatList(upload.accept) }),
5221
+ fileInput
5222
+ ]
5223
+ }
5224
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
5225
+ "section",
5226
+ {
5227
+ "aria-label": "Upload an image",
5228
+ ...dropHandlers,
5229
+ className: cn(
5230
+ "flex items-center gap-2 rounded-md border border-dashed px-3 py-2 text-[11px] text-muted-foreground transition-colors",
5231
+ dragging && "border-primary bg-primary/5 text-primary"
5232
+ ),
5233
+ children: [
5234
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { className: cn("h-3.5 w-3.5 shrink-0", uploading && "animate-pulse"), "aria-hidden": "true" }),
5235
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", "aria-live": "polite", children: uploading ? "Uploading\u2026" : dragging ? "Drop to upload" : "Drop an image here, or paste a URL below" }),
5236
+ pickButton("Upload image"),
5237
+ fileInput
5238
+ ]
5239
+ }
5240
+ ),
5241
+ error ? /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "text-[11px] leading-snug text-destructive", children: error }) : null
5242
+ ] });
5243
+ }
5017
5244
  function CardEditor({
5018
5245
  block,
5019
5246
  onChange,
@@ -5120,13 +5347,12 @@ function ImageFields({
5120
5347
  };
5121
5348
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 rounded-md border bg-muted/20 p-3", children: [
5122
5349
  /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label, help, htmlFor: `${idPrefix}-url`, children: /* @__PURE__ */ jsxRuntime.jsx(
5123
- Input,
5350
+ ImageUrlInput,
5124
5351
  {
5125
5352
  id: `${idPrefix}-url`,
5126
- type: "url",
5127
5353
  value: url,
5128
5354
  placeholder: "e.g. https://example.com/image.png",
5129
- onChange: (e) => update({ url: e.target.value })
5355
+ onChange: (next) => update({ url: next })
5130
5356
  }
5131
5357
  ) }),
5132
5358
  /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Alt text", help: "Describes the image for screen readers.", htmlFor: `${idPrefix}-alt`, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -7686,15 +7912,14 @@ function ContainerEditor({ block, onChange }) {
7686
7912
  }
7687
7913
  ) : null,
7688
7914
  /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Icon URL", help: "Optional image shown left of the title.", htmlFor: "container-icon", children: /* @__PURE__ */ jsxRuntime.jsx(
7689
- Input,
7915
+ ImageUrlInput,
7690
7916
  {
7691
7917
  id: "container-icon",
7692
- type: "url",
7693
7918
  value: iconUrl,
7694
7919
  placeholder: "https://example.com/icon.png",
7695
- onChange: (e) => onChange({
7920
+ onChange: (url) => onChange({
7696
7921
  ...block,
7697
- icon: e.target.value ? { type: "image", image_url: e.target.value, alt_text: iconAlt || "Container icon" } : void 0
7922
+ icon: url ? { type: "image", image_url: url, alt_text: iconAlt || "Container icon" } : void 0
7698
7923
  })
7699
7924
  }
7700
7925
  ) }),
@@ -8192,13 +8417,12 @@ function ImageItemCard({
8192
8417
  help: "A publicly accessible image URL. Best at small sizes (around 20px).",
8193
8418
  htmlFor: `ctx-img-url-${index}`,
8194
8419
  children: /* @__PURE__ */ jsxRuntime.jsx(
8195
- Input,
8420
+ ImageUrlInput,
8196
8421
  {
8197
8422
  id: `ctx-img-url-${index}`,
8198
- type: "url",
8199
8423
  value: element.image_url,
8200
8424
  placeholder: "e.g. https://example.com/avatar.png",
8201
- onChange: (e) => onChange({ ...element, image_url: e.target.value })
8425
+ onChange: (url) => onChange({ ...element, image_url: url })
8202
8426
  }
8203
8427
  )
8204
8428
  }
@@ -8940,36 +9164,44 @@ function HeaderEditor({ block, onChange }) {
8940
9164
  ] });
8941
9165
  }
8942
9166
  function ImageEditor({ block, onChange }) {
8943
- const source = "slack_file" in block ? "slack_file" : "url";
9167
+ const upload = useImageUpload();
9168
+ const [urlSource, setUrlSource] = React.useState(upload ? "upload" : "url");
9169
+ const isSlackFile = "slack_file" in block;
9170
+ const source = isSlackFile ? "slack_file" : upload ? urlSource : "url";
8944
9171
  const titleText = block.title?.text ?? "";
8945
9172
  const setSource = (next) => {
8946
9173
  if (next === source) {
8947
9174
  return;
8948
9175
  }
8949
- if (next === "url") {
9176
+ if (next !== "slack_file") {
9177
+ setUrlSource(next);
9178
+ }
9179
+ if (next !== "slack_file" && isSlackFile) {
8950
9180
  const { slack_file: _omit, ...rest } = block;
8951
9181
  onChange({ image_url: "", ...rest });
8952
- } else {
9182
+ } else if (next === "slack_file") {
8953
9183
  const { image_url: _omit, ...rest } = block;
8954
9184
  onChange({ slack_file: { id: "" }, ...rest });
8955
9185
  }
8956
9186
  };
9187
+ const sourceOptions = [
9188
+ ...upload ? [["upload", "Upload image"]] : [],
9189
+ ["url", "Public URL"],
9190
+ ["slack_file", "Slack file"]
9191
+ ];
8957
9192
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
8958
9193
  /* @__PURE__ */ jsxRuntime.jsx(
8959
9194
  EditorField,
8960
9195
  {
8961
9196
  label: "Image source",
8962
- help: "Where the image comes from: a public URL, or a file already uploaded to your Slack workspace.",
9197
+ help: upload ? "Where the image comes from: a file from your device, a public URL, or a file already uploaded to your Slack workspace." : "Where the image comes from: a public URL, or a file already uploaded to your Slack workspace.",
8963
9198
  children: /* @__PURE__ */ jsxRuntime.jsx(
8964
9199
  RadioGroup,
8965
9200
  {
8966
9201
  value: source,
8967
9202
  onValueChange: (v) => setSource(v),
8968
9203
  className: "flex flex-row flex-wrap gap-3",
8969
- children: [
8970
- ["url", "Public URL"],
8971
- ["slack_file", "Slack file"]
8972
- ].map(([value, label]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
9204
+ children: sourceOptions.map(([value, label]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
8973
9205
  /* @__PURE__ */ jsxRuntime.jsx(RadioGroupItem, { value, id: `img-source-${value}` }),
8974
9206
  /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `img-source-${value}`, className: "text-xs", children: label })
8975
9207
  ] }, value))
@@ -8977,7 +9209,15 @@ function ImageEditor({ block, onChange }) {
8977
9209
  )
8978
9210
  }
8979
9211
  ),
8980
- "slack_file" in block ? /* @__PURE__ */ jsxRuntime.jsx(SlackFileFields, { block, onChange }) : /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Image URL", help: "A publicly accessible image URL (PNG, JPG, GIF).", htmlFor: "img-url", children: /* @__PURE__ */ jsxRuntime.jsx(
9212
+ "slack_file" in block ? /* @__PURE__ */ jsxRuntime.jsx(SlackFileFields, { block, onChange }) : source === "upload" && upload ? /* @__PURE__ */ jsxRuntime.jsx(
9213
+ UploadDropZone,
9214
+ {
9215
+ upload,
9216
+ inputId: "img-upload",
9217
+ variant: "panel",
9218
+ onUploaded: (url) => onChange({ ...block, image_url: url })
9219
+ }
9220
+ ) : /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Image URL", help: "A publicly accessible image URL (PNG, JPG, GIF).", htmlFor: "img-url", children: /* @__PURE__ */ jsxRuntime.jsx(
8981
9221
  Input,
8982
9222
  {
8983
9223
  id: "img-url",
@@ -10160,13 +10400,12 @@ function ImageAccessoryFields({ image, onChange }) {
10160
10400
  help: "A publicly accessible image URL (PNG, JPG, GIF).",
10161
10401
  htmlFor: "section-acc-img-url",
10162
10402
  children: /* @__PURE__ */ jsxRuntime.jsx(
10163
- Input,
10403
+ ImageUrlInput,
10164
10404
  {
10165
10405
  id: "section-acc-img-url",
10166
- type: "url",
10167
10406
  value: image.image_url ?? "",
10168
10407
  placeholder: "e.g. https://example.com/thumb.png",
10169
- onChange: (e) => onChange({ ...image, image_url: e.target.value })
10408
+ onChange: (url) => onChange({ ...image, image_url: url })
10170
10409
  }
10171
10410
  )
10172
10411
  }
@@ -10529,10 +10768,24 @@ function BlockEditor({
10529
10768
  const custom = useCustomBlockEditor(block);
10530
10769
  const CustomForm = custom?.component;
10531
10770
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
10532
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-sm font-semibold text-foreground", children: [
10533
- "Edit ",
10534
- custom ? custom.label : labelForBlockType(block.type)
10535
- ] }) }),
10771
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
10772
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-sm font-semibold text-foreground", children: [
10773
+ "Edit ",
10774
+ custom ? custom.label : labelForBlockType(block.type)
10775
+ ] }),
10776
+ onDone ? /* @__PURE__ */ jsxRuntime.jsx(
10777
+ Button,
10778
+ {
10779
+ type: "button",
10780
+ variant: "ghost",
10781
+ size: "icon",
10782
+ "aria-label": "Close editor",
10783
+ className: "-my-1 -mr-1 h-7 w-7 text-muted-foreground",
10784
+ onClick: onDone,
10785
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, {})
10786
+ }
10787
+ ) : null
10788
+ ] }),
10536
10789
  errors && errors.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-0.5 rounded-md border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive", children: errors.map((err, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: err }, i)) }) : null,
10537
10790
  CustomForm ? /* @__PURE__ */ jsxRuntime.jsx(CustomForm, { block, onChange }) : dispatch(block, onChange),
10538
10791
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -10694,15 +10947,23 @@ function BlockRow({
10694
10947
  children: preview
10695
10948
  }
10696
10949
  ) }),
10697
- /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsxRuntime.jsx(
10698
- BlockEditor,
10950
+ /* @__PURE__ */ jsxRuntime.jsx(
10951
+ PopoverContent,
10699
10952
  {
10700
- block: builderBlock.block,
10701
- errors,
10702
- onChange: (next) => onUpdate(builderBlock.id, next),
10703
- onDone: () => onOpenChange?.(false)
10953
+ className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]",
10954
+ align: "start",
10955
+ onCloseAutoFocus: returnFocusUnlessMoved,
10956
+ children: /* @__PURE__ */ jsxRuntime.jsx(
10957
+ BlockEditor,
10958
+ {
10959
+ block: builderBlock.block,
10960
+ errors,
10961
+ onChange: (next) => onUpdate(builderBlock.id, next),
10962
+ onDone: () => onOpenChange?.(false)
10963
+ }
10964
+ )
10704
10965
  }
10705
- ) })
10966
+ )
10706
10967
  ] });
10707
10968
  const adornment = position && renderBlockAdornment?.(position);
10708
10969
  const hoverChrome = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -10968,7 +11229,7 @@ function ContainerRow({
10968
11229
  hasErrors ? "border-destructive/60" : "border-border"
10969
11230
  ),
10970
11231
  children: [
10971
- /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open: isOpen, onOpenChange: (open) => onOpenBlockChange?.(open ? id : null), children: [
11232
+ /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open: isOpen, onOpenChange: (open) => onOpenBlockChange?.(id, open), children: [
10972
11233
  /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
10973
11234
  "div",
10974
11235
  {
@@ -10980,7 +11241,7 @@ function ContainerRow({
10980
11241
  onKeyDown: (e) => {
10981
11242
  if (e.key === "Enter" || e.key === " ") {
10982
11243
  e.preventDefault();
10983
- onOpenBlockChange?.(!isOpen ? id : null);
11244
+ onOpenBlockChange?.(id, !isOpen);
10984
11245
  }
10985
11246
  },
10986
11247
  className: "flex w-full cursor-grab items-center gap-2 border-b bg-muted/40 px-3 py-2 text-left transition-colors hover:bg-muted active:cursor-grabbing",
@@ -10994,15 +11255,23 @@ function ContainerRow({
10994
11255
  ]
10995
11256
  }
10996
11257
  ) }),
10997
- /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]", align: "start", children: /* @__PURE__ */ jsxRuntime.jsx(
10998
- BlockEditor,
11258
+ /* @__PURE__ */ jsxRuntime.jsx(
11259
+ PopoverContent,
10999
11260
  {
11000
- block,
11001
- errors,
11002
- onChange: (next) => onUpdate(id, next),
11003
- onDone: () => onOpenBlockChange?.(null)
11261
+ className: "w-[min(32rem,calc(100vw-1.5rem))] sm:w-[32rem]",
11262
+ align: "start",
11263
+ onCloseAutoFocus: returnFocusUnlessMoved,
11264
+ children: /* @__PURE__ */ jsxRuntime.jsx(
11265
+ BlockEditor,
11266
+ {
11267
+ block,
11268
+ errors,
11269
+ onChange: (next) => onUpdate(id, next),
11270
+ onDone: () => onOpenBlockChange?.(id, false)
11271
+ }
11272
+ )
11004
11273
  }
11005
- ) })
11274
+ )
11006
11275
  ] }),
11007
11276
  /* @__PURE__ */ jsxRuntime.jsx(
11008
11277
  "div",
@@ -11031,7 +11300,7 @@ function ContainerRow({
11031
11300
  previewTheme,
11032
11301
  errors: errorsByBlockId?.get(child.id),
11033
11302
  isOpen: openBlockId === child.id,
11034
- onOpenChange: (open) => onOpenBlockChange?.(open ? child.id : null),
11303
+ onOpenChange: (open) => onOpenBlockChange?.(child.id, open),
11035
11304
  onUpdate,
11036
11305
  onDuplicate,
11037
11306
  onDelete,
@@ -11066,7 +11335,7 @@ function ContainerRow({
11066
11335
  {
11067
11336
  type: "button",
11068
11337
  "aria-label": `Show ${errors.length} validation ${errors.length === 1 ? "issue" : "issues"}`,
11069
- onClick: () => onOpenBlockChange?.(id),
11338
+ onClick: () => onOpenBlockChange?.(id, true),
11070
11339
  className: "flex h-6 w-6 items-center justify-center rounded text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
11071
11340
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "h-3.5 w-3.5" })
11072
11341
  }
@@ -11085,7 +11354,7 @@ function ContainerRow({
11085
11354
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Move down", disabled: index >= total - 1, onClick: () => onReorder(id, index + 1), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowDown, { className: "h-3.5 w-3.5" }) })
11086
11355
  ] }) : null,
11087
11356
  /* @__PURE__ */ jsxRuntime.jsx(BlockActionButtons, { actions: blockActions, position, variant: "toolbar" }),
11088
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Edit", onClick: () => onOpenBlockChange?.(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-3.5 w-3.5" }) }),
11357
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Edit", onClick: () => onOpenBlockChange?.(id, true), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-3.5 w-3.5" }) }),
11089
11358
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Duplicate", onClick: () => onDuplicate(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "h-3.5 w-3.5" }) }),
11090
11359
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton2, { label: "Delete", destructive: true, onClick: () => onDelete(id), children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" }) })
11091
11360
  ]
@@ -11118,7 +11387,7 @@ function ContainerRow({
11118
11387
  {
11119
11388
  type: "button",
11120
11389
  "aria-label": "Edit container",
11121
- onClick: () => onOpenBlockChange?.(id),
11390
+ onClick: () => onOpenBlockChange?.(id, true),
11122
11391
  className: "flex h-9 w-9 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
11123
11392
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-4 w-4" })
11124
11393
  }
@@ -11196,10 +11465,14 @@ function Surface({
11196
11465
  isPaletteDrag = false,
11197
11466
  onOpenPalette,
11198
11467
  blockActions,
11199
- renderBlockAdornment
11468
+ renderBlockAdornment,
11469
+ onAddBlock,
11470
+ imageDropEnabled = true
11200
11471
  }) {
11201
11472
  const { setNodeRef, isOver } = core.useDroppable({ id: SURFACE_DROPPABLE_ID });
11202
11473
  const isDark = previewTheme === "dark";
11474
+ const imageDrop = useCanvasImageDrop({ upload: useImageUpload(), enabled: imageDropEnabled, onAddBlock });
11475
+ const isDropTarget = isPaletteDrag || imageDrop.dragging;
11203
11476
  const showEndDropZone = isPaletteDrag && isOver && blocks.length > 0;
11204
11477
  const blocksList = /* @__PURE__ */ jsxRuntime.jsx(
11205
11478
  "div",
@@ -11208,10 +11481,19 @@ function Surface({
11208
11481
  className: cn(
11209
11482
  "flex min-h-[180px] flex-col px-3 py-2 transition-colors sm:min-h-[240px] sm:px-5",
11210
11483
  isDark ? "bg-[#1a1d21]" : "bg-white",
11211
- isPaletteDrag && (isDark ? "bg-[#22262c] outline-2 outline-primary/40 -outline-offset-2 outline-dashed" : "bg-[#f5f8ff] outline-2 outline-primary/40 -outline-offset-2 outline-dashed"),
11212
- isPaletteDrag && isOver && (isDark ? "bg-[#2c3036]" : "bg-[#eaf0ff]")
11484
+ isDropTarget && (isDark ? "bg-[#22262c] outline-2 outline-primary/40 -outline-offset-2 outline-dashed" : "bg-[#f5f8ff] outline-2 outline-primary/40 -outline-offset-2 outline-dashed"),
11485
+ (isPaletteDrag && isOver || imageDrop.dragging) && (isDark ? "bg-[#2c3036]" : "bg-[#eaf0ff]")
11213
11486
  ),
11214
- children: blocks.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { isDark, isPaletteDrag, isOver, onOpenPalette }) : /* @__PURE__ */ jsxRuntime.jsxs(sortable.SortableContext, { items: blocks.map((b) => b.id), strategy: sortable.verticalListSortingStrategy, children: [
11487
+ children: blocks.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
11488
+ EmptyState,
11489
+ {
11490
+ isDark,
11491
+ isPaletteDrag,
11492
+ isOver,
11493
+ isFileDrag: imageDrop.dragging,
11494
+ onOpenPalette
11495
+ }
11496
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(sortable.SortableContext, { items: blocks.map((b) => b.id), strategy: sortable.verticalListSortingStrategy, children: [
11215
11497
  blocks.map(
11216
11498
  (block, idx) => block.block.type === "container" ? /* @__PURE__ */ jsxRuntime.jsx(
11217
11499
  ContainerRow,
@@ -11241,7 +11523,7 @@ function Surface({
11241
11523
  previewTheme,
11242
11524
  errors: errorsByBlockId?.get(block.id),
11243
11525
  isOpen: openBlockId === block.id,
11244
- onOpenChange: (open) => onOpenBlockChange?.(open ? block.id : null),
11526
+ onOpenChange: (open) => onOpenBlockChange?.(block.id, open),
11245
11527
  onUpdate,
11246
11528
  onDuplicate,
11247
11529
  onDelete,
@@ -11256,7 +11538,7 @@ function Surface({
11256
11538
  block.id
11257
11539
  )
11258
11540
  ),
11259
- showEndDropZone ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator2, {}) : null
11541
+ showEndDropZone || imageDrop.dragging ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator2, {}) : null
11260
11542
  ] })
11261
11543
  }
11262
11544
  );
@@ -11265,16 +11547,28 @@ function Surface({
11265
11547
  {
11266
11548
  "aria-label": "Block preview",
11267
11549
  className: cn("flex min-w-0 flex-1 flex-col overflow-y-auto bg-muted p-3 sm:p-6"),
11268
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto w-full max-w-2xl", children: previewSurface === "modal" ? /* @__PURE__ */ jsxRuntime.jsx(ModalFrame, { isDark, children: blocksList }) : previewSurface === "app_home" ? /* @__PURE__ */ jsxRuntime.jsx(AppHomeFrame, { workspaceName, isDark, children: blocksList }) : /* @__PURE__ */ jsxRuntime.jsx(
11269
- SlackMessageFrame,
11270
- {
11271
- workspaceName,
11272
- authorName,
11273
- authorIcon,
11274
- isDark,
11275
- children: blocksList
11276
- }
11277
- ) })
11550
+ ...imageDrop.handlers,
11551
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-2xl", children: [
11552
+ imageDrop.handlers ? /* @__PURE__ */ jsxRuntime.jsx(
11553
+ ImageDropStatus,
11554
+ {
11555
+ dragging: imageDrop.dragging,
11556
+ pending: imageDrop.pending,
11557
+ error: imageDrop.error,
11558
+ onDismissError: imageDrop.dismissError
11559
+ }
11560
+ ) : null,
11561
+ previewSurface === "modal" ? /* @__PURE__ */ jsxRuntime.jsx(ModalFrame, { isDark, children: blocksList }) : previewSurface === "app_home" ? /* @__PURE__ */ jsxRuntime.jsx(AppHomeFrame, { workspaceName, isDark, children: blocksList }) : /* @__PURE__ */ jsxRuntime.jsx(
11562
+ SlackMessageFrame,
11563
+ {
11564
+ workspaceName,
11565
+ authorName,
11566
+ authorIcon,
11567
+ isDark,
11568
+ children: blocksList
11569
+ }
11570
+ )
11571
+ ] })
11278
11572
  }
11279
11573
  );
11280
11574
  }
@@ -11403,9 +11697,10 @@ function EmptyState({
11403
11697
  isDark,
11404
11698
  isPaletteDrag,
11405
11699
  isOver,
11700
+ isFileDrag: isFileDrag2 = false,
11406
11701
  onOpenPalette
11407
11702
  }) {
11408
- const active = isPaletteDrag && isOver;
11703
+ const active = isPaletteDrag && isOver || isFileDrag2;
11409
11704
  return /* @__PURE__ */ jsxRuntime.jsxs(
11410
11705
  "div",
11411
11706
  {
@@ -11432,7 +11727,7 @@ function EmptyState({
11432
11727
  "text-sm font-semibold transition-colors",
11433
11728
  active ? "text-primary" : isDark ? "text-white" : "text-[#1d1c1d]"
11434
11729
  ),
11435
- children: active ? "Drop to add block" : "Start adding blocks!"
11730
+ children: isFileDrag2 ? "Drop to add image" : active ? "Drop to add block" : "Start adding blocks!"
11436
11731
  }
11437
11732
  ),
11438
11733
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs", children: active ? "Release to insert it here." : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -11456,6 +11751,40 @@ function EmptyState({
11456
11751
  }
11457
11752
  );
11458
11753
  }
11754
+ function ImageDropStatus({
11755
+ dragging,
11756
+ pending,
11757
+ error,
11758
+ onDismissError
11759
+ }) {
11760
+ const progress = dragging ? "Drop images to add them at the end of the message." : pending > 0 ? `Uploading ${pending === 1 ? "1 image" : `${pending} images`}\u2026` : null;
11761
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11762
+ /* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", className: "empty:hidden", children: progress ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mb-2 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
11763
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImagePlus, { className: cn("h-3.5 w-3.5", pending > 0 && !dragging && "animate-pulse"), "aria-hidden": "true" }),
11764
+ progress
11765
+ ] }) : null }),
11766
+ error ? /* @__PURE__ */ jsxRuntime.jsxs(
11767
+ "div",
11768
+ {
11769
+ role: "alert",
11770
+ className: "mb-2 flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
11771
+ children: [
11772
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "flex-1", children: error }),
11773
+ /* @__PURE__ */ jsxRuntime.jsx(
11774
+ "button",
11775
+ {
11776
+ type: "button",
11777
+ "aria-label": "Dismiss",
11778
+ onClick: onDismissError,
11779
+ className: "rounded p-0.5 hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
11780
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3.5 w-3.5" })
11781
+ }
11782
+ )
11783
+ ]
11784
+ }
11785
+ ) : null
11786
+ ] });
11787
+ }
11459
11788
  function DropIndicator2() {
11460
11789
  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)]" }) });
11461
11790
  }
@@ -12018,6 +12347,7 @@ function BlockKitchen(props) {
12018
12347
  blockEditors,
12019
12348
  customEmojis,
12020
12349
  mentions,
12350
+ imageUpload,
12021
12351
  loadChannels,
12022
12352
  loadSendAsUserStatus,
12023
12353
  onSend,
@@ -12148,6 +12478,9 @@ function BlockKitchen(props) {
12148
12478
  setPaletteAdvanced(false);
12149
12479
  }
12150
12480
  const [openBlockId, setOpenBlockId] = React.useState(null);
12481
+ const setBlockOpen = React.useCallback((id, open) => {
12482
+ setOpenBlockId((current) => open ? id : current === id ? null : current);
12483
+ }, []);
12151
12484
  const [uncontrolledPreviewTheme, setUncontrolledPreviewTheme] = React.useState(defaultPreviewTheme);
12152
12485
  const isPreviewThemeControlled = controlledPreviewTheme !== void 0;
12153
12486
  const previewTheme = controlledPreviewTheme ?? uncontrolledPreviewTheme;
@@ -12401,7 +12734,7 @@ function BlockKitchen(props) {
12401
12734
  }
12402
12735
  )
12403
12736
  ] }),
12404
- /* @__PURE__ */ jsxRuntime.jsx(MentionsProvider, { mentions, children: /* @__PURE__ */ jsxRuntime.jsx(BlockEditorsProvider, { blockEditors, children: /* @__PURE__ */ jsxRuntime.jsx(
12737
+ /* @__PURE__ */ jsxRuntime.jsx(MentionsProvider, { mentions, children: /* @__PURE__ */ jsxRuntime.jsx(ImageUploadProvider, { imageUpload, children: /* @__PURE__ */ jsxRuntime.jsx(BlockEditorsProvider, { blockEditors, children: /* @__PURE__ */ jsxRuntime.jsx(
12405
12738
  Surface,
12406
12739
  {
12407
12740
  blocks,
@@ -12413,7 +12746,7 @@ function BlockKitchen(props) {
12413
12746
  previewSurface,
12414
12747
  errorsByBlockId: validation.byBlockId,
12415
12748
  openBlockId,
12416
- onOpenBlockChange: setOpenBlockId,
12749
+ onOpenBlockChange: setBlockOpen,
12417
12750
  onUpdate: updateBlock,
12418
12751
  onDuplicate: duplicateBlock,
12419
12752
  onDelete: removeBlock,
@@ -12421,9 +12754,11 @@ function BlockKitchen(props) {
12421
12754
  isPaletteDrag: activePaletteVariant !== null,
12422
12755
  onOpenPalette: () => setPaletteOpen(true),
12423
12756
  blockActions,
12424
- renderBlockAdornment
12757
+ renderBlockAdornment,
12758
+ onAddBlock: addBlock,
12759
+ imageDropEnabled: !disabledBlockTypes?.includes("image")
12425
12760
  }
12426
- ) }) })
12761
+ ) }) }) })
12427
12762
  ] })
12428
12763
  ]
12429
12764
  }