@tightknitai/block-kitchen 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { B as BlockKitchenProps, $ as Template, K as PreviewSurface, N as PreviewTheme } from './palette-DIbvAzRA.cjs';
3
- export { A as AlertBlock, a as AlertLevel, b as BrandPreset, c as BrandTheme, d as BrandTokens, e as BuilderBlock, C as CardBlock, f as CarouselBlock, g as CartesianChart, h as ChannelOption, i as Chart, j as ChartAxisConfig, k as ChartDataPoint, l as ChartSeries, m as ChartType, n as ContainerBlock, o as ContainerChildBlock, p as ContextActionsBlock, q as ContextActionsElement, r as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, s as EditingConfig, F as FeedbackButtonSubobject, t as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, u as IconButtonIcon, v as InputBlock, L as LoadMessageInput, w as LoadResult, M as MarkdownBlock, P as PaletteSection, x as PaletteVariant, y as PieChart, z as PieChartSegment, G as PlanBlock, J as PreviewHooks, R as RecentMessage, S as SendAsUserStatus, O as SendPayload, Q as SendResult, T as SupportedBlock, U as SupportedBlockType, V as SupportedHeaderBlock, W as TableBlock, X as TableCell, Y as TableColumnSetting, Z as TaskCardBlock, _ as TaskCardStatus, a0 as UpdatePayload, a1 as UpdateResult, a2 as UrlSourceElement, a3 as VideoBlock, a5 as defaultPalette, a6 as extraAlertVariant, a7 as legacyInputVariants } from './palette-DIbvAzRA.cjs';
2
+ import { B as BlockKitchenProps, $ as Template, K as PreviewSurface, N as PreviewTheme } from './palette-inX8F01o.cjs';
3
+ export { A as AlertBlock, a as AlertLevel, b as BrandPreset, c as BrandTheme, d as BrandTokens, e as BuilderBlock, C as CardBlock, f as CarouselBlock, g as CartesianChart, h as ChannelOption, i as Chart, j as ChartAxisConfig, k as ChartDataPoint, l as ChartSeries, m as ChartType, n as ContainerBlock, o as ContainerChildBlock, p as ContextActionsBlock, q as ContextActionsElement, r as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, s as EditingConfig, F as FeedbackButtonSubobject, t as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, u as IconButtonIcon, v as InputBlock, L as LoadMessageInput, w as LoadResult, M as MarkdownBlock, P as PaletteSection, x as PaletteVariant, y as PieChart, z as PieChartSegment, G as PlanBlock, J as PreviewHooks, R as RecentMessage, S as SendAsUserStatus, O as SendPayload, Q as SendResult, T as SupportedBlock, U as SupportedBlockType, V as SupportedHeaderBlock, W as TableBlock, X as TableCell, Y as TableColumnSetting, Z as TaskCardBlock, _ as TaskCardStatus, a0 as UpdatePayload, a1 as UpdateResult, a2 as UrlSourceElement, a3 as VideoBlock, a5 as defaultPalette, a6 as extraAlertVariant, a7 as legacyInputVariants } from './palette-inX8F01o.cjs';
4
4
  export { decodeBlocksFromString, encodeBlocksToString, toSlackBlocks } from './helpers.cjs';
5
5
  import 'slack-web-api-client';
6
6
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { B as BlockKitchenProps, $ as Template, K as PreviewSurface, N as PreviewTheme } from './palette-DIbvAzRA.js';
3
- export { A as AlertBlock, a as AlertLevel, b as BrandPreset, c as BrandTheme, d as BrandTokens, e as BuilderBlock, C as CardBlock, f as CarouselBlock, g as CartesianChart, h as ChannelOption, i as Chart, j as ChartAxisConfig, k as ChartDataPoint, l as ChartSeries, m as ChartType, n as ContainerBlock, o as ContainerChildBlock, p as ContextActionsBlock, q as ContextActionsElement, r as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, s as EditingConfig, F as FeedbackButtonSubobject, t as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, u as IconButtonIcon, v as InputBlock, L as LoadMessageInput, w as LoadResult, M as MarkdownBlock, P as PaletteSection, x as PaletteVariant, y as PieChart, z as PieChartSegment, G as PlanBlock, J as PreviewHooks, R as RecentMessage, S as SendAsUserStatus, O as SendPayload, Q as SendResult, T as SupportedBlock, U as SupportedBlockType, V as SupportedHeaderBlock, W as TableBlock, X as TableCell, Y as TableColumnSetting, Z as TaskCardBlock, _ as TaskCardStatus, a0 as UpdatePayload, a1 as UpdateResult, a2 as UrlSourceElement, a3 as VideoBlock, a5 as defaultPalette, a6 as extraAlertVariant, a7 as legacyInputVariants } from './palette-DIbvAzRA.js';
2
+ import { B as BlockKitchenProps, $ as Template, K as PreviewSurface, N as PreviewTheme } from './palette-inX8F01o.js';
3
+ export { A as AlertBlock, a as AlertLevel, b as BrandPreset, c as BrandTheme, d as BrandTokens, e as BuilderBlock, C as CardBlock, f as CarouselBlock, g as CartesianChart, h as ChannelOption, i as Chart, j as ChartAxisConfig, k as ChartDataPoint, l as ChartSeries, m as ChartType, n as ContainerBlock, o as ContainerChildBlock, p as ContextActionsBlock, q as ContextActionsElement, r as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, s as EditingConfig, F as FeedbackButtonSubobject, t as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, u as IconButtonIcon, v as InputBlock, L as LoadMessageInput, w as LoadResult, M as MarkdownBlock, P as PaletteSection, x as PaletteVariant, y as PieChart, z as PieChartSegment, G as PlanBlock, J as PreviewHooks, R as RecentMessage, S as SendAsUserStatus, O as SendPayload, Q as SendResult, T as SupportedBlock, U as SupportedBlockType, V as SupportedHeaderBlock, W as TableBlock, X as TableCell, Y as TableColumnSetting, Z as TaskCardBlock, _ as TaskCardStatus, a0 as UpdatePayload, a1 as UpdateResult, a2 as UrlSourceElement, a3 as VideoBlock, a5 as defaultPalette, a6 as extraAlertVariant, a7 as legacyInputVariants } from './palette-inX8F01o.js';
4
4
  export { decodeBlocksFromString, encodeBlocksToString, toSlackBlocks } from './helpers.js';
5
5
  import 'slack-web-api-client';
6
6
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, pointerWithin, closestCenter, DndContext, DragOverlay, useDroppable, useDraggable } from '@dnd-kit/core';
2
2
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
3
- import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, MailSearch, Plus, ChevronDown, BookOpen, ExternalLink, AlertTriangle, Trash2, Code2, Pencil, Send, Search, Info, CheckCircle2, Check, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Copy, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Undo2, Redo2, Smile, Link as Link$1 } from 'lucide-react';
3
+ import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, MailSearch, Plus, ChevronDown, BookOpen, ExternalLink, AlertTriangle, Trash2, Code2, Pencil, Send, Search, Check, Copy, Info, CheckCircle2, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Loader2, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Undo2, Redo2, Smile, Link as Link$1 } from 'lucide-react';
4
4
  import * as React from 'react';
5
5
  import { createContext, useMemo, useState, useCallback, useRef, useEffect, useId, useContext } from 'react';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -1887,6 +1887,10 @@ function sanitizeImageSrc(value) {
1887
1887
  // src/lib/sanitize-blocks.ts
1888
1888
  var HREF_KEYS = /* @__PURE__ */ new Set(["url"]);
1889
1889
  var IMAGE_KEYS = /* @__PURE__ */ new Set(["image_url"]);
1890
+ var RETRIEVAL_ONLY_KEYS = /* @__PURE__ */ new Map([
1891
+ // Covers both the image block and the image element — both `type: 'image'`.
1892
+ ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])]
1893
+ ]);
1890
1894
  function sanitizeValue(value) {
1891
1895
  if (value === null || typeof value !== "object") {
1892
1896
  return value;
@@ -1904,8 +1908,14 @@ function sanitizeValue(value) {
1904
1908
  return changed ? out : value;
1905
1909
  }
1906
1910
  const src = value;
1911
+ const dropKeys = typeof src.type === "string" ? RETRIEVAL_ONLY_KEYS.get(src.type) : void 0;
1907
1912
  let copy = null;
1908
1913
  for (const key of Object.keys(src)) {
1914
+ if (dropKeys?.has(key)) {
1915
+ copy ??= { ...src };
1916
+ delete copy[key];
1917
+ continue;
1918
+ }
1909
1919
  const original = src[key];
1910
1920
  let next = original;
1911
1921
  if (typeof original === "string") {
@@ -1929,6 +1939,18 @@ function sanitizeValue(value) {
1929
1939
  function sanitizeBlock(block) {
1930
1940
  return sanitizeValue(block);
1931
1941
  }
1942
+ function sanitizeBlocks(blocks) {
1943
+ let changed = false;
1944
+ const out = new Array(blocks.length);
1945
+ for (let i = 0; i < blocks.length; i++) {
1946
+ const next = sanitizeBlock(blocks[i]);
1947
+ if (next !== blocks[i]) {
1948
+ changed = true;
1949
+ }
1950
+ out[i] = next;
1951
+ }
1952
+ return changed ? out : blocks;
1953
+ }
1932
1954
 
1933
1955
  // src/lib/to-slack-blocks.ts
1934
1956
  function toSlackBlocks(blocks) {
@@ -2186,7 +2208,7 @@ function useBlockKitchenState({
2186
2208
  });
2187
2209
  }, []);
2188
2210
  const replaceAll = useCallback((newBlocks) => {
2189
- setBlocks(newBlocks.map(wrap));
2211
+ setBlocks(sanitizeBlocks(newBlocks).map(wrap));
2190
2212
  }, []);
2191
2213
  return {
2192
2214
  blocks,
@@ -2595,8 +2617,10 @@ function JsonDrawer({
2595
2617
  const [value, setValue] = useState("");
2596
2618
  const [parseError, setParseError] = useState(null);
2597
2619
  const [validationErrors, setValidationErrors] = useState([]);
2620
+ const [copied, setCopied] = useState(false);
2598
2621
  const textareaRef = useRef(null);
2599
2622
  const gutterRef = useRef(null);
2623
+ const copyResetRef = useRef(null);
2600
2624
  const blocksRef = useRef(blocks);
2601
2625
  useEffect(() => {
2602
2626
  blocksRef.current = blocks;
@@ -2606,8 +2630,27 @@ function JsonDrawer({
2606
2630
  setValue(JSON.stringify(blocksRef.current, null, 2));
2607
2631
  setParseError(null);
2608
2632
  setValidationErrors([]);
2633
+ setCopied(false);
2609
2634
  }
2610
2635
  }, [open]);
2636
+ useEffect(
2637
+ () => () => {
2638
+ if (copyResetRef.current) {
2639
+ clearTimeout(copyResetRef.current);
2640
+ }
2641
+ },
2642
+ []
2643
+ );
2644
+ const handleCopy = () => {
2645
+ navigator.clipboard?.writeText(value).then(() => {
2646
+ setCopied(true);
2647
+ if (copyResetRef.current) {
2648
+ clearTimeout(copyResetRef.current);
2649
+ }
2650
+ copyResetRef.current = setTimeout(() => setCopied(false), 2e3);
2651
+ }).catch(() => {
2652
+ });
2653
+ };
2611
2654
  const handleChange = (next) => {
2612
2655
  setValue(next);
2613
2656
  if (next.length > MAX_JSON_BYTES) {
@@ -2654,7 +2697,17 @@ function JsonDrawer({
2654
2697
  /* @__PURE__ */ jsx(SheetTitle, { children: "Block Kit JSON" }),
2655
2698
  /* @__PURE__ */ jsx(SheetDescription, { children: "Edits update the preview as you type. Parse errors show below." })
2656
2699
  ] }),
2657
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 overflow-hidden rounded-md border border-input bg-muted/30 shadow-sm focus-within:ring-1 focus-within:ring-ring", children: [
2700
+ /* @__PURE__ */ jsxs("div", { className: "relative flex flex-1 overflow-hidden rounded-md border border-input bg-muted/30 shadow-sm focus-within:ring-1 focus-within:ring-ring", children: [
2701
+ /* @__PURE__ */ jsx(
2702
+ "button",
2703
+ {
2704
+ type: "button",
2705
+ onClick: handleCopy,
2706
+ "aria-label": copied ? "Copied to clipboard" : "Copy JSON",
2707
+ className: "absolute right-2 top-2 z-10 inline-flex items-center justify-center rounded-md border border-input bg-background/80 p-1.5 text-muted-foreground shadow-sm backdrop-blur transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
2708
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-emerald-600" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
2709
+ }
2710
+ ),
2658
2711
  /* @__PURE__ */ jsx(
2659
2712
  "div",
2660
2713
  {
@@ -2803,6 +2856,79 @@ var Label = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ j
2803
2856
  }
2804
2857
  ));
2805
2858
  Label.displayName = LabelPrimitive.Root.displayName;
2859
+ var POLL_INTERVAL_MS = 2500;
2860
+ var MAX_POLLS = 24;
2861
+ function useSlackSignIn(loadStatus, { open, enabled }) {
2862
+ const [userStatus, setUserStatus] = useState(null);
2863
+ const [polling, setPolling] = useState(false);
2864
+ const loadStatusRef = useRef(loadStatus);
2865
+ useEffect(() => {
2866
+ loadStatusRef.current = loadStatus;
2867
+ });
2868
+ const refresh = useCallback(() => {
2869
+ loadStatusRef.current().then(setUserStatus).catch(() => setUserStatus({ canSendAsUser: false }));
2870
+ }, []);
2871
+ const pollRef = useRef(null);
2872
+ const stopPolling = useCallback(() => {
2873
+ if (pollRef.current) {
2874
+ clearInterval(pollRef.current);
2875
+ pollRef.current = null;
2876
+ }
2877
+ setPolling(false);
2878
+ }, []);
2879
+ const startSignIn = useCallback(() => {
2880
+ const url = userStatus?.oauthUrl;
2881
+ if (!url || !isSafeHref(url)) {
2882
+ return;
2883
+ }
2884
+ window.open(url, "_blank", "noopener,noreferrer");
2885
+ stopPolling();
2886
+ setPolling(true);
2887
+ let tries = 0;
2888
+ pollRef.current = setInterval(() => {
2889
+ tries += 1;
2890
+ loadStatusRef.current().then((next) => {
2891
+ setUserStatus(next);
2892
+ if (next.canSendAsUser) {
2893
+ stopPolling();
2894
+ }
2895
+ }).catch(() => {
2896
+ }).finally(() => {
2897
+ if (tries >= MAX_POLLS) {
2898
+ stopPolling();
2899
+ }
2900
+ });
2901
+ }, POLL_INTERVAL_MS);
2902
+ }, [userStatus, stopPolling]);
2903
+ useEffect(() => {
2904
+ if (!open || !enabled) {
2905
+ setUserStatus(null);
2906
+ stopPolling();
2907
+ return;
2908
+ }
2909
+ setUserStatus(null);
2910
+ refresh();
2911
+ return stopPolling;
2912
+ }, [open, enabled, refresh, stopPolling]);
2913
+ useEffect(() => {
2914
+ if (!open || !enabled) {
2915
+ return;
2916
+ }
2917
+ const handler = () => refresh();
2918
+ window.addEventListener("focus", handler);
2919
+ return () => window.removeEventListener("focus", handler);
2920
+ }, [open, enabled, refresh]);
2921
+ return { userStatus, polling, startSignIn };
2922
+ }
2923
+ function SlackSignInButton({ onClick, polling }) {
2924
+ return /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", className: "w-fit", onClick, disabled: polling, children: polling ? /* @__PURE__ */ jsxs(Fragment, { children: [
2925
+ /* @__PURE__ */ jsx(Loader2, { className: "animate-spin" }),
2926
+ " Waiting for Slack\u2026"
2927
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2928
+ "Sign in with Slack ",
2929
+ /* @__PURE__ */ jsx(ExternalLink, {})
2930
+ ] }) });
2931
+ }
2806
2932
  function recentToResult(msg) {
2807
2933
  return {
2808
2934
  ok: true,
@@ -2816,6 +2942,17 @@ function recentToResult(msg) {
2816
2942
  iconUrl: msg.iconUrl
2817
2943
  };
2818
2944
  }
2945
+ function previewText(m) {
2946
+ if (m.label) {
2947
+ return m.label;
2948
+ }
2949
+ for (const b of m.blocks) {
2950
+ if ((b.type === "header" || b.type === "section") && "text" in b && b.text && "text" in b.text) {
2951
+ return b.text.text;
2952
+ }
2953
+ }
2954
+ return `${m.blocks.length} block${m.blocks.length === 1 ? "" : "s"}`;
2955
+ }
2819
2956
  function LoadMessageDialog({
2820
2957
  open,
2821
2958
  onOpenChange,
@@ -2832,7 +2969,10 @@ function LoadMessageDialog({
2832
2969
  const [channelId, setChannelId] = useState("");
2833
2970
  const [recent, setRecent] = useState(null);
2834
2971
  const [recentError, setRecentError] = useState(null);
2972
+ const [selectedRecent, setSelectedRecent] = useState(null);
2973
+ const [signInPolling, setSignInPolling] = useState(false);
2835
2974
  const inputRef = useRef(null);
2975
+ const signInPollRef = useRef(null);
2836
2976
  const onLoadMessageRef = useRef(onLoadMessage);
2837
2977
  const loadRecentMessagesRef = useRef(loadRecentMessages);
2838
2978
  const loadChannelsRef = useRef(loadChannels);
@@ -2851,6 +2991,7 @@ function LoadMessageDialog({
2851
2991
  setChannelId("");
2852
2992
  setRecent(null);
2853
2993
  setRecentError(null);
2994
+ setSelectedRecent(null);
2854
2995
  if (!loadRecentMessagesRef.current) {
2855
2996
  setChannels([]);
2856
2997
  setChannelsError(null);
@@ -2878,6 +3019,7 @@ function LoadMessageDialog({
2878
3019
  }
2879
3020
  setRecent(null);
2880
3021
  setRecentError(null);
3022
+ setSelectedRecent(null);
2881
3023
  let cancelled = false;
2882
3024
  loadRecentMessagesRef.current(channelId).then((list) => {
2883
3025
  if (!cancelled) {
@@ -2892,7 +3034,25 @@ function LoadMessageDialog({
2892
3034
  cancelled = true;
2893
3035
  };
2894
3036
  }, [open, channelId]);
3037
+ const stopSignInPoll = useCallback(() => {
3038
+ if (signInPollRef.current) {
3039
+ clearInterval(signInPollRef.current);
3040
+ signInPollRef.current = null;
3041
+ }
3042
+ setSignInPolling(false);
3043
+ }, []);
3044
+ useEffect(() => {
3045
+ if (!open) {
3046
+ stopSignInPoll();
3047
+ }
3048
+ return stopSignInPoll;
3049
+ }, [open, stopSignInPoll]);
2895
3050
  const handleLoad = async () => {
3051
+ stopSignInPoll();
3052
+ if (selectedRecent) {
3053
+ onLoaded(recentToResult(selectedRecent));
3054
+ return;
3055
+ }
2896
3056
  const trimmed = link.trim();
2897
3057
  if (!trimmed) {
2898
3058
  setStatus({ kind: "error", error: "Paste a Slack message link first." });
@@ -2905,12 +3065,42 @@ function LoadMessageDialog({
2905
3065
  onLoaded(result);
2906
3066
  return;
2907
3067
  }
2908
- setStatus({ kind: "not-editable", reason: result.reason, blocks: result.blocks });
3068
+ setStatus({ kind: "not-editable", reason: result.reason, blocks: result.blocks, oauthUrl: result.oauthUrl });
2909
3069
  } catch (e) {
2910
3070
  setStatus({ kind: "error", error: e instanceof Error ? e.message : "Failed to load message." });
2911
3071
  }
2912
3072
  };
3073
+ const startFindSignIn = (oauthUrl) => {
3074
+ if (!isSafeHref(oauthUrl)) {
3075
+ return;
3076
+ }
3077
+ const trimmed = link.trim();
3078
+ if (!trimmed) {
3079
+ return;
3080
+ }
3081
+ window.open(oauthUrl, "_blank", "noopener,noreferrer");
3082
+ stopSignInPoll();
3083
+ setSignInPolling(true);
3084
+ let tries = 0;
3085
+ const POLL_INTERVAL_MS2 = 2500;
3086
+ const MAX_POLLS2 = 24;
3087
+ signInPollRef.current = setInterval(() => {
3088
+ tries += 1;
3089
+ onLoadMessageRef.current({ link: trimmed }).then((result) => {
3090
+ if (result.ok) {
3091
+ stopSignInPoll();
3092
+ onLoaded(result);
3093
+ }
3094
+ }).catch(() => {
3095
+ }).finally(() => {
3096
+ if (tries >= MAX_POLLS2) {
3097
+ stopSignInPoll();
3098
+ }
3099
+ });
3100
+ }, POLL_INTERVAL_MS2);
3101
+ };
2913
3102
  const notEditable = status.kind === "not-editable" ? status : null;
3103
+ const signInUrl = notEditable?.oauthUrl && isSafeHref(notEditable.oauthUrl) ? notEditable.oauthUrl : null;
2914
3104
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
2915
3105
  DialogContent,
2916
3106
  {
@@ -2921,7 +3111,7 @@ function LoadMessageDialog({
2921
3111
  },
2922
3112
  children: [
2923
3113
  /* @__PURE__ */ jsxs(DialogHeader, { children: [
2924
- /* @__PURE__ */ jsx(DialogTitle, { children: "Edit an existing message" }),
3114
+ /* @__PURE__ */ jsx(DialogTitle, { children: "Find an existing message" }),
2925
3115
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5 text-left", children: [
2926
3116
  /* @__PURE__ */ jsx(DialogDescription, { children: hasRecent ? "Paste a Slack message link, or pick a recent message your app posted." : `Paste a Slack message link (Slack's "Copy link") to load its blocks for editing.` }),
2927
3117
  /* @__PURE__ */ jsxs(Tooltip, { children: [
@@ -2955,6 +3145,8 @@ function LoadMessageDialog({
2955
3145
  value: link,
2956
3146
  onChange: (e) => {
2957
3147
  setLink(e.target.value);
3148
+ setSelectedRecent(null);
3149
+ stopSignInPoll();
2958
3150
  if (status.kind !== "idle" && status.kind !== "loading") {
2959
3151
  setStatus({ kind: "idle" });
2960
3152
  }
@@ -2977,7 +3169,8 @@ function LoadMessageDialog({
2977
3169
  /* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
2978
3170
  /* @__PURE__ */ jsx("span", { className: "flex-1", children: notEditable.reason })
2979
3171
  ] }),
2980
- /* @__PURE__ */ jsx(
3172
+ signInUrl && /* @__PURE__ */ jsx(SlackSignInButton, { onClick: () => startFindSignIn(signInUrl), polling: signInPolling }),
3173
+ notEditable.blocks && notEditable.blocks.length > 0 && /* @__PURE__ */ jsx(
2981
3174
  Button,
2982
3175
  {
2983
3176
  type: "button",
@@ -3023,26 +3216,52 @@ function LoadMessageDialog({
3023
3216
  channelId && recent && recent.length === 0 && !recentError && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No recent messages from this app in this channel." }),
3024
3217
  channelId && recent && recent.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex max-h-48 min-w-0 flex-col gap-1 overflow-y-auto", children: recent.map((m) => {
3025
3218
  const asUser = (m.editableVia ?? "bot") === "user";
3219
+ const authorName = m.username || (asUser ? "You" : "App bot");
3220
+ const safeIcon = isSafeImageSrc(m.iconUrl) ? m.iconUrl : void 0;
3221
+ const isSelected = selectedRecent?.channelId === m.channelId && selectedRecent?.ts === m.ts;
3026
3222
  return /* @__PURE__ */ jsxs(
3027
3223
  "button",
3028
3224
  {
3029
3225
  type: "button",
3030
- onClick: () => onLoaded(recentToResult(m)),
3031
- className: "flex min-w-0 flex-col gap-0.5 rounded-md border border-input bg-background px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
3226
+ "aria-pressed": isSelected,
3227
+ onClick: () => {
3228
+ setSelectedRecent(m);
3229
+ setLink("");
3230
+ if (status.kind !== "idle" && status.kind !== "loading") {
3231
+ setStatus({ kind: "idle" });
3232
+ }
3233
+ },
3234
+ className: cn(
3235
+ // Inset rings so the scroll container's overflow clip
3236
+ // can't shave the corners off the selected outline.
3237
+ "flex min-w-0 items-start gap-2.5 rounded-md border border-input bg-background px-3 py-2 text-left hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
3238
+ isSelected && "border-primary! bg-accent ring-1 ring-inset ring-primary"
3239
+ ),
3032
3240
  children: [
3033
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-baseline justify-between gap-2", children: [
3034
- /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded border px-1 py-px text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: asUser ? "You" : "Bot" }),
3035
- /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-xs text-muted-foreground", children: [
3036
- /* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: new Date(Number(m.ts) * 1e3).toLocaleString() }),
3241
+ safeIcon ? /* @__PURE__ */ jsx("img", { src: safeIcon, alt: "", className: "h-9 w-9 shrink-0 rounded-md object-cover" }) : /* @__PURE__ */ jsx(
3242
+ "span",
3243
+ {
3244
+ "aria-hidden": true,
3245
+ className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-sm font-semibold text-muted-foreground",
3246
+ children: authorName.charAt(0).toUpperCase()
3247
+ }
3248
+ ),
3249
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
3250
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [
3251
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-foreground", children: authorName }),
3252
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded border px-1 py-px text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: asUser ? "You" : "Bot" })
3253
+ ] }),
3254
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
3255
+ new Date(Number(m.ts) * 1e3).toLocaleString(),
3037
3256
  " ",
3038
3257
  /* @__PURE__ */ jsxs("span", { className: "font-mono", children: [
3039
3258
  "(",
3040
3259
  m.ts,
3041
3260
  ")"
3042
3261
  ] })
3043
- ] })
3044
- ] }),
3045
- m.label && /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground", children: m.label })
3262
+ ] }),
3263
+ /* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-sm text-foreground", children: previewText(m) })
3264
+ ] })
3046
3265
  ]
3047
3266
  },
3048
3267
  `${m.channelId}:${m.ts}`
@@ -3052,7 +3271,15 @@ function LoadMessageDialog({
3052
3271
  ] }),
3053
3272
  /* @__PURE__ */ jsxs(DialogFooter, { children: [
3054
3273
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }),
3055
- /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleLoad, disabled: status.kind === "loading" || !link.trim(), children: status.kind === "loading" ? "Loading\u2026" : "Load message" })
3274
+ /* @__PURE__ */ jsx(
3275
+ Button,
3276
+ {
3277
+ type: "button",
3278
+ onClick: handleLoad,
3279
+ disabled: status.kind === "loading" || !selectedRecent && !link.trim(),
3280
+ children: status.kind === "loading" ? "Loading\u2026" : "Find message"
3281
+ }
3282
+ )
3056
3283
  ] })
3057
3284
  ]
3058
3285
  }
@@ -3271,18 +3498,13 @@ function SendDialog({
3271
3498
  const [channels, setChannels] = useState(null);
3272
3499
  const [channelsError, setChannelsError] = useState(null);
3273
3500
  const [channelId, setChannelId] = useState("");
3274
- const [userStatus, setUserStatus] = useState(null);
3275
3501
  const [sendAs, setSendAs] = useState("bot");
3276
3502
  const [status, setStatus] = useState({ kind: "idle" });
3503
+ const { userStatus, polling, startSignIn } = useSlackSignIn(loadSendAsUserStatus, { open, enabled: true });
3277
3504
  const loadChannelsRef = useRef(loadChannels);
3278
- const loadSendAsUserStatusRef = useRef(loadSendAsUserStatus);
3279
3505
  useEffect(() => {
3280
3506
  loadChannelsRef.current = loadChannels;
3281
- loadSendAsUserStatusRef.current = loadSendAsUserStatus;
3282
3507
  });
3283
- const refreshSendAsUser = useCallback(() => {
3284
- loadSendAsUserStatusRef.current().then(setUserStatus).catch(() => setUserStatus({ canSendAsUser: false }));
3285
- }, []);
3286
3508
  useEffect(() => {
3287
3509
  if (!open) {
3288
3510
  return;
@@ -3290,7 +3512,6 @@ function SendDialog({
3290
3512
  setStatus({ kind: "idle" });
3291
3513
  setChannels(null);
3292
3514
  setChannelsError(null);
3293
- setUserStatus(null);
3294
3515
  setChannelId("");
3295
3516
  setSendAs("bot");
3296
3517
  let cancelled = false;
@@ -3306,19 +3527,10 @@ function SendDialog({
3306
3527
  }
3307
3528
  setChannelsError(e instanceof Error ? e.message : "Failed to load channels");
3308
3529
  });
3309
- refreshSendAsUser();
3310
3530
  return () => {
3311
3531
  cancelled = true;
3312
3532
  };
3313
- }, [open, refreshSendAsUser]);
3314
- useEffect(() => {
3315
- if (!open) {
3316
- return;
3317
- }
3318
- const handler = () => refreshSendAsUser();
3319
- window.addEventListener("focus", handler);
3320
- return () => window.removeEventListener("focus", handler);
3321
- }, [open, refreshSendAsUser]);
3533
+ }, [open]);
3322
3534
  const handleSubmit = async () => {
3323
3535
  if (!channelId) {
3324
3536
  setStatus({ kind: "error", error: "Please pick a channel." });
@@ -3372,22 +3584,9 @@ function SendDialog({
3372
3584
  ]
3373
3585
  }
3374
3586
  ),
3375
- userStatus && !userStatus.canSendAsUser && userStatus.oauthUrl && isSafeHref(userStatus.oauthUrl) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
3376
- /* @__PURE__ */ jsxs(
3377
- "a",
3378
- {
3379
- href: userStatus.oauthUrl,
3380
- target: "_blank",
3381
- rel: "noopener noreferrer",
3382
- className: "inline-flex items-center gap-1 text-primary hover:underline",
3383
- children: [
3384
- "Sign in with Slack ",
3385
- /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3" })
3386
- ]
3387
- }
3388
- ),
3389
- " ",
3390
- "to post as yourself."
3587
+ userStatus && !userStatus.canSendAsUser && userStatus.oauthUrl && isSafeHref(userStatus.oauthUrl) && /* @__PURE__ */ jsxs(Fragment, { children: [
3588
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Connect your Slack account to post as yourself." }),
3589
+ /* @__PURE__ */ jsx(SlackSignInButton, { onClick: startSignIn, polling })
3391
3590
  ] })
3392
3591
  ] }),
3393
3592
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
@@ -9161,7 +9360,7 @@ function Toolbar2({
9161
9360
  onOpenLoad,
9162
9361
  onOpenUpdate,
9163
9362
  onExitEdit,
9164
- loadButtonLabel = "Load message",
9363
+ loadButtonLabel = "Find message",
9165
9364
  updateButtonLabel = "Review & update"
9166
9365
  }) {
9167
9366
  const activeTheme = THEME_OPTIONS.find((t) => t.value === previewTheme) ?? THEME_OPTIONS[0];
@@ -9445,33 +9644,13 @@ function UpdateDialog({
9445
9644
  onShowIssues
9446
9645
  }) {
9447
9646
  const asUser = target.editableVia === "user";
9448
- const [userStatus, setUserStatus] = useState(null);
9449
9647
  const [status, setStatus] = useState({ kind: "idle" });
9450
- const loadSendAsUserStatusRef = useRef(loadSendAsUserStatus);
9648
+ const { userStatus, polling, startSignIn } = useSlackSignIn(loadSendAsUserStatus, { open, enabled: asUser });
9451
9649
  useEffect(() => {
9452
- loadSendAsUserStatusRef.current = loadSendAsUserStatus;
9453
- });
9454
- const refreshSendAsUser = useCallback(() => {
9455
- loadSendAsUserStatusRef.current().then(setUserStatus).catch(() => setUserStatus({ canSendAsUser: false }));
9456
- }, []);
9457
- useEffect(() => {
9458
- if (!open) {
9459
- return;
9460
- }
9461
- setStatus({ kind: "idle" });
9462
- if (asUser) {
9463
- setUserStatus(null);
9464
- refreshSendAsUser();
9465
- }
9466
- }, [open, asUser, refreshSendAsUser]);
9467
- useEffect(() => {
9468
- if (!open || !asUser) {
9469
- return;
9650
+ if (open) {
9651
+ setStatus({ kind: "idle" });
9470
9652
  }
9471
- const handler = () => refreshSendAsUser();
9472
- window.addEventListener("focus", handler);
9473
- return () => window.removeEventListener("focus", handler);
9474
- }, [open, asUser, refreshSendAsUser]);
9653
+ }, [open]);
9475
9654
  const needsSignIn = asUser && userStatus !== null && !userStatus.canSendAsUser;
9476
9655
  const handleSubmit = async () => {
9477
9656
  setStatus({ kind: "updating" });
@@ -9506,22 +9685,9 @@ function UpdateDialog({
9506
9685
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
9507
9686
  /* @__PURE__ */ jsx(Label, { children: "Posting as" }),
9508
9687
  /* @__PURE__ */ jsx("p", { className: "flex h-9 items-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground", children: asUser ? "Your account" : "App bot" }),
9509
- needsSignIn && userStatus?.oauthUrl && isSafeHref(userStatus.oauthUrl) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
9510
- /* @__PURE__ */ jsxs(
9511
- "a",
9512
- {
9513
- href: userStatus.oauthUrl,
9514
- target: "_blank",
9515
- rel: "noopener noreferrer",
9516
- className: "inline-flex items-center gap-1 text-primary hover:underline",
9517
- children: [
9518
- "Sign in with Slack ",
9519
- /* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3" })
9520
- ]
9521
- }
9522
- ),
9523
- " ",
9524
- "to update your own message."
9688
+ needsSignIn && userStatus?.oauthUrl && isSafeHref(userStatus.oauthUrl) && /* @__PURE__ */ jsxs(Fragment, { children: [
9689
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Connect your Slack account to edit your own messages." }),
9690
+ /* @__PURE__ */ jsx(SlackSignInButton, { onClick: startSignIn, polling })
9525
9691
  ] }),
9526
9692
  needsSignIn && !userStatus?.oauthUrl && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Sign in with Slack to update your own message." })
9527
9693
  ] }),