@nkootstra/block-kit 0.3.5 → 0.5.0

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.js CHANGED
@@ -50,12 +50,12 @@ function toViewOutput(view, state, identity) {
50
50
  bot_id: "B00000000",
51
51
  callback_id: view.callback_id ?? "",
52
52
  type: view.type,
53
- title: view.title ?? null,
54
- close: view.close ?? null,
55
- submit: view.submit ?? null,
56
- blocks: view.blocks,
53
+ title: normalizeText(view.title ?? null),
54
+ close: normalizeText(view.close ?? null),
55
+ submit: normalizeText(view.submit ?? null),
56
+ blocks: normalizeBlocks(view.blocks),
57
57
  private_metadata: view.private_metadata ?? "",
58
- state: { values: state },
58
+ state: { values: normalizeState(state) },
59
59
  hash: view.hash ?? "0.0000000000",
60
60
  clear_on_close: view.clear_on_close ?? false,
61
61
  notify_on_close: view.notify_on_close ?? false,
@@ -96,6 +96,7 @@ function normalizeEchoes(entry) {
96
96
  if (out.text !== void 0) out.text = normalizeText(out.text);
97
97
  if (out.placeholder !== void 0) out.placeholder = normalizeText(out.placeholder);
98
98
  if (out.selected_option) out.selected_option = normalizeOption(out.selected_option);
99
+ if (out.initial_option) out.initial_option = normalizeOption(out.initial_option);
99
100
  if (Array.isArray(out.selected_options)) out.selected_options = out.selected_options.map(normalizeOption);
100
101
  if (typeof out.confirm === "object" && out.confirm !== null) {
101
102
  const confirm = { ...out.confirm };
@@ -112,6 +113,35 @@ function normalizeEchoes(entry) {
112
113
  function normalizeState(state) {
113
114
  return Object.fromEntries(Object.entries(state).map(([blockId, elements]) => [blockId, Object.fromEntries(Object.entries(elements).map(([actionId, value]) => [actionId, normalizeEchoes(value)]))]));
114
115
  }
116
+ /**
117
+ * Blocks as Slack echoes a stored view: every text object normalized like {@link normalizeText},
118
+ * and an input block's `optional` and `dispatch_action` filled in as `false`.
119
+ */
120
+ function normalizeBlocks(blocks) {
121
+ return normalizeBlockNode(blocks);
122
+ }
123
+ function normalizeBlockNode(node) {
124
+ if (Array.isArray(node)) return node.map(normalizeBlockNode);
125
+ if (typeof node !== "object" || node === null) return node;
126
+ const object = node;
127
+ if ((object.type === "plain_text" || object.type === "mrkdwn") && typeof object.text === "string") return normalizeText(object);
128
+ const out = Object.fromEntries(Object.entries(object).map(([key, value]) => [key, normalizeBlockNode(value)]));
129
+ if (object.type === "input") {
130
+ out.optional ??= false;
131
+ out.dispatch_action ??= false;
132
+ }
133
+ return out;
134
+ }
135
+ /** `enterprise` and `is_enterprise_install`, as every interaction payload carries them. */
136
+ function enterpriseFields(id) {
137
+ return {
138
+ enterprise: id.team?.enterprise_id ? {
139
+ id: id.team.enterprise_id,
140
+ name: id.team.enterprise_name ?? ""
141
+ } : null,
142
+ is_enterprise_install: id.isEnterpriseInstall
143
+ };
144
+ }
115
145
  function buildBlockActionsPayload({ action, state, container, identity }) {
116
146
  const id = resolveIdentity(identity);
117
147
  state = normalizeState(state);
@@ -119,11 +149,7 @@ function buildBlockActionsPayload({ action, state, container, identity }) {
119
149
  type: "block_actions",
120
150
  actions: [normalizeEchoes(action)],
121
151
  team: id.team,
122
- enterprise: id.team?.enterprise_id ? {
123
- id: id.team.enterprise_id,
124
- name: id.team.enterprise_name ?? ""
125
- } : null,
126
- is_enterprise_install: id.isEnterpriseInstall,
152
+ ...enterpriseFields(id),
127
153
  user: id.user,
128
154
  token: id.token,
129
155
  response_url: id.responseUrl,
@@ -163,6 +189,7 @@ function buildViewSubmissionPayload({ view, state, identity, responseUrls }) {
163
189
  return {
164
190
  type: "view_submission",
165
191
  team: id.team,
192
+ ...enterpriseFields(id),
166
193
  user: {
167
194
  id: id.user.id,
168
195
  name: id.user.name ?? id.user.username ?? "",
@@ -181,6 +208,7 @@ function buildViewClosedPayload({ view, state, identity, isCleared }) {
181
208
  return {
182
209
  type: "view_closed",
183
210
  team: id.team,
211
+ ...enterpriseFields(id),
184
212
  user: {
185
213
  id: id.user.id,
186
214
  name: id.user.name ?? id.user.username ?? "",
@@ -203,7 +231,8 @@ function buildBlockSuggestionPayload({ actionId, blockId, value, container, stat
203
231
  action_id: actionId,
204
232
  block_id: blockId,
205
233
  value,
206
- team: id.team
234
+ team: id.team,
235
+ ...enterpriseFields(id)
207
236
  };
208
237
  if (container.type === "message") return {
209
238
  ...base,
@@ -233,6 +262,22 @@ function buildBlockSuggestionPayload({ actionId, blockId, value, container, stat
233
262
  }
234
263
  //#endregion
235
264
  //#region src/icons/index.tsx
265
+ /** Slack's `caret-down`, the filled 20×20 chevron of its button select (Block Kit Builder). */
266
+ function CaretDownIcon$1(props) {
267
+ return /* @__PURE__ */ jsx("svg", {
268
+ viewBox: "0 0 20 20",
269
+ width: "20",
270
+ height: "20",
271
+ "aria-hidden": "true",
272
+ ...props,
273
+ children: /* @__PURE__ */ jsx("path", {
274
+ fill: "currentColor",
275
+ fillRule: "evenodd",
276
+ d: "M5.72 7.47a.75.75 0 0 1 1.06 0L10 10.69l3.22-3.22a.75.75 0 1 1 1.06 1.06l-3.75 3.75a.75.75 0 0 1-1.06 0L5.72 8.53a.75.75 0 0 1 0-1.06",
277
+ clipRule: "evenodd"
278
+ })
279
+ });
280
+ }
236
281
  function ChevronDownIcon(props) {
237
282
  return /* @__PURE__ */ jsx("svg", {
238
283
  viewBox: "0 0 20 20",
@@ -362,6 +407,38 @@ function ThumbsDownIcon(props) {
362
407
  })
363
408
  });
364
409
  }
410
+ /** Slack's `thumbs-up-filled` glyph, shown on a picked feedback button. */
411
+ function ThumbsUpFilledIcon(props) {
412
+ return /* @__PURE__ */ jsx("svg", {
413
+ viewBox: "0 0 20 20",
414
+ width: "16",
415
+ height: "16",
416
+ "aria-hidden": "true",
417
+ ...props,
418
+ children: /* @__PURE__ */ jsx("path", {
419
+ fill: "currentColor",
420
+ fillRule: "evenodd",
421
+ clipRule: "evenodd",
422
+ d: "M12.997 1.77a2.315 2.315 0 0 0-3.3 1.304 7.5 7.5 0 0 1-1.06 2.052L7.146 7.17A.75.75 0 0 0 7 7.613v9.919a.75.75 0 0 0 .65.743l1.765.237c1.396.188 2.814.142 4.195-.134l.45-.09a4.75 4.75 0 0 0 3.717-3.68l.862-4.092c.395-1.88-1.209-3.576-3.108-3.285l-2.43.373.61-1.832.005-.011.003-.01.386-1.288a2.31 2.31 0 0 0-1.108-2.693M5.75 7.785a.75.75 0 0 0-.75-.75H2.25a.75.75 0 0 0-.735.602l-.015.148v8.096a2.653 2.653 0 0 0 2.653 2.654H5a.75.75 0 0 0 .75-.75z"
423
+ })
424
+ });
425
+ }
426
+ /** Slack's `thumbs-down-filled` glyph, shown on a picked feedback button. */
427
+ function ThumbsDownFilledIcon(props) {
428
+ return /* @__PURE__ */ jsx("svg", {
429
+ viewBox: "0 0 20 20",
430
+ width: "16",
431
+ height: "16",
432
+ "aria-hidden": "true",
433
+ ...props,
434
+ children: /* @__PURE__ */ jsx("path", {
435
+ fill: "currentColor",
436
+ fillRule: "evenodd",
437
+ clipRule: "evenodd",
438
+ d: "M12.997 18.343a2.315 2.315 0 0 1-3.3-1.303 7.5 7.5 0 0 0-1.06-2.052l-1.492-2.045A.75.75 0 0 1 7 12.5V2.58a.75.75 0 0 1 .65-.743L9.415 1.6a12.8 12.8 0 0 1 4.195.135l.45.09a4.75 4.75 0 0 1 3.717 3.68l.862 4.091c.395 1.88-1.209 3.576-3.108 3.285l-2.43-.373.61 1.832.005.012.003.01.386 1.287a2.31 2.31 0 0 1-1.108 2.693M5.75 12.33a.75.75 0 0 1-.75.75H2.25a.75.75 0 0 1-.735-.603L1.5 12.33V4.232a2.653 2.653 0 0 1 2.653-2.653H5a.75.75 0 0 1 .75.75z"
439
+ })
440
+ });
441
+ }
365
442
  function KebabIcon$1(props) {
366
443
  return /* @__PURE__ */ jsxs("svg", {
367
444
  viewBox: "0 0 20 20",
@@ -664,6 +741,38 @@ function EllipsisVerticalIcon(props) {
664
741
  })
665
742
  });
666
743
  }
744
+ /** Slack's `table` glyph, from a chart's "View as table" menu item. */
745
+ function TableIcon(props) {
746
+ return /* @__PURE__ */ jsx("svg", {
747
+ viewBox: "0 0 20 20",
748
+ width: "15",
749
+ height: "15",
750
+ "aria-hidden": "true",
751
+ ...props,
752
+ children: /* @__PURE__ */ jsx("path", {
753
+ fill: "currentColor",
754
+ fillRule: "evenodd",
755
+ d: "M15.5 4h-11A1.5 1.5 0 0 0 3 5.5V7h14V5.5A1.5 1.5 0 0 0 15.5 4M17 8.5H3v3h14zm-14 6V13h14v1.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5m1.5-12a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h11a3 3 0 0 0 3-3v-9a3 3 0 0 0-3-3z",
756
+ clipRule: "evenodd"
757
+ })
758
+ });
759
+ }
760
+ /** Slack's `image` glyph, from a chart's "Copy as image" menu item. */
761
+ function ImageIcon(props) {
762
+ return /* @__PURE__ */ jsx("svg", {
763
+ viewBox: "0 0 20 20",
764
+ width: "15",
765
+ height: "15",
766
+ "aria-hidden": "true",
767
+ ...props,
768
+ children: /* @__PURE__ */ jsx("path", {
769
+ fill: "currentColor",
770
+ fillRule: "evenodd",
771
+ d: "M16 3.5H4A1.5 1.5 0 0 0 2.5 5v7.287l1.664-1.536a1.25 1.25 0 0 1 1.662-.031l2.635 2.259 4.146-4.147a1.25 1.25 0 0 1 1.79.023l3.103 3.267V5A1.5 1.5 0 0 0 16 3.5M17.5 15c0-.45-.173-.882-.483-1.208l-3.53-3.718-4.13 4.129a1.25 1.25 0 0 1-1.697.065l-2.642-2.264-2.024 1.869c-.315.29-.494.699-.494 1.127A1.5 1.5 0 0 0 4 16.5h12a1.5 1.5 0 0 0 1.5-1.5M1 15a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H4a3 3 0 0 0-3 3zm7-7a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 1.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5",
772
+ clipRule: "evenodd"
773
+ })
774
+ });
775
+ }
667
776
  //#endregion
668
777
  //#region src/validation.ts
669
778
  /**
@@ -899,6 +1008,7 @@ const BlockKitContext = createContext({
899
1008
  errors: {},
900
1009
  state: {},
901
1010
  setValue: noop,
1011
+ seedValue: noop,
902
1012
  dispatch: noop,
903
1013
  identity: {},
904
1014
  setContainer: noop,
@@ -1027,7 +1137,7 @@ function BlockKitProvider(props) {
1027
1137
  const setValue = useCallback((blockId, actionId, value) => {
1028
1138
  const block = { ...stateRef.current[blockId] };
1029
1139
  if (value === void 0) delete block[actionId];
1030
- else block[actionId] = value;
1140
+ else block[actionId] = normalizeEchoes(value);
1031
1141
  const next = {
1032
1142
  ...stateRef.current,
1033
1143
  [blockId]: block
@@ -1036,14 +1146,17 @@ function BlockKitProvider(props) {
1036
1146
  setState(next);
1037
1147
  onStateChange?.(next);
1038
1148
  }, [onStateChange]);
1149
+ const seedValue = useCallback((blockId, actionId, value) => {
1150
+ if (stateRef.current[blockId]?.[actionId] === void 0) setValue(blockId, actionId, value);
1151
+ }, [setValue]);
1039
1152
  const setContainer = useCallback((container) => {
1040
1153
  containerRef.current = container;
1041
1154
  }, []);
1042
1155
  const dispatch = useCallback((action, scope) => {
1043
- const full = {
1156
+ const full = normalizeEchoes({
1044
1157
  ...action,
1045
1158
  action_ts: action.action_ts ?? actionTs()
1046
- };
1159
+ });
1047
1160
  const message = scope?.message;
1048
1161
  onAction?.(full, {
1049
1162
  state: stateRef.current,
@@ -1103,6 +1216,7 @@ function BlockKitProvider(props) {
1103
1216
  timeZone,
1104
1217
  state,
1105
1218
  setValue,
1219
+ seedValue,
1106
1220
  dispatch,
1107
1221
  identity: identity ?? {},
1108
1222
  setContainer,
@@ -1125,6 +1239,7 @@ function BlockKitProvider(props) {
1125
1239
  timeZone,
1126
1240
  state,
1127
1241
  setValue,
1242
+ seedValue,
1128
1243
  dispatch,
1129
1244
  identity,
1130
1245
  setContainer,
@@ -5776,7 +5891,7 @@ function UserMention({ id, name }) {
5776
5891
  type: "user",
5777
5892
  id
5778
5893
  }) : void 0;
5779
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
5894
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
5780
5895
  ref: anchorRef,
5781
5896
  className: "sbk-mention sbk-mention--user",
5782
5897
  role: "button",
@@ -5786,7 +5901,7 @@ function UserMention({ id, name }) {
5786
5901
  "aria-controls": open ? cardId : void 0,
5787
5902
  onClick: () => setOpen((o) => !o),
5788
5903
  onKeyDown,
5789
- children: ["@", name]
5904
+ children: `@${name}`
5790
5905
  }), profile && typeof document !== "undefined" && createPortal(/* @__PURE__ */ jsxs("div", {
5791
5906
  ref: cardRef,
5792
5907
  id: cardId,
@@ -5891,14 +6006,14 @@ function renderNode(node, resolvers, timeZone, emojiSize) {
5891
6006
  return name ? /* @__PURE__ */ jsx(UserMention, {
5892
6007
  id: node.id,
5893
6008
  name
5894
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: ["@", node.id] });
6009
+ }) : /* @__PURE__ */ jsx(Fragment$1, { children: `@${node.id}` });
5895
6010
  }
5896
6011
  case "usergroup": {
5897
6012
  const name = resolvers.usergroup?.(node.id);
5898
- return name ? /* @__PURE__ */ jsxs(MentionLink, {
6013
+ return name ? /* @__PURE__ */ jsx(MentionLink, {
5899
6014
  type: "usergroup",
5900
6015
  id: node.id,
5901
- children: ["@", name]
6016
+ children: `@${name}`
5902
6017
  }) : /* @__PURE__ */ jsx("span", {
5903
6018
  className: "sbk-mention--loading",
5904
6019
  "aria-label": "Loading user group"
@@ -5906,18 +6021,18 @@ function renderNode(node, resolvers, timeZone, emojiSize) {
5906
6021
  }
5907
6022
  case "channel": {
5908
6023
  const name = resolvers.channel?.(node.id) ?? node.label;
5909
- return name ? /* @__PURE__ */ jsxs(MentionLink, {
6024
+ return name ? /* @__PURE__ */ jsx(MentionLink, {
5910
6025
  type: "channel",
5911
6026
  id: node.id,
5912
- children: ["#", name]
6027
+ children: `#${name}`
5913
6028
  }) : /* @__PURE__ */ jsxs("span", {
5914
6029
  className: "sbk-mention--private",
5915
6030
  children: [/* @__PURE__ */ jsx(LockIcon, {}), "Private channel"]
5916
6031
  });
5917
6032
  }
5918
- case "broadcast": return /* @__PURE__ */ jsxs("span", {
6033
+ case "broadcast": return /* @__PURE__ */ jsx("span", {
5919
6034
  className: "sbk-mention--broadcast",
5920
- children: ["@", node.range]
6035
+ children: `@${node.range}`
5921
6036
  });
5922
6037
  case "date": {
5923
6038
  const text = formatSlackDate(node.timestamp, node.format, { timeZone });
@@ -5982,33 +6097,89 @@ function Text({ text, emojiSize = 22 }) {
5982
6097
  }
5983
6098
  //#endregion
5984
6099
  //#region src/confirm/ConfirmDialog.tsx
5985
- /** Slack's confirmation dialog: blocks the triggering action until the user picks confirm/deny. */
6100
+ /**
6101
+ * Slack's confirmation dialog (`c-dialog`): blocks the triggering action until the user confirms or
6102
+ * denies. Escape, the close button and a click on the overlay all deny.
6103
+ *
6104
+ * Slack marks it `role="dialog"`; this keeps `alertdialog`, the role WAI-ARIA gives a dialog that
6105
+ * interrupts to ask for a confirmation, so screen readers announce its text on open. Focus starts
6106
+ * on the deny button, the least destructive choice, stays inside while it's open, and goes back to
6107
+ * whatever opened it when it closes.
6108
+ */
5986
6109
  function ConfirmDialog({ confirm, onConfirm, onDeny }) {
5987
6110
  const { theme } = useBlockKit();
5988
6111
  const confirmStyle = confirm.style ?? "primary";
6112
+ const titleId = useId();
6113
+ const textId = useId();
6114
+ const dialogRef = useRef(null);
6115
+ const denyRef = useRef(null);
6116
+ useEffect(() => {
6117
+ const opener = document.activeElement;
6118
+ denyRef.current?.focus();
6119
+ return () => {
6120
+ if (opener?.isConnected) opener.focus();
6121
+ };
6122
+ }, []);
6123
+ function onKeyDown(event) {
6124
+ if (event.key === "Escape") {
6125
+ event.stopPropagation();
6126
+ onDeny();
6127
+ return;
6128
+ }
6129
+ if (event.key !== "Tab") return;
6130
+ const buttons = [...dialogRef.current?.querySelectorAll("button") ?? []];
6131
+ const first = buttons[0];
6132
+ const last = buttons.at(-1);
6133
+ if (event.shiftKey && document.activeElement === first) {
6134
+ event.preventDefault();
6135
+ last?.focus();
6136
+ } else if (!event.shiftKey && document.activeElement === last) {
6137
+ event.preventDefault();
6138
+ first?.focus();
6139
+ }
6140
+ }
5989
6141
  const node = /* @__PURE__ */ jsx("div", {
5990
6142
  className: "sbk-confirm__overlay",
5991
6143
  role: "presentation",
5992
6144
  "data-theme": theme,
5993
6145
  onClick: onDeny,
5994
6146
  children: /* @__PURE__ */ jsxs("div", {
6147
+ ref: dialogRef,
5995
6148
  className: "sbk-confirm",
5996
6149
  role: "alertdialog",
5997
6150
  "aria-modal": "true",
5998
- "aria-label": confirm.title?.text,
6151
+ "aria-labelledby": confirm.title ? titleId : void 0,
6152
+ "aria-label": confirm.title ? void 0 : confirm.confirm?.text ?? "Confirm",
6153
+ "aria-describedby": textId,
6154
+ onKeyDown,
5999
6155
  onClick: (event) => event.stopPropagation(),
6000
6156
  children: [
6001
- confirm.title && /* @__PURE__ */ jsx("h2", {
6002
- className: "sbk-confirm__title",
6003
- children: confirm.title.text
6157
+ /* @__PURE__ */ jsxs("div", {
6158
+ className: "sbk-confirm__header",
6159
+ children: [confirm.title && /* @__PURE__ */ jsx("h2", {
6160
+ id: titleId,
6161
+ className: "sbk-confirm__title",
6162
+ children: confirm.title.text
6163
+ }), /* @__PURE__ */ jsx("button", {
6164
+ type: "button",
6165
+ className: "sbk-confirm__close",
6166
+ "aria-label": "Close",
6167
+ onClick: onDeny,
6168
+ children: /* @__PURE__ */ jsx(CloseIcon, {
6169
+ width: "20",
6170
+ height: "20"
6171
+ })
6172
+ })]
6004
6173
  }),
6005
6174
  /* @__PURE__ */ jsx("div", {
6175
+ id: textId,
6006
6176
  className: "sbk-confirm__text",
6007
6177
  children: /* @__PURE__ */ jsx(Text, { text: confirm.text })
6008
6178
  }),
6009
6179
  /* @__PURE__ */ jsxs("div", {
6010
6180
  className: "sbk-confirm__actions",
6011
6181
  children: [/* @__PURE__ */ jsx("button", {
6182
+ ref: denyRef,
6012
6183
  type: "button",
6013
6184
  className: "sbk-confirm__button sbk-confirm__button--deny",
6014
6185
  onClick: onDeny,
@@ -6170,242 +6341,21 @@ function Checkboxes({ element, blockId }) {
6170
6341
  });
6171
6342
  }
6172
6343
  //#endregion
6173
- //#region src/elements/calendar/Calendar.tsx
6174
- function pad(n) {
6175
- return String(n).padStart(2, "0");
6176
- }
6177
- function toISO(y, m, d) {
6178
- return `${y}-${pad(m + 1)}-${pad(d)}`;
6179
- }
6180
- function todayISO() {
6181
- const now = /* @__PURE__ */ new Date();
6182
- return toISO(now.getFullYear(), now.getMonth(), now.getDate());
6183
- }
6184
- function Caret({ direction }) {
6185
- return /* @__PURE__ */ jsx("svg", {
6186
- viewBox: "0 0 20 20",
6187
- width: "20",
6188
- height: "20",
6189
- "aria-hidden": "true",
6190
- children: /* @__PURE__ */ jsx("path", {
6191
- fill: "currentColor",
6192
- fillRule: "evenodd",
6193
- d: direction === "left" ? "M12.28 5.22a.75.75 0 0 1 0 1.06L8.56 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0" : "M7.72 14.78a.75.75 0 0 1 0-1.06L11.44 10 7.72 6.28a.75.75 0 0 1 1.06-1.06l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0",
6194
- clipRule: "evenodd"
6195
- })
6196
- });
6197
- }
6198
- /** A month grid matching Slack's `c-date_picker_calendar`: a bordered 7-column grid of 44×42
6199
- * day cells, a ring around today, and solid blue for the selected day. */
6200
- function Calendar({ value, onSelect, onClear }) {
6201
- const initial = value ? /* @__PURE__ */ new Date(`${value}T00:00:00Z`) : /* @__PURE__ */ new Date();
6202
- const [year, setYear] = useState(initial.getUTCFullYear());
6203
- const [month, setMonth] = useState(initial.getUTCMonth());
6204
- const first = new Date(Date.UTC(year, month, 1));
6205
- const startWeekday = first.getUTCDay();
6206
- const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
6207
- const cells = [...Array(startWeekday).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)];
6208
- const today = todayISO();
6209
- function go(delta) {
6210
- const d = new Date(Date.UTC(year, month + delta, 1));
6211
- setYear(d.getUTCFullYear());
6212
- setMonth(d.getUTCMonth());
6213
- }
6214
- const monthLabel = first.toLocaleDateString("en-US", {
6215
- month: "long",
6216
- year: "numeric",
6217
- timeZone: "UTC"
6218
- });
6219
- return /* @__PURE__ */ jsxs("div", {
6220
- className: "sbk-calendar",
6221
- children: [
6222
- /* @__PURE__ */ jsxs("div", {
6223
- className: "sbk-calendar__header",
6224
- children: [
6225
- /* @__PURE__ */ jsx("button", {
6226
- type: "button",
6227
- className: "sbk-calendar__nav",
6228
- onClick: () => go(-1),
6229
- "aria-label": "Previous month",
6230
- children: /* @__PURE__ */ jsx(Caret, { direction: "left" })
6231
- }),
6232
- /* @__PURE__ */ jsx("span", {
6233
- className: "sbk-calendar__label",
6234
- children: monthLabel
6235
- }),
6236
- /* @__PURE__ */ jsx("button", {
6237
- type: "button",
6238
- className: "sbk-calendar__nav",
6239
- onClick: () => go(1),
6240
- "aria-label": "Next month",
6241
- children: /* @__PURE__ */ jsx(Caret, { direction: "right" })
6242
- })
6243
- ]
6244
- }),
6245
- /* @__PURE__ */ jsx("div", {
6246
- className: "sbk-calendar__weekdays",
6247
- children: [
6248
- "Sun",
6249
- "Mon",
6250
- "Tue",
6251
- "Wed",
6252
- "Thu",
6253
- "Fri",
6254
- "Sat"
6255
- ].map((w) => /* @__PURE__ */ jsx("span", { children: w }, w))
6256
- }),
6257
- /* @__PURE__ */ jsx("div", {
6258
- className: "sbk-calendar__grid",
6259
- children: cells.map((day, i) => {
6260
- if (day === null) return /* @__PURE__ */ jsx("span", { className: "sbk-calendar__cell sbk-calendar__cell--empty" }, i);
6261
- const iso = toISO(year, month, day);
6262
- const classes = ["sbk-calendar__cell"];
6263
- if (iso === value) classes.push("sbk-calendar__cell--selected");
6264
- if (iso === today) classes.push("sbk-calendar__cell--today");
6265
- return /* @__PURE__ */ jsx("button", {
6266
- type: "button",
6267
- className: classes.join(" "),
6268
- "aria-pressed": iso === value,
6269
- "aria-current": iso === today ? "date" : void 0,
6270
- onClick: () => onSelect(iso),
6271
- children: day
6272
- }, iso);
6273
- })
6274
- }),
6275
- onClear && value && /* @__PURE__ */ jsx("div", {
6276
- className: "sbk-calendar__footer",
6277
- children: /* @__PURE__ */ jsx("button", {
6278
- type: "button",
6279
- className: "sbk-calendar__clear",
6280
- onClick: onClear,
6281
- children: "Clear selection"
6282
- })
6283
- })
6284
- ]
6285
- });
6286
- }
6287
- //#endregion
6288
- //#region src/elements/dateFormat.ts
6289
- /** "1st", "2nd", "3rd", "11th"… as Slack writes the day in a long date ("January 1st, 2026"). */
6290
- function ordinal$1(day) {
6291
- if (day % 10 === 1 && day !== 11) return `${day}st`;
6292
- if (day % 10 === 2 && day !== 12) return `${day}nd`;
6293
- if (day % 10 === 3 && day !== 13) return `${day}rd`;
6294
- return `${day}th`;
6295
- }
6296
- /** Slack names time zones by their Windows display name (minus the UTC offset), not the IANA id. */
6297
- const TIME_ZONE_LABELS = {
6298
- "Europe/Amsterdam": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6299
- "Europe/Berlin": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6300
- "Europe/Zurich": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6301
- "Europe/Rome": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6302
- "Europe/Stockholm": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6303
- "Europe/Vienna": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6304
- "Europe/Brussels": "Brussels, Copenhagen, Madrid, Paris",
6305
- "Europe/Copenhagen": "Brussels, Copenhagen, Madrid, Paris",
6306
- "Europe/Madrid": "Brussels, Copenhagen, Madrid, Paris",
6307
- "Europe/Paris": "Brussels, Copenhagen, Madrid, Paris",
6308
- "Europe/Belgrade": "Belgrade, Bratislava, Budapest, Ljubljana, Prague",
6309
- "Europe/Budapest": "Belgrade, Bratislava, Budapest, Ljubljana, Prague",
6310
- "Europe/Prague": "Belgrade, Bratislava, Budapest, Ljubljana, Prague",
6311
- "Europe/Warsaw": "Sarajevo, Skopje, Warsaw, Zagreb",
6312
- "Europe/Dublin": "Dublin, Edinburgh, Lisbon, London",
6313
- "Europe/Lisbon": "Dublin, Edinburgh, Lisbon, London",
6314
- "Europe/London": "Dublin, Edinburgh, Lisbon, London",
6315
- "Europe/Athens": "Athens, Bucharest",
6316
- "Europe/Bucharest": "Athens, Bucharest",
6317
- "Europe/Helsinki": "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",
6318
- "Europe/Kyiv": "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",
6319
- "Europe/Istanbul": "Istanbul",
6320
- "Europe/Moscow": "Moscow, St. Petersburg",
6321
- "America/New_York": "Eastern Time (US and Canada)",
6322
- "America/Chicago": "Central Time (US and Canada)",
6323
- "America/Denver": "Mountain Time (US and Canada)",
6324
- "America/Phoenix": "Arizona",
6325
- "America/Los_Angeles": "Pacific Time (US and Canada)",
6326
- "America/Anchorage": "Alaska",
6327
- "Pacific/Honolulu": "Hawaii",
6328
- "America/Halifax": "Atlantic Time (Canada)",
6329
- "America/Sao_Paulo": "Brasilia",
6330
- "Asia/Dubai": "Abu Dhabi, Muscat",
6331
- "Asia/Kolkata": "Chennai, Kolkata, Mumbai, New Delhi",
6332
- "Asia/Shanghai": "Beijing, Chongqing, Hong Kong, Urumqi",
6333
- "Asia/Hong_Kong": "Beijing, Chongqing, Hong Kong, Urumqi",
6334
- "Asia/Singapore": "Kuala Lumpur, Singapore",
6335
- "Asia/Tokyo": "Osaka, Sapporo, Tokyo",
6336
- "Australia/Sydney": "Canberra, Melbourne, Sydney",
6337
- "Australia/Melbourne": "Canberra, Melbourne, Sydney",
6338
- "Pacific/Auckland": "Auckland, Wellington"
6339
- };
6340
- /** The label Slack shows under a datetimepicker; unknown zones fall back to their IANA id. */
6341
- function timeZoneLabel(timeZone) {
6342
- return TIME_ZONE_LABELS[timeZone] ?? timeZone;
6343
- }
6344
- /** A timestamp's wall-clock date ("2026-01-01") and time ("11:00") in `timeZone`. */
6345
- function wallClock(ts, timeZone) {
6346
- const parts = new Intl.DateTimeFormat("en-US", {
6347
- timeZone,
6348
- year: "numeric",
6349
- month: "2-digit",
6350
- day: "2-digit",
6351
- hour: "2-digit",
6352
- minute: "2-digit",
6353
- hourCycle: "h23"
6354
- }).formatToParts(/* @__PURE__ */ new Date(ts * 1e3));
6355
- const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
6356
- return {
6357
- date: `${get("year")}-${get("month")}-${get("day")}`,
6358
- time: `${get("hour")}:${get("minute")}`
6359
- };
6360
- }
6361
- /** The timestamp of a wall-clock date and time in `timeZone`. Around a daylight saving change it
6362
- * resolves like JavaScript's `Date`: a time the clocks skip moves forward by the gap, and a time
6363
- * they repeat takes its first occurrence. */
6364
- function fromWallClock(date, time, timeZone) {
6365
- const asUtc = Date.parse(`${date}T${time}:00Z`) / 1e3;
6366
- const offsetAt = (ts) => {
6367
- const wall = wallClock(ts, timeZone);
6368
- return Date.parse(`${wall.date}T${wall.time}:00Z`) / 1e3 - ts;
6369
- };
6370
- const before = asUtc - offsetAt(asUtc - 86400);
6371
- const matches = [before, asUtc - offsetAt(asUtc + 86400)].filter((ts) => {
6372
- const wall = wallClock(ts, timeZone);
6373
- return wall.date === date && wall.time === time;
6374
- });
6375
- return matches.length > 0 ? Math.min(...matches) : before;
6376
- }
6377
- //#endregion
6378
- //#region src/elements/inputBlockContext.ts
6379
- /** True while rendering the element of an `input` block, where Slack lays some elements out differently. */
6380
- const InputBlockContext = createContext(false);
6381
- function useInInputBlock() {
6382
- return useContext(InputBlockContext);
6383
- }
6384
- /** The id of the error shown under an `input` block's element, while it has one. */
6385
- const InputErrorContext = createContext(void 0);
6386
- /**
6387
- * Props for the element's focusable control: an `input` block with an error marks it invalid,
6388
- * which also gives it Slack's red focus ring, and describes it with the error text.
6389
- */
6390
- function useInvalidProps() {
6391
- const errorId = useContext(InputErrorContext);
6392
- return errorId ? {
6393
- "aria-invalid": true,
6394
- "aria-describedby": errorId
6395
- } : {};
6396
- }
6397
- //#endregion
6398
6344
  //#region src/elements/Popover.tsx
6399
6345
  /** Space between the anchor and the popover, as in Slack's menus. */
6400
6346
  const DEFAULT_GAP = 4;
6347
+ /** The least space kept between a popover and the window's left and right edges. */
6348
+ const EDGE = 8;
6401
6349
  /**
6402
6350
  * The layer a menu or calendar opens in. Slack mounts its popovers at the top of the page, so a
6403
6351
  * message list, a modal body or a carousel never clips them; this portals to `<body>` the same way
6404
6352
  * and keeps the popover pinned to its anchor as the page scrolls or resizes, or its content does.
6353
+ * Where keeping its alignment would run it past the window's left or right edge (a narrow phone
6354
+ * screen), it shifts just enough to stay inside.
6405
6355
  * The layer is as wide as the anchor, so a child sized `width: 100%` matches the control it opened
6406
6356
  * from.
6407
6357
  */
6408
- function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, children }) {
6358
+ function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, offsetX = 0, onPlaced, children }) {
6409
6359
  const { theme } = useBlockKit();
6410
6360
  const layerRef = useRef(null);
6411
6361
  const [position, setPosition] = useState(null);
@@ -6425,10 +6375,20 @@ function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, children }) {
6425
6375
  const fitsBelow = below + height <= window.innerHeight;
6426
6376
  const fitsAbove = above >= 0;
6427
6377
  let top = !fitsBelow && fitsAbove ? above : below;
6428
- if (!fitsBelow && !fitsAbove) top = Math.max(gap, Math.min(below, window.innerHeight - height - gap));
6378
+ if (!fitsBelow && !fitsAbove) top = Math.max(DEFAULT_GAP, Math.min(below, window.innerHeight - height - DEFAULT_GAP));
6379
+ layer.style.width = `${a.width}px`;
6380
+ const content = layer.firstElementChild?.getBoundingClientRect();
6381
+ const layerLeft = layer.getBoundingClientRect().left;
6382
+ let left = a.left + offsetX;
6383
+ if (content) {
6384
+ const contentLeft = left + content.left - layerLeft;
6385
+ const room = document.documentElement.clientWidth - EDGE;
6386
+ const shift = Math.max(EDGE - contentLeft, Math.min(0, room - (contentLeft + content.width)));
6387
+ left += shift;
6388
+ }
6429
6389
  setPosition({
6430
6390
  top,
6431
- left: a.left,
6391
+ left,
6432
6392
  width: a.width
6433
6393
  });
6434
6394
  }
@@ -6443,7 +6403,18 @@ function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, children }) {
6443
6403
  window.removeEventListener("resize", place);
6444
6404
  resize?.disconnect();
6445
6405
  };
6446
- }, [anchorRef, gap]);
6406
+ }, [
6407
+ anchorRef,
6408
+ gap,
6409
+ offsetX
6410
+ ]);
6411
+ const placed = position !== null;
6412
+ const notified = useRef(false);
6413
+ useEffect(() => {
6414
+ if (!placed || notified.current) return;
6415
+ notified.current = true;
6416
+ onPlaced?.();
6417
+ }, [placed]);
6447
6418
  useEffect(() => {
6448
6419
  function onDocDown(e) {
6449
6420
  const target = e.target;
@@ -6467,237 +6438,1697 @@ function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, children }) {
6467
6438
  }), document.body);
6468
6439
  }
6469
6440
  //#endregion
6470
- //#region src/elements/DatePicker.tsx
6441
+ //#region src/elements/timeZoneNames.ts
6442
+ /** The distinct names, referenced by index below. */
6443
+ const NAMES = [
6444
+ "Abu Dhabi, Muscat",
6445
+ "Adelaide",
6446
+ "Alaska",
6447
+ "Aleutian Islands",
6448
+ "Amman",
6449
+ "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6450
+ "Anadyr, Petropavlovsk-Kamchatsky",
6451
+ "Araguaina",
6452
+ "Arizona, Vancouver",
6453
+ "Astana",
6454
+ "Astrakhan, Ulyanovsk, Volgograd",
6455
+ "Asuncion",
6456
+ "Athens, Bucharest",
6457
+ "Atlantic Time (Canada)",
6458
+ "Auckland, Wellington",
6459
+ "Azores",
6460
+ "Baghdad",
6461
+ "Baja California",
6462
+ "Baku",
6463
+ "Bangkok, Hanoi, Jakarta",
6464
+ "Barnaul, Gorno-Altaysk",
6465
+ "Beijing, Chongqing, Hong Kong SAR, Urumqi",
6466
+ "Beirut",
6467
+ "Belgrade, Bratislava, Budapest, Ljubljana, Prague",
6468
+ "Bogota, Lima, Quito",
6469
+ "Bougainville Island",
6470
+ "Brasilia",
6471
+ "Brisbane",
6472
+ "Brussels, Copenhagen, Madrid, Paris",
6473
+ "Cabo Verde Islands",
6474
+ "Cairo",
6475
+ "Canberra, Melbourne, Sydney",
6476
+ "Caracas",
6477
+ "Casablanca",
6478
+ "Cayenne, Fortaleza",
6479
+ "Central America",
6480
+ "Central Time (US and Canada)",
6481
+ "Chatham Islands",
6482
+ "Chennai, Kolkata, Mumbai, New Delhi",
6483
+ "Chetumal",
6484
+ "Chihuahua",
6485
+ "Chita",
6486
+ "Chokurdakh",
6487
+ "City of Buenos Aires",
6488
+ "Cuiaba",
6489
+ "Damascus",
6490
+ "Darwin",
6491
+ "Dhaka",
6492
+ "Dublin, Edinburgh, Lisbon, London",
6493
+ "Easter Island",
6494
+ "Eastern Time (US and Canada)",
6495
+ "Ekaterinburg",
6496
+ "Eucla",
6497
+ "Fernando de Noronha",
6498
+ "Fiji Islands",
6499
+ "Gaza, Hebron",
6500
+ "Georgetown, La Paz, Manaus, San Juan",
6501
+ "Greenland",
6502
+ "Guadalajara, Mexico City, Monterrey",
6503
+ "Guam, Port Moresby",
6504
+ "Haiti",
6505
+ "Harare, Pretoria",
6506
+ "Havana",
6507
+ "Hawaii",
6508
+ "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",
6509
+ "Hobart",
6510
+ "Hovd",
6511
+ "Indiana (East)",
6512
+ "Irkutsk",
6513
+ "Islamabad, Karachi",
6514
+ "Istanbul",
6515
+ "Izhevsk, Samara",
6516
+ "Jerusalem",
6517
+ "Kabul",
6518
+ "Kaliningrad",
6519
+ "Kathmandu",
6520
+ "Kiritimati Island",
6521
+ "Krasnoyarsk",
6522
+ "Kuala Lumpur, Singapore",
6523
+ "Kuwait, Riyadh",
6524
+ "La Paz, Mazatlan",
6525
+ "Lord Howe Island",
6526
+ "Magadan",
6527
+ "Marquesas Islands",
6528
+ "Midway Island, American Samoa",
6529
+ "Minsk",
6530
+ "Monrovia, Reykjavik",
6531
+ "Montevideo",
6532
+ "Moscow, St. Petersburg",
6533
+ "Mountain Time (US and Canada), Navajo Nation",
6534
+ "Nairobi",
6535
+ "Newfoundland",
6536
+ "Norfolk Island",
6537
+ "Novosibirsk",
6538
+ "Nuku'alofa",
6539
+ "Osaka, Sapporo, Tokyo",
6540
+ "Pacific Time (US and Canada)",
6541
+ "Perth",
6542
+ "Port Louis",
6543
+ "Pyongyang",
6544
+ "Saint Pierre and Miquelon",
6545
+ "Sakhalin",
6546
+ "Salvador",
6547
+ "Samoa",
6548
+ "Santiago",
6549
+ "Sarajevo, Skopje, Warsaw, Zagreb",
6550
+ "Saskatchewan",
6551
+ "Seoul",
6552
+ "Solomon Islands, New Caledonia",
6553
+ "Sri Jayawardenepura",
6554
+ "Taipei",
6555
+ "Tashkent, Ashgabat",
6556
+ "Tbilisi",
6557
+ "Tehran",
6558
+ "Tomsk",
6559
+ "Tripoli",
6560
+ "Turks and Caicos",
6561
+ "Ulaanbaatar",
6562
+ "Vladivostok",
6563
+ "West Central Africa",
6564
+ "Windhoek",
6565
+ "Yakutsk",
6566
+ "Yangon (Rangoon)",
6567
+ "Yerevan"
6568
+ ];
6569
+ /** IANA zone → index into NAMES. */
6570
+ const ZONES = {
6571
+ "Africa/Abidjan": 86,
6572
+ "Africa/Accra": 86,
6573
+ "Africa/Addis_Ababa": 88,
6574
+ "Africa/Algiers": 119,
6575
+ "Africa/Asmara": 88,
6576
+ "Africa/Asmera": 88,
6577
+ "Africa/Bamako": 86,
6578
+ "Africa/Bangui": 119,
6579
+ "Africa/Banjul": 86,
6580
+ "Africa/Bissau": 86,
6581
+ "Africa/Blantyre": 61,
6582
+ "Africa/Brazzaville": 119,
6583
+ "Africa/Bujumbura": 61,
6584
+ "Africa/Cairo": 30,
6585
+ "Africa/Casablanca": 33,
6586
+ "Africa/Ceuta": 5,
6587
+ "Africa/Conakry": 86,
6588
+ "Africa/Dakar": 86,
6589
+ "Africa/Dar_es_Salaam": 88,
6590
+ "Africa/Djibouti": 88,
6591
+ "Africa/Douala": 119,
6592
+ "Africa/El_Aaiun": 33,
6593
+ "Africa/Freetown": 86,
6594
+ "Africa/Gaborone": 61,
6595
+ "Africa/Harare": 61,
6596
+ "Africa/Johannesburg": 61,
6597
+ "Africa/Juba": 88,
6598
+ "Africa/Kampala": 88,
6599
+ "Africa/Khartoum": 88,
6600
+ "Africa/Kigali": 61,
6601
+ "Africa/Kinshasa": 119,
6602
+ "Africa/Lagos": 119,
6603
+ "Africa/Libreville": 119,
6604
+ "Africa/Lome": 86,
6605
+ "Africa/Luanda": 119,
6606
+ "Africa/Lubumbashi": 61,
6607
+ "Africa/Lusaka": 61,
6608
+ "Africa/Malabo": 119,
6609
+ "Africa/Maputo": 61,
6610
+ "Africa/Maseru": 61,
6611
+ "Africa/Mbabane": 61,
6612
+ "Africa/Mogadishu": 88,
6613
+ "Africa/Monrovia": 86,
6614
+ "Africa/Nairobi": 90,
6615
+ "Africa/Ndjamena": 119,
6616
+ "Africa/Niamey": 119,
6617
+ "Africa/Nouakchott": 86,
6618
+ "Africa/Ouagadougou": 86,
6619
+ "Africa/Porto-Novo": 119,
6620
+ "Africa/Sao_Tome": 86,
6621
+ "Africa/Tripoli": 115,
6622
+ "Africa/Tunis": 119,
6623
+ "Africa/Windhoek": 120,
6624
+ "America/Adak": 3,
6625
+ "America/Anchorage": 2,
6626
+ "America/Anguilla": 56,
6627
+ "America/Antigua": 56,
6628
+ "America/Araguaina": 43,
6629
+ "America/Argentina/Buenos_Aires": 43,
6630
+ "America/Argentina/Catamarca": 43,
6631
+ "America/Argentina/Cordoba": 43,
6632
+ "America/Argentina/Jujuy": 43,
6633
+ "America/Argentina/La_Rioja": 43,
6634
+ "America/Argentina/Mendoza": 43,
6635
+ "America/Argentina/Rio_Gallegos": 43,
6636
+ "America/Argentina/Salta": 43,
6637
+ "America/Argentina/San_Juan": 43,
6638
+ "America/Argentina/San_Luis": 43,
6639
+ "America/Argentina/Tucuman": 43,
6640
+ "America/Argentina/Ushuaia": 43,
6641
+ "America/Aruba": 56,
6642
+ "America/Asuncion": 11,
6643
+ "America/Atikokan": 24,
6644
+ "America/Bahia": 102,
6645
+ "America/Bahia_Banderas": 58,
6646
+ "America/Barbados": 56,
6647
+ "America/Belem": 43,
6648
+ "America/Belize": 35,
6649
+ "America/Blanc-Sablon": 56,
6650
+ "America/Boa_Vista": 56,
6651
+ "America/Bogota": 24,
6652
+ "America/Boise": 89,
6653
+ "America/Buenos_Aires": 43,
6654
+ "America/Cambridge_Bay": 89,
6655
+ "America/Campo_Grande": 44,
6656
+ "America/Cancun": 39,
6657
+ "America/Caracas": 32,
6658
+ "America/Catamarca": 43,
6659
+ "America/Cayenne": 34,
6660
+ "America/Cayman": 24,
6661
+ "America/Chicago": 36,
6662
+ "America/Chihuahua": 40,
6663
+ "America/Coral_Harbour": 24,
6664
+ "America/Cordoba": 43,
6665
+ "America/Costa_Rica": 106,
6666
+ "America/Creston": 8,
6667
+ "America/Cuiaba": 44,
6668
+ "America/Curacao": 56,
6669
+ "America/Danmarkshavn": 86,
6670
+ "America/Dawson": 96,
6671
+ "America/Dawson_Creek": 8,
6672
+ "America/Denver": 89,
6673
+ "America/Detroit": 50,
6674
+ "America/Dominica": 56,
6675
+ "America/Edmonton": 89,
6676
+ "America/Eirunepe": 24,
6677
+ "America/El_Salvador": 106,
6678
+ "America/Fort_Nelson": 8,
6679
+ "America/Fortaleza": 7,
6680
+ "America/Glace_Bay": 13,
6681
+ "America/Godthab": 57,
6682
+ "America/Goose_Bay": 13,
6683
+ "America/Grand_Turk": 116,
6684
+ "America/Grenada": 56,
6685
+ "America/Guadeloupe": 56,
6686
+ "America/Guatemala": 106,
6687
+ "America/Guayaquil": 24,
6688
+ "America/Guyana": 56,
6689
+ "America/Halifax": 13,
6690
+ "America/Havana": 62,
6691
+ "America/Hermosillo": 8,
6692
+ "America/Indiana/Indianapolis": 67,
6693
+ "America/Indiana/Knox": 36,
6694
+ "America/Indiana/Marengo": 50,
6695
+ "America/Indiana/Petersburg": 50,
6696
+ "America/Indiana/Tell_City": 36,
6697
+ "America/Indiana/Vevay": 50,
6698
+ "America/Indiana/Vincennes": 50,
6699
+ "America/Indiana/Winamac": 50,
6700
+ "America/Indianapolis": 50,
6701
+ "America/Inuvik": 89,
6702
+ "America/Iqaluit": 50,
6703
+ "America/Jamaica": 24,
6704
+ "America/Jujuy": 43,
6705
+ "America/Juneau": 2,
6706
+ "America/Kentucky/Louisville": 50,
6707
+ "America/Kentucky/Monticello": 50,
6708
+ "America/Kralendijk": 56,
6709
+ "America/La_Paz": 56,
6710
+ "America/Lima": 24,
6711
+ "America/Los_Angeles": 96,
6712
+ "America/Louisville": 50,
6713
+ "America/Lower_Princes": 56,
6714
+ "America/Maceio": 43,
6715
+ "America/Managua": 106,
6716
+ "America/Manaus": 56,
6717
+ "America/Marigot": 56,
6718
+ "America/Martinique": 56,
6719
+ "America/Matamoros": 36,
6720
+ "America/Mazatlan": 80,
6721
+ "America/Mendoza": 43,
6722
+ "America/Menominee": 36,
6723
+ "America/Merida": 58,
6724
+ "America/Mexico_City": 58,
6725
+ "America/Miquelon": 100,
6726
+ "America/Moncton": 13,
6727
+ "America/Monterrey": 58,
6728
+ "America/Montevideo": 87,
6729
+ "America/Montreal": 50,
6730
+ "America/Montserrat": 56,
6731
+ "America/Nassau": 50,
6732
+ "America/New_York": 50,
6733
+ "America/Nome": 2,
6734
+ "America/Noronha": 53,
6735
+ "America/North_Dakota/Beulah": 36,
6736
+ "America/North_Dakota/Center": 36,
6737
+ "America/North_Dakota/New_Salem": 36,
6738
+ "America/Ojinaga": 36,
6739
+ "America/Panama": 24,
6740
+ "America/Paramaribo": 43,
6741
+ "America/Phoenix": 8,
6742
+ "America/Port-au-Prince": 60,
6743
+ "America/Port_of_Spain": 56,
6744
+ "America/Porto_Velho": 56,
6745
+ "America/Puerto_Rico": 56,
6746
+ "America/Rankin_Inlet": 36,
6747
+ "America/Recife": 43,
6748
+ "America/Regina": 106,
6749
+ "America/Resolute": 36,
6750
+ "America/Rio_Branco": 24,
6751
+ "America/Santarem": 43,
6752
+ "America/Santiago": 104,
6753
+ "America/Santo_Domingo": 56,
6754
+ "America/Sao_Paulo": 26,
6755
+ "America/Scoresbysund": 15,
6756
+ "America/Sitka": 2,
6757
+ "America/St_Barthelemy": 56,
6758
+ "America/St_Johns": 91,
6759
+ "America/St_Kitts": 56,
6760
+ "America/St_Lucia": 56,
6761
+ "America/St_Thomas": 56,
6762
+ "America/St_Vincent": 56,
6763
+ "America/Swift_Current": 106,
6764
+ "America/Tegucigalpa": 106,
6765
+ "America/Thule": 13,
6766
+ "America/Tijuana": 17,
6767
+ "America/Toronto": 50,
6768
+ "America/Tortola": 56,
6769
+ "America/Vancouver": 8,
6770
+ "America/Whitehorse": 96,
6771
+ "America/Winnipeg": 36,
6772
+ "America/Yakutat": 2,
6773
+ "Antarctica/Casey": 21,
6774
+ "Antarctica/Davis": 19,
6775
+ "Antarctica/DumontDUrville": 27,
6776
+ "Antarctica/Macquarie": 25,
6777
+ "Antarctica/Mawson": 111,
6778
+ "Antarctica/McMurdo": 14,
6779
+ "Antarctica/Palmer": 104,
6780
+ "Antarctica/Rothera": 43,
6781
+ "Antarctica/Syowa": 88,
6782
+ "Antarctica/Vostok": 47,
6783
+ "Arctic/Longyearbyen": 5,
6784
+ "Asia/Aden": 88,
6785
+ "Asia/Almaty": 9,
6786
+ "Asia/Amman": 4,
6787
+ "Asia/Anadyr": 6,
6788
+ "Asia/Aqtau": 111,
6789
+ "Asia/Aqtobe": 111,
6790
+ "Asia/Ashgabat": 111,
6791
+ "Asia/Baghdad": 16,
6792
+ "Asia/Bahrain": 88,
6793
+ "Asia/Baku": 18,
6794
+ "Asia/Bangkok": 19,
6795
+ "Asia/Barnaul": 20,
6796
+ "Asia/Beirut": 22,
6797
+ "Asia/Bishkek": 47,
6798
+ "Asia/Brunei": 21,
6799
+ "Asia/Calcutta": 38,
6800
+ "Asia/Chita": 41,
6801
+ "Asia/Chongqing": 21,
6802
+ "Asia/Colombo": 109,
6803
+ "Asia/Damascus": 45,
6804
+ "Asia/Dhaka": 47,
6805
+ "Asia/Dili": 95,
6806
+ "Asia/Dubai": 0,
6807
+ "Asia/Dushanbe": 111,
6808
+ "Asia/Gaza": 55,
6809
+ "Asia/Hebron": 55,
6810
+ "Asia/Ho_Chi_Minh": 19,
6811
+ "Asia/Hong_Kong": 21,
6812
+ "Asia/Hovd": 66,
6813
+ "Asia/Irkutsk": 68,
6814
+ "Asia/Istanbul": 70,
6815
+ "Asia/Jakarta": 19,
6816
+ "Asia/Jayapura": 95,
6817
+ "Asia/Jerusalem": 72,
6818
+ "Asia/Kabul": 73,
6819
+ "Asia/Kamchatka": 6,
6820
+ "Asia/Karachi": 69,
6821
+ "Asia/Kashgar": 47,
6822
+ "Asia/Kathmandu": 75,
6823
+ "Asia/Katmandu": 75,
6824
+ "Asia/Khandyga": 95,
6825
+ "Asia/Kolkata": 38,
6826
+ "Asia/Krasnoyarsk": 77,
6827
+ "Asia/Kuala_Lumpur": 78,
6828
+ "Asia/Kuching": 21,
6829
+ "Asia/Kuwait": 79,
6830
+ "Asia/Macau": 21,
6831
+ "Asia/Magadan": 82,
6832
+ "Asia/Makassar": 21,
6833
+ "Asia/Manila": 21,
6834
+ "Asia/Muscat": 0,
6835
+ "Asia/Nicosia": 12,
6836
+ "Asia/Novokuznetsk": 19,
6837
+ "Asia/Novosibirsk": 93,
6838
+ "Asia/Omsk": 47,
6839
+ "Asia/Oral": 111,
6840
+ "Asia/Phnom_Penh": 19,
6841
+ "Asia/Pontianak": 19,
6842
+ "Asia/Pyongyang": 99,
6843
+ "Asia/Qatar": 88,
6844
+ "Asia/Qyzylorda": 47,
6845
+ "Asia/Rangoon": 122,
6846
+ "Asia/Riyadh": 88,
6847
+ "Asia/Saigon": 19,
6848
+ "Asia/Sakhalin": 101,
6849
+ "Asia/Samarkand": 111,
6850
+ "Asia/Seoul": 107,
6851
+ "Asia/Shanghai": 21,
6852
+ "Asia/Singapore": 21,
6853
+ "Asia/Srednekolymsk": 42,
6854
+ "Asia/Taipei": 110,
6855
+ "Asia/Tashkent": 111,
6856
+ "Asia/Tbilisi": 112,
6857
+ "Asia/Tehran": 113,
6858
+ "Asia/Thimphu": 47,
6859
+ "Asia/Tokyo": 95,
6860
+ "Asia/Tomsk": 114,
6861
+ "Asia/Ulaanbaatar": 117,
6862
+ "Asia/Ulan_Bator": 117,
6863
+ "Asia/Urumqi": 47,
6864
+ "Asia/Ust-Nera": 27,
6865
+ "Asia/Vientiane": 19,
6866
+ "Asia/Vladivostok": 118,
6867
+ "Asia/Yakutsk": 121,
6868
+ "Asia/Yekaterinburg": 51,
6869
+ "Asia/Yerevan": 123,
6870
+ "Atlantic/Azores": 15,
6871
+ "Atlantic/Bermuda": 13,
6872
+ "Atlantic/Canary": 48,
6873
+ "Atlantic/Cape_Verde": 29,
6874
+ "Atlantic/Faeroe": 48,
6875
+ "Atlantic/Faroe": 48,
6876
+ "Atlantic/Madeira": 48,
6877
+ "Atlantic/Reykjavik": 86,
6878
+ "Atlantic/South_Georgia": 53,
6879
+ "Atlantic/St_Helena": 86,
6880
+ "Atlantic/Stanley": 43,
6881
+ "Australia/ACT": 31,
6882
+ "Australia/Adelaide": 1,
6883
+ "Australia/Brisbane": 27,
6884
+ "Australia/Broken_Hill": 1,
6885
+ "Australia/Darwin": 46,
6886
+ "Australia/Eucla": 52,
6887
+ "Australia/Hobart": 65,
6888
+ "Australia/Lindeman": 27,
6889
+ "Australia/Lord_Howe": 81,
6890
+ "Australia/Melbourne": 31,
6891
+ "Australia/Perth": 97,
6892
+ "Australia/Sydney": 31,
6893
+ "Etc/GMT": 86,
6894
+ "Etc/GMT+5": 24,
6895
+ "Etc/UTC": 86,
6896
+ "Europe/Amsterdam": 5,
6897
+ "Europe/Andorra": 5,
6898
+ "Europe/Astrakhan": 10,
6899
+ "Europe/Athens": 12,
6900
+ "Europe/Belfast": 48,
6901
+ "Europe/Belgrade": 23,
6902
+ "Europe/Berlin": 5,
6903
+ "Europe/Bratislava": 5,
6904
+ "Europe/Brussels": 28,
6905
+ "Europe/Bucharest": 12,
6906
+ "Europe/Budapest": 5,
6907
+ "Europe/Busingen": 5,
6908
+ "Europe/Copenhagen": 5,
6909
+ "Europe/Dublin": 48,
6910
+ "Europe/Gibraltar": 5,
6911
+ "Europe/Guernsey": 48,
6912
+ "Europe/Helsinki": 64,
6913
+ "Europe/Isle_of_Man": 48,
6914
+ "Europe/Istanbul": 70,
6915
+ "Europe/Jersey": 48,
6916
+ "Europe/Kaliningrad": 74,
6917
+ "Europe/Kirov": 88,
6918
+ "Europe/Kyiv": 12,
6919
+ "Europe/Lisbon": 48,
6920
+ "Europe/Ljubljana": 5,
6921
+ "Europe/London": 48,
6922
+ "Europe/Luxembourg": 5,
6923
+ "Europe/Madrid": 5,
6924
+ "Europe/Malta": 5,
6925
+ "Europe/Mariehamn": 12,
6926
+ "Europe/Minsk": 85,
6927
+ "Europe/Monaco": 5,
6928
+ "Europe/Moscow": 88,
6929
+ "Europe/Oslo": 5,
6930
+ "Europe/Paris": 5,
6931
+ "Europe/Podgorica": 5,
6932
+ "Europe/Prague": 23,
6933
+ "Europe/Riga": 12,
6934
+ "Europe/Rome": 5,
6935
+ "Europe/Samara": 71,
6936
+ "Europe/San_Marino": 5,
6937
+ "Europe/Sarajevo": 5,
6938
+ "Europe/Simferopol": 88,
6939
+ "Europe/Skopje": 5,
6940
+ "Europe/Sofia": 12,
6941
+ "Europe/Stockholm": 5,
6942
+ "Europe/Tallinn": 12,
6943
+ "Europe/Tirane": 5,
6944
+ "Europe/Ulyanovsk": 10,
6945
+ "Europe/Vaduz": 5,
6946
+ "Europe/Vatican": 5,
6947
+ "Europe/Vienna": 5,
6948
+ "Europe/Vilnius": 12,
6949
+ "Europe/Volgograd": 10,
6950
+ "Europe/Warsaw": 105,
6951
+ "Europe/Zagreb": 5,
6952
+ "Europe/Zurich": 5,
6953
+ GMT: 86,
6954
+ "Indian/Antananarivo": 88,
6955
+ "Indian/Chagos": 47,
6956
+ "Indian/Christmas": 19,
6957
+ "Indian/Cocos": 122,
6958
+ "Indian/Comoro": 88,
6959
+ "Indian/Kerguelen": 111,
6960
+ "Indian/Mahe": 0,
6961
+ "Indian/Maldives": 111,
6962
+ "Indian/Mauritius": 98,
6963
+ "Indian/Mayotte": 88,
6964
+ "Indian/Reunion": 0,
6965
+ "Pacific/Apia": 103,
6966
+ "Pacific/Auckland": 14,
6967
+ "Pacific/Bougainville": 25,
6968
+ "Pacific/Chatham": 37,
6969
+ "Pacific/Chuuk": 27,
6970
+ "Pacific/Easter": 49,
6971
+ "Pacific/Efate": 25,
6972
+ "Pacific/Enderbury": 94,
6973
+ "Pacific/Fakaofo": 94,
6974
+ "Pacific/Fiji": 54,
6975
+ "Pacific/Funafuti": 6,
6976
+ "Pacific/Galapagos": 106,
6977
+ "Pacific/Guadalcanal": 108,
6978
+ "Pacific/Guam": 59,
6979
+ "Pacific/Honolulu": 63,
6980
+ "Pacific/Kiritimati": 76,
6981
+ "Pacific/Kosrae": 25,
6982
+ "Pacific/Kwajalein": 6,
6983
+ "Pacific/Majuro": 6,
6984
+ "Pacific/Marquesas": 83,
6985
+ "Pacific/Midway": 84,
6986
+ "Pacific/Nauru": 6,
6987
+ "Pacific/Niue": 84,
6988
+ "Pacific/Norfolk": 92,
6989
+ "Pacific/Noumea": 25,
6990
+ "Pacific/Pago_Pago": 84,
6991
+ "Pacific/Palau": 95,
6992
+ "Pacific/Pohnpei": 25,
6993
+ "Pacific/Ponape": 25,
6994
+ "Pacific/Port_Moresby": 27,
6995
+ "Pacific/Rarotonga": 63,
6996
+ "Pacific/Saipan": 27,
6997
+ "Pacific/Tahiti": 63,
6998
+ "Pacific/Tarawa": 6,
6999
+ "Pacific/Tongatapu": 94,
7000
+ "Pacific/Truk": 27,
7001
+ "Pacific/Wake": 6,
7002
+ "Pacific/Wallis": 6,
7003
+ "US/Eastern": 50,
7004
+ "US/Pacific": 96,
7005
+ UTC: 86
7006
+ };
7007
+ /** Slack's name for an IANA zone, if the Builder named it. */
7008
+ function slackTimeZoneName(timeZone) {
7009
+ const index = ZONES[timeZone];
7010
+ return index === void 0 ? void 0 : NAMES[index];
7011
+ }
7012
+ //#endregion
7013
+ //#region src/elements/dateFormat.ts
7014
+ /** "1st", "2nd", "3rd", "11th"… as Slack writes the day in a long date ("January 1st, 2026"). */
7015
+ function ordinal$1(day) {
7016
+ if (day % 10 === 1 && day !== 11) return `${day}st`;
7017
+ if (day % 10 === 2 && day !== 12) return `${day}nd`;
7018
+ if (day % 10 === 3 && day !== 13) return `${day}rd`;
7019
+ return `${day}th`;
7020
+ }
7021
+ /**
7022
+ * The name Slack shows for a time zone ("Eastern Time (US and Canada)"), from the names measured
7023
+ * in Block Kit Builder. An alias the Builder wasn't asked about is looked up by its canonical id;
7024
+ * a zone it doesn't name falls back to the IANA id.
7025
+ */
7026
+ function timeZoneLabel(timeZone) {
7027
+ const direct = slackTimeZoneName(timeZone);
7028
+ if (direct) return direct;
7029
+ try {
7030
+ const canonical = new Intl.DateTimeFormat("en-US", { timeZone }).resolvedOptions().timeZone;
7031
+ return slackTimeZoneName(canonical) ?? timeZone;
7032
+ } catch {
7033
+ return timeZone;
7034
+ }
7035
+ }
7036
+ /** A timestamp's wall-clock date ("2026-01-01") and time ("11:00") in `timeZone`. */
7037
+ function wallClock(ts, timeZone) {
7038
+ const parts = new Intl.DateTimeFormat("en-US", {
7039
+ timeZone,
7040
+ year: "numeric",
7041
+ month: "2-digit",
7042
+ day: "2-digit",
7043
+ hour: "2-digit",
7044
+ minute: "2-digit",
7045
+ hourCycle: "h23"
7046
+ }).formatToParts(/* @__PURE__ */ new Date(ts * 1e3));
7047
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
7048
+ return {
7049
+ date: `${get("year")}-${get("month")}-${get("day")}`,
7050
+ time: `${get("hour")}:${get("minute")}`
7051
+ };
7052
+ }
7053
+ /** The timestamp of a wall-clock date and time in `timeZone`. Around a daylight saving change it
7054
+ * resolves like JavaScript's `Date`: a time the clocks skip moves forward by the gap, and a time
7055
+ * they repeat takes its first occurrence. */
7056
+ function fromWallClock(date, time, timeZone) {
7057
+ const asUtc = Date.parse(`${date}T${time}:00Z`) / 1e3;
7058
+ const offsetAt = (ts) => {
7059
+ const wall = wallClock(ts, timeZone);
7060
+ return Date.parse(`${wall.date}T${wall.time}:00Z`) / 1e3 - ts;
7061
+ };
7062
+ const before = asUtc - offsetAt(asUtc - 86400);
7063
+ const matches = [before, asUtc - offsetAt(asUtc + 86400)].filter((ts) => {
7064
+ const wall = wallClock(ts, timeZone);
7065
+ return wall.date === date && wall.time === time;
7066
+ });
7067
+ return matches.length > 0 ? Math.min(...matches) : before;
7068
+ }
7069
+ const MONTHS = [
7070
+ "january",
7071
+ "february",
7072
+ "march",
7073
+ "april",
7074
+ "may",
7075
+ "june",
7076
+ "july",
7077
+ "august",
7078
+ "september",
7079
+ "october",
7080
+ "november",
7081
+ "december"
7082
+ ];
7083
+ /** `YYYY-MM-DD` for a real calendar date, or undefined (e.g. April 31st). */
7084
+ function isoDate(year, month, day) {
7085
+ const d = new Date(Date.UTC(year, month - 1, day));
7086
+ if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) return;
7087
+ return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
7088
+ }
7089
+ /** A two-digit year as Slack reads it: 69–99 in the 1900s, 00–68 in the 2000s. */
7090
+ function fullYear(year) {
7091
+ const n = Number(year);
7092
+ if (year.length > 2) return n;
7093
+ return n >= 69 ? 1900 + n : 2e3 + n;
7094
+ }
7095
+ /**
7096
+ * Reads a date typed into a datepicker field, as Slack's does: "05/01/1990", "5/4/90",
7097
+ * "1990-05-02", "May 3, 1990" or the field's own long form, "April 28th, 1990". Returns
7098
+ * `YYYY-MM-DD`, or undefined for anything else.
7099
+ */
7100
+ function parseTypedDate(input) {
7101
+ const text = input.trim();
7102
+ let m = /^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/.exec(text);
7103
+ if (m) return isoDate(fullYear(m[3]), Number(m[1]), Number(m[2]));
7104
+ m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(text);
7105
+ if (m) return isoDate(Number(m[1]), Number(m[2]), Number(m[3]));
7106
+ m = /^([a-z]+)\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})$/i.exec(text);
7107
+ if (m) {
7108
+ const name = m[1].toLowerCase();
7109
+ const index = MONTHS.findIndex((month) => month === name || name.length >= 3 && month.startsWith(name));
7110
+ if (index >= 0) return isoDate(Number(m[3]), index + 1, Number(m[2]));
7111
+ }
7112
+ }
7113
+ //#endregion
7114
+ //#region src/elements/calendar/Calendar.tsx
7115
+ function pad(n) {
7116
+ return String(n).padStart(2, "0");
7117
+ }
7118
+ function toISO(y, m, d) {
7119
+ return `${y}-${pad(m + 1)}-${pad(d)}`;
7120
+ }
7121
+ function todayISO() {
7122
+ const now = /* @__PURE__ */ new Date();
7123
+ return toISO(now.getFullYear(), now.getMonth(), now.getDate());
7124
+ }
7125
+ /** Slack's calendar header icons, 20×20. */
7126
+ const NAV_PATHS = {
7127
+ "previous-year": "M11.03 2.78a.75.75 0 1 0-1.06-1.06L2.22 9.47a.75.75 0 0 0 0 1.06l7.75 7.75a.75.75 0 1 0 1.06-1.06L3.81 10zm6.25 0a.75.75 0 0 0-1.06-1.06L8.47 9.47a.75.75 0 0 0 0 1.06l7.75 7.75a.75.75 0 1 0 1.06-1.06L10.06 10z",
7128
+ "previous-month": "M14.53 1.72a.75.75 0 0 1 0 1.06L7.31 10l7.22 7.22a.75.75 0 1 1-1.06 1.06l-7.75-7.75a.75.75 0 0 1 0-1.06l7.75-7.75a.75.75 0 0 1 1.06 0",
7129
+ "next-month": "M5.72 1.72a.75.75 0 0 1 1.06 0l7.75 7.75a.75.75 0 0 1 0 1.06l-7.75 7.75a.75.75 0 0 1-1.06-1.06L12.94 10 5.72 2.78a.75.75 0 0 1 0-1.06",
7130
+ "next-year": "M3.78 1.72a.75.75 0 0 0-1.06 1.06L9.94 10l-7.22 7.22a.75.75 0 1 0 1.06 1.06l7.75-7.75a.75.75 0 0 0 0-1.06zm6.25 0a.75.75 0 1 0-1.06 1.06L16.19 10l-7.22 7.22a.75.75 0 1 0 1.06 1.06l7.75-7.75a.75.75 0 0 0 0-1.06z"
7131
+ };
7132
+ /** A header button that moves the calendar by `months`. */
7133
+ function NavButton({ icon, label, onClick }) {
7134
+ return /* @__PURE__ */ jsx("button", {
7135
+ type: "button",
7136
+ className: "sbk-calendar__nav",
7137
+ onClick,
7138
+ "aria-label": label,
7139
+ children: /* @__PURE__ */ jsx("svg", {
7140
+ viewBox: "0 0 20 20",
7141
+ width: "20",
7142
+ height: "20",
7143
+ "aria-hidden": "true",
7144
+ children: /* @__PURE__ */ jsx("path", {
7145
+ fill: "currentColor",
7146
+ fillRule: "evenodd",
7147
+ clipRule: "evenodd",
7148
+ d: NAV_PATHS[icon]
7149
+ })
7150
+ })
7151
+ });
7152
+ }
7153
+ /** A day's name in full, as Slack labels its day buttons: "Saturday, April 28th, 1990". */
7154
+ function dayLabel(iso) {
7155
+ const [y = 0, m = 1, d = 1] = iso.split("-").map(Number);
7156
+ const date = new Date(Date.UTC(y, m - 1, d));
7157
+ return `${date.toLocaleString("en-US", {
7158
+ weekday: "long",
7159
+ timeZone: "UTC"
7160
+ })}, ${date.toLocaleString("en-US", {
7161
+ month: "long",
7162
+ timeZone: "UTC"
7163
+ })} ${ordinal$1(d)}, ${y}`;
7164
+ }
7165
+ /** The day `delta` days after the `YYYY-MM-DD` date, as `YYYY-MM-DD`. */
7166
+ function addDays(iso, delta) {
7167
+ const d = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
7168
+ d.setUTCDate(d.getUTCDate() + delta);
7169
+ return toISO(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
7170
+ }
7171
+ /** Days the arrow keys move the focused day: a day sideways, a week up or down. */
7172
+ const ARROW_DAYS = {
7173
+ ArrowLeft: -1,
7174
+ ArrowRight: 1,
7175
+ ArrowUp: -7,
7176
+ ArrowDown: 7
7177
+ };
7178
+ /** A month grid matching Slack's `c-date_picker_calendar`: a bordered 7-column grid of 44×42
7179
+ * day cells, a ring around today, and solid blue for the selected day. The grid is one tab stop:
7180
+ * the focused day (the selected one, else today, else the 1st) takes Tab, and the arrow keys move
7181
+ * it a day or a week, turning the month when they cross its edge. */
7182
+ function Calendar({ value, onSelect, onClear }) {
7183
+ const initial = value ? /* @__PURE__ */ new Date(`${value}T00:00:00Z`) : /* @__PURE__ */ new Date();
7184
+ const [year, setYear] = useState(initial.getUTCFullYear());
7185
+ const [month, setMonth] = useState(initial.getUTCMonth());
7186
+ const [focused, setFocused] = useState(() => {
7187
+ if (value) return value;
7188
+ const today = todayISO();
7189
+ return today.startsWith(toISO(year, month, 1).slice(0, 7)) ? today : toISO(year, month, 1);
7190
+ });
7191
+ const gridRef = useRef(null);
7192
+ const [keyboard, setKeyboard] = useState(false);
7193
+ const moveFocus = useRef(false);
7194
+ useEffect(() => {
7195
+ if (!moveFocus.current) return;
7196
+ moveFocus.current = false;
7197
+ gridRef.current?.querySelector(`[data-date="${focused}"]`)?.focus();
7198
+ }, [
7199
+ focused,
7200
+ year,
7201
+ month
7202
+ ]);
7203
+ function onGridKeyDown(e) {
7204
+ const delta = ARROW_DAYS[e.key];
7205
+ if (delta === void 0) return;
7206
+ e.preventDefault();
7207
+ setKeyboard(true);
7208
+ const next = addDays(focused, delta);
7209
+ const [y = year, m = month + 1] = next.split("-").map(Number);
7210
+ moveFocus.current = true;
7211
+ setFocused(next);
7212
+ if (y !== year || m - 1 !== month) {
7213
+ setYear(y);
7214
+ setMonth(m - 1);
7215
+ }
7216
+ }
7217
+ const shown = toISO(year, month, 1).slice(0, 7);
7218
+ const tabStop = focused.startsWith(shown) ? focused : toISO(year, month, 1);
7219
+ const first = new Date(Date.UTC(year, month, 1));
7220
+ const startWeekday = first.getUTCDay();
7221
+ const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
7222
+ const cells = [...Array(startWeekday).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)];
7223
+ const today = todayISO();
7224
+ const lastRow = Math.floor((startWeekday + daysInMonth - 1) / 7);
7225
+ const lastRowFull = (startWeekday + daysInMonth) % 7 === 0;
7226
+ /** The grid's outer corners a day sits on, which Slack rounds. */
7227
+ function corners(day) {
7228
+ const at = startWeekday + day - 1;
7229
+ const row = Math.floor(at / 7);
7230
+ const col = at % 7;
7231
+ const out = [];
7232
+ if (day === 1 || row === 1 && col === 0) out.push("top-left");
7233
+ if (row === 0 && col === 6) out.push("top-right");
7234
+ if (row === lastRow && col === 0) out.push("bottom-left");
7235
+ if (day === daysInMonth || !lastRowFull && row === lastRow - 1 && col === 6) out.push("bottom-right");
7236
+ return out;
7237
+ }
7238
+ function go(delta) {
7239
+ const d = new Date(Date.UTC(year, month + delta, 1));
7240
+ setYear(d.getUTCFullYear());
7241
+ setMonth(d.getUTCMonth());
7242
+ }
7243
+ const monthLabel = first.toLocaleDateString("en-US", {
7244
+ month: "long",
7245
+ year: "numeric",
7246
+ timeZone: "UTC"
7247
+ });
7248
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
7249
+ className: "sbk-calendar",
7250
+ children: [
7251
+ /* @__PURE__ */ jsxs("div", {
7252
+ className: "sbk-calendar__header",
7253
+ children: [
7254
+ /* @__PURE__ */ jsx(NavButton, {
7255
+ icon: "previous-year",
7256
+ label: "Previous year",
7257
+ onClick: () => go(-12)
7258
+ }),
7259
+ /* @__PURE__ */ jsx(NavButton, {
7260
+ icon: "previous-month",
7261
+ label: "Previous month",
7262
+ onClick: () => go(-1)
7263
+ }),
7264
+ /* @__PURE__ */ jsx("span", {
7265
+ className: "sbk-calendar__label",
7266
+ children: monthLabel
7267
+ }),
7268
+ /* @__PURE__ */ jsx(NavButton, {
7269
+ icon: "next-month",
7270
+ label: "Next month",
7271
+ onClick: () => go(1)
7272
+ }),
7273
+ /* @__PURE__ */ jsx(NavButton, {
7274
+ icon: "next-year",
7275
+ label: "Next year",
7276
+ onClick: () => go(12)
7277
+ })
7278
+ ]
7279
+ }),
7280
+ /* @__PURE__ */ jsx("div", {
7281
+ className: "sbk-calendar__weekdays",
7282
+ children: [
7283
+ "Su",
7284
+ "Mo",
7285
+ "Tu",
7286
+ "We",
7287
+ "Th",
7288
+ "Fr",
7289
+ "Sa"
7290
+ ].map((w) => /* @__PURE__ */ jsx("span", { children: w }, w))
7291
+ }),
7292
+ /* @__PURE__ */ jsx("div", {
7293
+ className: `sbk-calendar__grid${keyboard ? " sbk-calendar__grid--keyboard" : ""}`,
7294
+ ref: gridRef,
7295
+ onKeyDown: onGridKeyDown,
7296
+ onPointerDown: () => setKeyboard(false),
7297
+ children: cells.map((day, i) => {
7298
+ if (day === null) return /* @__PURE__ */ jsx("span", { className: "sbk-calendar__cell sbk-calendar__cell--empty" }, i);
7299
+ const iso = toISO(year, month, day);
7300
+ const classes = ["sbk-calendar__cell", ...corners(day).map((c) => `sbk-calendar__cell--${c}`)];
7301
+ if (iso === value) classes.push("sbk-calendar__cell--selected");
7302
+ if (iso === today) classes.push("sbk-calendar__cell--today");
7303
+ return /* @__PURE__ */ jsx("button", {
7304
+ type: "button",
7305
+ className: classes.join(" "),
7306
+ "aria-pressed": iso === value,
7307
+ "aria-current": iso === today ? "date" : void 0,
7308
+ "data-date": iso,
7309
+ "aria-label": dayLabel(iso),
7310
+ tabIndex: iso === tabStop ? 0 : -1,
7311
+ onFocus: () => setFocused(iso),
7312
+ onClick: () => onSelect(iso),
7313
+ children: day
7314
+ }, iso);
7315
+ })
7316
+ })
7317
+ ]
7318
+ }), onClear && value && /* @__PURE__ */ jsx("div", {
7319
+ className: "sbk-calendar__footer",
7320
+ children: /* @__PURE__ */ jsx("button", {
7321
+ type: "button",
7322
+ className: "sbk-calendar__clear",
7323
+ onClick: onClear,
7324
+ children: "Clear selection"
7325
+ })
7326
+ })] });
7327
+ }
7328
+ //#endregion
7329
+ //#region src/elements/calendar/CalendarPopover.tsx
7330
+ /** Moves focus onto the calendar's focused day: the selected one, else today, else the 1st. */
7331
+ function focusCalendarDay(popup) {
7332
+ popup?.querySelector(".sbk-calendar__grid [tabindex='0']")?.focus();
7333
+ }
7334
+ /**
7335
+ * Slack's datepicker dropdown (`c-date_picker__dropdown`): the month calendar in a popover that
7336
+ * overlaps the bottom of its field by 4px, its right edge on the field's. The datepicker and the
7337
+ * date half of the datetimepicker both open it.
7338
+ */
7339
+ function CalendarPopover({ anchorRef, popupRef, value, onSelect, onClear, onDismiss, onEscape, focusOnOpen, offsetX }) {
7340
+ return /* @__PURE__ */ jsx(Popover, {
7341
+ anchorRef,
7342
+ gap: -4,
7343
+ offsetX,
7344
+ onDismiss,
7345
+ onPlaced: () => {
7346
+ if (focusOnOpen()) focusCalendarDay(popupRef.current);
7347
+ },
7348
+ children: /* @__PURE__ */ jsx("div", {
7349
+ className: "sbk-datepicker__popup",
7350
+ ref: popupRef,
7351
+ role: "dialog",
7352
+ "aria-label": "Choose a date",
7353
+ onKeyDown: (e) => {
7354
+ if (e.key !== "Escape") return;
7355
+ e.preventDefault();
7356
+ onEscape();
7357
+ },
7358
+ children: /* @__PURE__ */ jsx(Calendar, {
7359
+ value,
7360
+ onSelect,
7361
+ onClear
7362
+ })
7363
+ })
7364
+ });
7365
+ }
7366
+ //#endregion
7367
+ //#region src/elements/inputBlockContext.ts
7368
+ /** True while rendering the element of an `input` block, where Slack lays some elements out differently. */
7369
+ const InputBlockContext = createContext(false);
7370
+ function useInInputBlock() {
7371
+ return useContext(InputBlockContext);
7372
+ }
7373
+ /** True while rendering the element of an `input` block marked `optional`, which Slack lets the user empty. */
7374
+ const InputOptionalContext = createContext(false);
7375
+ /**
7376
+ * True for an `input` block's element that Slack lets the user empty with "Clear selection": an
7377
+ * optional input, or any input on the message surface, where Slack treats inputs as optional (it
7378
+ * shows no "(optional)" suffix there either). Measured in Block Kit Builder's references.
7379
+ */
7380
+ function useInClearableInput() {
7381
+ const inInput = useContext(InputBlockContext);
7382
+ const optional = useContext(InputOptionalContext);
7383
+ const { surface } = useBlockKit();
7384
+ return inInput && (optional || surface === "message");
7385
+ }
7386
+ /** The id of the error shown under an `input` block's element, while it has one. */
7387
+ const InputErrorContext = createContext(void 0);
7388
+ /**
7389
+ * Props for the element's focusable control: an `input` block with an error marks it invalid,
7390
+ * which also gives it Slack's red focus ring, and describes it with the error text.
7391
+ */
7392
+ function useInvalidProps() {
7393
+ const errorId = useContext(InputErrorContext);
7394
+ return errorId ? {
7395
+ "aria-invalid": true,
7396
+ "aria-describedby": errorId
7397
+ } : {};
7398
+ }
7399
+ //#endregion
7400
+ //#region src/elements/DatePicker.tsx
7401
+ /**
7402
+ * Formats `YYYY-MM-DD` the way Slack's closed datepicker control shows it: "04/28/1990" in an
7403
+ * actions block or section accessory, and "April 28th, 1990" in an `input` block.
7404
+ */
7405
+ function formatDate(date, long) {
7406
+ const [y = 0, m = 1, d = 1] = date.split("-").map(Number);
7407
+ if (long) return `${new Date(Date.UTC(y, m - 1, 1)).toLocaleString("en-US", {
7408
+ month: "long",
7409
+ timeZone: "UTC"
7410
+ })} ${ordinal$1(d)}, ${y}`;
7411
+ return `${String(m).padStart(2, "0")}/${String(d).padStart(2, "0")}/${y}`;
7412
+ }
7413
+ function DatePicker({ element, blockId }) {
7414
+ const { setValue, dispatch, surface } = useBlockKit();
7415
+ const medium = surface !== "message";
7416
+ const inInputBlock = useInInputBlock();
7417
+ const clearable = useInClearableInput();
7418
+ const { ask, dialog } = useConfirm(element.confirm);
7419
+ const [date, setDate] = useState(element.initial_date);
7420
+ const [open, setOpen] = useState(false);
7421
+ const rootRef = useRef(null);
7422
+ const inputRef = useFocusOnLoad(element);
7423
+ const invalid = useInvalidProps();
7424
+ const quietFocus = useRef(element.focus_on_load === true);
7425
+ const focusCalendar = useRef(false);
7426
+ const popupRef = useRef(null);
7427
+ const actionId = element.action_id ?? "";
7428
+ const [text, setText] = useState(null);
7429
+ function focusDay() {
7430
+ focusCalendarDay(popupRef.current);
7431
+ }
7432
+ function close() {
7433
+ setOpen(false);
7434
+ const input = inputRef.current;
7435
+ if (!input || document.activeElement === input) return;
7436
+ quietFocus.current = true;
7437
+ input.focus();
7438
+ }
7439
+ useEffect(() => {
7440
+ if (date) setValue(blockId, actionId, {
7441
+ type: "datepicker",
7442
+ selected_date: date
7443
+ });
7444
+ }, []);
7445
+ async function pick(next) {
7446
+ if (!await ask()) return;
7447
+ close();
7448
+ commit(next);
7449
+ }
7450
+ /** Chooses `next` and sends it, as a pick in the calendar or a typed date does. */
7451
+ function commit(next) {
7452
+ setDate(next);
7453
+ setValue(blockId, actionId, {
7454
+ type: "datepicker",
7455
+ selected_date: next
7456
+ });
7457
+ dispatch({
7458
+ type: "datepicker",
7459
+ action_id: actionId,
7460
+ block_id: blockId,
7461
+ selected_date: next,
7462
+ ...element.initial_date !== void 0 ? { initial_date: element.initial_date } : {}
7463
+ });
7464
+ }
7465
+ /**
7466
+ * A typed date: Enter or leaving the field takes it, as Slack's field does, and anything that
7467
+ * isn't a date is ignored. The calendar stays open after Enter, as in Slack.
7468
+ */
7469
+ async function takeTyped() {
7470
+ if (text === null) return;
7471
+ const next = parseTypedDate(text);
7472
+ if (!next) return;
7473
+ if (next !== date && !await ask()) return;
7474
+ if (next !== date) commit(next);
7475
+ }
7476
+ async function clear() {
7477
+ if (!await ask()) return;
7478
+ setDate(void 0);
7479
+ close();
7480
+ setValue(blockId, actionId, {
7481
+ type: "datepicker",
7482
+ selected_date: null
7483
+ });
7484
+ dispatch({
7485
+ type: "datepicker",
7486
+ action_id: actionId,
7487
+ block_id: blockId,
7488
+ selected_date: null,
7489
+ ...element.initial_date !== void 0 ? { initial_date: element.initial_date } : {}
7490
+ });
7491
+ }
7492
+ return /* @__PURE__ */ jsxs("div", {
7493
+ className: `sbk-datepicker${medium ? " sbk-datepicker--medium" : ""}`,
7494
+ ref: rootRef,
7495
+ children: [
7496
+ /* @__PURE__ */ jsxs("div", {
7497
+ className: "sbk-datepicker__input_wrap",
7498
+ children: [
7499
+ /* @__PURE__ */ jsx(CalendarIcon, { className: "sbk-datepicker__icon" }),
7500
+ /* @__PURE__ */ jsx("input", {
7501
+ className: "sbk-datepicker__input",
7502
+ type: "text",
7503
+ autoComplete: "off",
7504
+ spellCheck: false,
7505
+ value: text ?? (date ? formatDate(date, inInputBlock) : ""),
7506
+ placeholder: element.placeholder?.text ?? "Select a date",
7507
+ "aria-label": "Date",
7508
+ ...invalid,
7509
+ ref: inputRef,
7510
+ onChange: (e) => {
7511
+ setText(e.target.value);
7512
+ setOpen(true);
7513
+ },
7514
+ onFocus: () => {
7515
+ if (quietFocus.current) quietFocus.current = false;
7516
+ else setOpen(true);
7517
+ },
7518
+ onBlur: async () => {
7519
+ await takeTyped();
7520
+ setText(null);
7521
+ },
7522
+ onMouseDown: () => {
7523
+ focusCalendar.current = true;
7524
+ },
7525
+ onClick: () => {
7526
+ if (open) focusDay();
7527
+ else focusCalendar.current = true;
7528
+ setOpen(true);
7529
+ },
7530
+ onKeyDown: (e) => {
7531
+ if (e.key === "Escape") {
7532
+ setText(null);
7533
+ setOpen(false);
7534
+ }
7535
+ if (e.key === "Enter") {
7536
+ e.preventDefault();
7537
+ takeTyped();
7538
+ }
7539
+ if (e.key === "ArrowDown") {
7540
+ e.preventDefault();
7541
+ if (open) focusDay();
7542
+ else {
7543
+ focusCalendar.current = true;
7544
+ setOpen(true);
7545
+ }
7546
+ }
7547
+ }
7548
+ }),
7549
+ /* @__PURE__ */ jsx("button", {
7550
+ type: "button",
7551
+ className: "sbk-datepicker__toggle",
7552
+ "aria-label": "Open calendar",
7553
+ onClick: () => {
7554
+ focusCalendar.current = true;
7555
+ if (open) focusDay();
7556
+ else setOpen(true);
7557
+ },
7558
+ children: /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datepicker__chevron" })
7559
+ })
7560
+ ]
7561
+ }),
7562
+ open && /* @__PURE__ */ jsx(CalendarPopover, {
7563
+ anchorRef: rootRef,
7564
+ popupRef,
7565
+ offsetX: medium ? -4 : void 0,
7566
+ value: date,
7567
+ onSelect: pick,
7568
+ onClear: clearable ? clear : void 0,
7569
+ onDismiss: () => setOpen(false),
7570
+ onEscape: close,
7571
+ focusOnOpen: () => {
7572
+ const focus = focusCalendar.current;
7573
+ focusCalendar.current = false;
7574
+ return focus;
7575
+ }
7576
+ }),
7577
+ dialog
7578
+ ]
7579
+ });
7580
+ }
7581
+ //#endregion
7582
+ //#region src/elements/useMenuNavigation.ts
7583
+ /**
7584
+ * Slack's menus and option lists keep one "active" row that follows both the pointer and the
7585
+ * arrow keys (`c-menu_item--highlighted` / `c-select_options_list__option--active`), so hover and
7586
+ * keyboard highlight share one style. Returns the active index plus handlers for the list.
7587
+ */
7588
+ function useMenuNavigation({ open, count, initialIndex = -1, onChoose, onClose, onOpen, listRef }) {
7589
+ const [active, setActive] = useState(-1);
7590
+ useEffect(() => {
7591
+ setActive(open ? initialIndex : -1);
7592
+ }, [open]);
7593
+ useEffect(() => {
7594
+ if (active >= count) setActive(count - 1);
7595
+ }, [active, count]);
7596
+ useEffect(() => {
7597
+ (listRef?.current?.querySelector("[data-active]"))?.scrollIntoView?.({ block: "nearest" });
7598
+ }, [active, listRef]);
7599
+ function onKeyDown(e) {
7600
+ if (!open) {
7601
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
7602
+ e.preventDefault();
7603
+ onOpen?.();
7604
+ }
7605
+ return;
7606
+ }
7607
+ switch (e.key) {
7608
+ case "ArrowDown":
7609
+ e.preventDefault();
7610
+ setActive((i) => count === 0 ? -1 : (i + 1) % count);
7611
+ break;
7612
+ case "ArrowUp":
7613
+ e.preventDefault();
7614
+ setActive((i) => count === 0 ? -1 : i <= 0 ? count - 1 : i - 1);
7615
+ break;
7616
+ case "Home":
7617
+ e.preventDefault();
7618
+ setActive(count > 0 ? 0 : -1);
7619
+ break;
7620
+ case "End":
7621
+ e.preventDefault();
7622
+ setActive(count - 1);
7623
+ break;
7624
+ case "Enter":
7625
+ if (active >= 0 && active < count) {
7626
+ e.preventDefault();
7627
+ onChoose(active);
7628
+ }
7629
+ break;
7630
+ case "Escape":
7631
+ e.preventDefault();
7632
+ onClose();
7633
+ }
7634
+ }
7635
+ /** Props for the row at `index`: pointer movement moves the highlight, like Slack's lists. */
7636
+ function itemProps(index) {
7637
+ return {
7638
+ onMouseEnter: () => setActive(index),
7639
+ onMouseMove: () => {
7640
+ if (active !== index) setActive(index);
7641
+ },
7642
+ "data-active": active === index || void 0
7643
+ };
7644
+ }
7645
+ return {
7646
+ active,
7647
+ setActive,
7648
+ onKeyDown,
7649
+ itemProps
7650
+ };
7651
+ }
7652
+ //#endregion
7653
+ //#region src/elements/useCombobox.ts
7654
+ /**
7655
+ * The typeable trigger Slack's select and time pickers share (`c-select_input`): a text input with
7656
+ * role="combobox" that shows the chosen value while closed, and the query while its list is open.
7657
+ * Typing filters the list and highlights its first row; the arrow keys move the highlight, Enter
7658
+ * picks it, Escape and Tab close the list and put the chosen value back.
7659
+ */
7660
+ function useCombobox({ query, onQueryChange, count, initialIndex, onChoose, onSubmitQuery, listRef, returnFocusRef, open: controlledOpen, onOpenChange, keepQuery = false, typedHighlight = false, highlightOnType = true, holdFirstArrow = true, chosenAsPlaceholder = true }) {
7661
+ const [ownOpen, setOwnOpen] = useState(false);
7662
+ const open = controlledOpen ?? ownOpen;
7663
+ const setOpenState = onOpenChange ?? setOwnOpen;
7664
+ const inputRef = useRef(null);
7665
+ const id = useId();
7666
+ const listId = `${id}-list`;
7667
+ const optionId = (index) => `${id}-option-${index}`;
7668
+ const [typed, setTyped] = useState(true);
7669
+ const pointerOpen = useRef(false);
7670
+ useEffect(() => {
7671
+ if (open) setTyped(!pointerOpen.current);
7672
+ else pointerOpen.current = false;
7673
+ }, [open]);
7674
+ function setOpen(next) {
7675
+ setOpenState(next);
7676
+ if (!next && !keepQuery) onQueryChange("");
7677
+ }
7678
+ const nav = useMenuNavigation({
7679
+ open,
7680
+ count,
7681
+ initialIndex,
7682
+ onChoose,
7683
+ onClose: () => {
7684
+ setOpen(false);
7685
+ (returnFocusRef ?? inputRef).current?.focus();
7686
+ },
7687
+ onOpen: () => setOpenState(true),
7688
+ listRef
7689
+ });
7690
+ /** Keys for the input, or for a container around a button trigger and its list. */
7691
+ function onKeyDown(e) {
7692
+ if (e.key === "Tab") {
7693
+ if (open) setOpen(false);
7694
+ return;
7695
+ }
7696
+ if (open && e.key === "Enter" && nav.active < 0 && query.trim()) {
7697
+ e.preventDefault();
7698
+ onSubmitQuery?.(query.trim());
7699
+ return;
7700
+ }
7701
+ if (typedHighlight && open && typed && [
7702
+ "ArrowDown",
7703
+ "ArrowUp",
7704
+ "Home",
7705
+ "End"
7706
+ ].includes(e.key)) {
7707
+ setTyped(false);
7708
+ if ((e.key === "ArrowDown" || e.key === "ArrowUp") && nav.active >= 0 && holdFirstArrow) {
7709
+ e.preventDefault();
7710
+ return;
7711
+ }
7712
+ }
7713
+ nav.onKeyDown(e);
7714
+ }
7715
+ /** Props for the input. `display` is the chosen value, shown while the list is closed. */
7716
+ /** The combobox input's props; `label` names it for screen readers, else the placeholder does. */
7717
+ function inputProps(display, placeholder, label = placeholder) {
7718
+ return {
7719
+ ref: inputRef,
7720
+ type: "text",
7721
+ role: "combobox",
7722
+ autoComplete: "off",
7723
+ size: 1,
7724
+ spellCheck: false,
7725
+ "aria-autocomplete": "list",
7726
+ "aria-expanded": open,
7727
+ "aria-controls": open ? listId : void 0,
7728
+ "aria-activedescendant": open && nav.active >= 0 ? optionId(nav.active) : void 0,
7729
+ "aria-label": label,
7730
+ placeholder: open && chosenAsPlaceholder ? display ?? placeholder : placeholder,
7731
+ value: open || keepQuery && query ? query : display ?? "",
7732
+ onClick: () => {
7733
+ if (!open) pointerOpen.current = true;
7734
+ setOpenState(true);
7735
+ },
7736
+ onChange: (e) => {
7737
+ pointerOpen.current = false;
7738
+ onQueryChange(e.target.value);
7739
+ setOpenState(true);
7740
+ setTyped(true);
7741
+ nav.setActive(highlightOnType ? 0 : -1);
7742
+ },
7743
+ onKeyDown
7744
+ };
7745
+ }
7746
+ /** Props for the row at `index`, tying it to the input's aria-activedescendant. */
7747
+ function optionProps(index) {
7748
+ const item = nav.itemProps(index);
7749
+ return {
7750
+ id: optionId(index),
7751
+ ...item,
7752
+ onMouseMove: () => {
7753
+ if (typedHighlight) setTyped(false);
7754
+ item.onMouseMove();
7755
+ }
7756
+ };
7757
+ }
7758
+ return {
7759
+ open,
7760
+ setOpen,
7761
+ onKeyDown,
7762
+ listId,
7763
+ optionId,
7764
+ inputRef,
7765
+ inputProps,
7766
+ optionProps,
7767
+ active: nav.active,
7768
+ /** Whether the highlighted row is the typed highlight (with `typedHighlight`). */
7769
+ typed: typedHighlight && typed
7770
+ };
7771
+ }
7772
+ //#endregion
7773
+ //#region src/elements/TimePicker.tsx
7774
+ /** Formats `HH:mm` the way Slack's timepicker shows it, e.g. "1:37 PM". */
7775
+ function formatTime(time) {
7776
+ const [h = 0, m = 0] = time.split(":").map(Number);
7777
+ const period = h >= 12 ? "PM" : "AM";
7778
+ return `${h % 12 === 0 ? 12 : h % 12}:${String(m).padStart(2, "0")} ${period}`;
7779
+ }
7780
+ /** Every hour of the day, as Slack's timepicker list offers. */
7781
+ const HOURS = Array.from({ length: 24 }, (_, h) => `${String(h).padStart(2, "0")}:00`);
6471
7782
  /**
6472
- * Formats `YYYY-MM-DD` the way Slack's closed datepicker control shows it: "04/28/1990" in an
6473
- * actions block or section accessory, and "April 28th, 1990" in an `input` block.
7783
+ * Reads a typed time as `HH:mm`: "2:15 PM", "2:15pm", "2pm", "14:15" or "9". Without AM or PM the
7784
+ * hour is read on a 24-hour clock. Slack accepts typed times too; whether it keeps one off the hour
7785
+ * (2:15 PM) or rounds it hasn't been verified, so the minutes are kept as typed.
6474
7786
  */
6475
- function formatDate$1(date, long) {
6476
- const [y = 0, m = 1, d = 1] = date.split("-").map(Number);
6477
- if (long) return `${new Date(Date.UTC(y, m - 1, 1)).toLocaleString("en-US", {
6478
- month: "long",
6479
- timeZone: "UTC"
6480
- })} ${ordinal$1(d)}, ${y}`;
6481
- return `${String(m).padStart(2, "0")}/${String(d).padStart(2, "0")}/${y}`;
7787
+ function parseTime(input) {
7788
+ const match = /^(\d{1,2})(?::?(\d{2}))?\s*([ap])\.?m?\.?$|^(\d{1,2})(?::(\d{2}))?$/i.exec(input.trim());
7789
+ if (!match) return void 0;
7790
+ const meridiem = match[3]?.toLowerCase();
7791
+ let hour = Number(match[1] ?? match[4]);
7792
+ const minute = Number(match[2] ?? match[5] ?? 0);
7793
+ if (minute > 59) return void 0;
7794
+ if (meridiem) {
7795
+ if (hour < 1 || hour > 12) return void 0;
7796
+ hour = hour % 12 + (meridiem === "p" ? 12 : 0);
7797
+ } else if (hour > 23) return;
7798
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
6482
7799
  }
6483
- function DatePicker({ element, blockId }) {
6484
- const { setValue, dispatch } = useBlockKit();
6485
- const inInputBlock = useInInputBlock();
7800
+ /**
7801
+ * The rows of Slack's time list: every hour, whatever is typed. Slack's list doesn't filter as you
7802
+ * type; Enter reads the typed time instead.
7803
+ */
7804
+ const TIMES = HOURS;
7805
+ /** The "Clear selection" row's key in the list. */
7806
+ const CLEAR$1 = "clear";
7807
+ function TimePicker({ element, blockId }) {
7808
+ const { setValue, dispatch, surface } = useBlockKit();
7809
+ const medium = surface !== "message";
6486
7810
  const { ask, dialog } = useConfirm(element.confirm);
6487
- const [date, setDate] = useState(element.initial_date);
6488
- const [open, setOpen] = useState(false);
7811
+ const [time, setTime] = useState(element.initial_time);
6489
7812
  const rootRef = useRef(null);
6490
- const inputRef = useFocusOnLoad(element);
7813
+ const listRef = useRef(null);
6491
7814
  const invalid = useInvalidProps();
6492
- const loadFocus = useRef(element.focus_on_load === true);
7815
+ const inInputBlock = useInInputBlock();
7816
+ const clearable = useInClearableInput();
6493
7817
  const actionId = element.action_id ?? "";
6494
7818
  useEffect(() => {
6495
- if (date) setValue(blockId, actionId, {
6496
- type: "datepicker",
6497
- selected_date: date
7819
+ if (time) setValue(blockId, actionId, {
7820
+ type: "timepicker",
7821
+ selected_time: time
6498
7822
  });
6499
7823
  }, []);
6500
7824
  async function pick(next) {
7825
+ combo.setOpen(false);
6501
7826
  if (!await ask()) return;
6502
- setDate(next);
6503
- setOpen(false);
7827
+ setTime(next);
6504
7828
  setValue(blockId, actionId, {
6505
- type: "datepicker",
6506
- selected_date: next
7829
+ type: "timepicker",
7830
+ selected_time: next
6507
7831
  });
6508
7832
  dispatch({
6509
- type: "datepicker",
7833
+ type: "timepicker",
6510
7834
  action_id: actionId,
6511
7835
  block_id: blockId,
6512
- selected_date: next,
6513
- ...element.initial_date !== void 0 ? { initial_date: element.initial_date } : {}
7836
+ selected_time: next,
7837
+ ...element.initial_time !== void 0 ? { initial_time: element.initial_time } : {}
6514
7838
  });
6515
7839
  }
6516
7840
  async function clear() {
7841
+ combo.setOpen(false);
6517
7842
  if (!await ask()) return;
6518
- setDate(void 0);
6519
- setOpen(false);
7843
+ setTime(void 0);
7844
+ setQuery("");
6520
7845
  setValue(blockId, actionId, {
6521
- type: "datepicker",
6522
- selected_date: null
7846
+ type: "timepicker",
7847
+ selected_time: null
6523
7848
  });
6524
7849
  dispatch({
6525
- type: "datepicker",
7850
+ type: "timepicker",
6526
7851
  action_id: actionId,
6527
7852
  block_id: blockId,
6528
- selected_date: null,
6529
- ...element.initial_date !== void 0 ? { initial_date: element.initial_date } : {}
7853
+ selected_time: null,
7854
+ ...element.initial_time !== void 0 ? { initial_time: element.initial_time } : {}
6530
7855
  });
6531
7856
  }
7857
+ const [query, setQuery] = useState("");
7858
+ const rows = [...clearable && time !== void 0 && (query === "" || query === formatTime(time)) ? [CLEAR$1] : [], ...TIMES];
7859
+ const combo = useCombobox({
7860
+ query,
7861
+ onQueryChange: setQuery,
7862
+ count: rows.length,
7863
+ initialIndex: -1,
7864
+ onChoose: (i) => {
7865
+ const row = rows[i];
7866
+ if (row === CLEAR$1) clear();
7867
+ else if (row) pick(row);
7868
+ },
7869
+ onSubmitQuery: (typed) => {
7870
+ const t = parseTime(typed);
7871
+ if (t) pick(t);
7872
+ },
7873
+ listRef,
7874
+ highlightOnType: false
7875
+ });
7876
+ useFocusOnLoad(element, combo.inputRef);
7877
+ const selectOnOpen = useRef(false);
7878
+ useEffect(() => {
7879
+ if (!combo.open || !time) return;
7880
+ selectOnOpen.current = true;
7881
+ setQuery(formatTime(time));
7882
+ }, [combo.open]);
7883
+ useClientLayoutEffect(() => {
7884
+ if (!selectOnOpen.current) return;
7885
+ selectOnOpen.current = false;
7886
+ combo.inputRef.current?.select();
7887
+ }, [query]);
7888
+ const display = time ? formatTime(time) : void 0;
7889
+ const input = combo.inputProps(display, element.placeholder?.text ?? "Select time", "Time");
7890
+ const overlay = display !== void 0 && !combo.open;
6532
7891
  return /* @__PURE__ */ jsxs("div", {
6533
- className: "sbk-datepicker",
7892
+ className: `sbk-timepicker${medium ? " sbk-timepicker--medium" : ""}`,
6534
7893
  ref: rootRef,
6535
7894
  children: [
6536
- /* @__PURE__ */ jsxs("div", {
6537
- className: "sbk-datepicker__input_wrap",
7895
+ /* @__PURE__ */ jsxs("label", {
7896
+ className: "sbk-timepicker__control",
6538
7897
  children: [
6539
- /* @__PURE__ */ jsx(CalendarIcon, { className: "sbk-datepicker__icon" }),
7898
+ /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-timepicker__icon" }),
6540
7899
  /* @__PURE__ */ jsx("input", {
6541
- className: "sbk-datepicker__input",
6542
- readOnly: true,
6543
- value: date ? formatDate$1(date, inInputBlock) : "",
6544
- placeholder: element.placeholder?.text ?? "Select a date",
6545
- "aria-haspopup": "dialog",
6546
- "aria-expanded": open,
7900
+ ...input,
6547
7901
  ...invalid,
6548
- ref: inputRef,
6549
- onFocus: () => {
6550
- if (loadFocus.current) loadFocus.current = false;
6551
- else setOpen(true);
6552
- },
6553
- onClick: () => setOpen(true),
6554
- onKeyDown: (e) => {
6555
- if (e.key === "Escape") setOpen(false);
6556
- }
7902
+ className: `sbk-timepicker__input${overlay ? " sbk-timepicker__input--behind" : ""}`
6557
7903
  }),
6558
- /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datepicker__chevron" })
7904
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-timepicker__chevron" }),
7905
+ overlay && /* @__PURE__ */ jsx("span", {
7906
+ className: "sbk-timepicker__content",
7907
+ "aria-hidden": "true",
7908
+ children: /* @__PURE__ */ jsx("span", {
7909
+ className: "sbk-timepicker__content-text",
7910
+ children: display
7911
+ })
7912
+ })
6559
7913
  ]
6560
7914
  }),
6561
- open && /* @__PURE__ */ jsx(Popover, {
7915
+ combo.open && /* @__PURE__ */ jsx(Popover, {
6562
7916
  anchorRef: rootRef,
6563
- onDismiss: () => setOpen(false),
7917
+ onDismiss: () => combo.setOpen(false),
7918
+ offsetX: -12,
7919
+ gap: -4,
6564
7920
  children: /* @__PURE__ */ jsx("div", {
6565
- className: "sbk-datepicker__popup",
6566
- children: /* @__PURE__ */ jsx(Calendar, {
6567
- value: date,
6568
- onSelect: pick,
6569
- onClear: clear
6570
- })
7921
+ className: `sbk-timepicker__menu${inInputBlock ? " sbk-timepicker__menu--input" : ""}`,
7922
+ role: "listbox",
7923
+ id: combo.listId,
7924
+ ref: listRef,
7925
+ children: rows.map((row, i) => /* @__PURE__ */ jsx("div", {
7926
+ role: "option",
7927
+ "aria-selected": row === time,
7928
+ className: `sbk-timepicker__option${row === CLEAR$1 ? " sbk-timepicker__option--clear" : ""}${row === time ? " sbk-timepicker__option--selected" : ""}`,
7929
+ onMouseDown: (e) => e.preventDefault(),
7930
+ onClick: () => row === CLEAR$1 ? clear() : pick(row),
7931
+ ...combo.optionProps(i),
7932
+ children: row === CLEAR$1 ? "Clear selection" : formatTime(row)
7933
+ }, row))
6571
7934
  })
6572
7935
  }),
7936
+ element.timezone && /* @__PURE__ */ jsx("div", {
7937
+ className: "sbk-timepicker__hint",
7938
+ children: `Time zone: ${timeZoneLabel(element.timezone)}`
7939
+ }),
6573
7940
  dialog
6574
7941
  ]
6575
7942
  });
6576
7943
  }
6577
7944
  //#endregion
6578
7945
  //#region src/elements/DateTimePicker.tsx
6579
- /** Slack's Builder splits a datetimepicker's closed control into a date box ("January 1st,
6580
- * 2026") and a separate time box ("11:00 AM"), rather than one combined string. */
6581
- function formatDate(ts, timeZone) {
6582
- const d = /* @__PURE__ */ new Date(ts * 1e3);
6583
- const parts = new Intl.DateTimeFormat("en-US", {
7946
+ /** The time list's first row while a value is chosen. */
7947
+ const CLEAR = "clear";
7948
+ /** Slack spells a datetimepicker's day out ("January 1st, 2026"), and calls today "Today". */
7949
+ function formatDay(date, today) {
7950
+ if (date === today) return "Today";
7951
+ const d = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
7952
+ return `${d.toLocaleString("en-US", {
6584
7953
  month: "long",
6585
- day: "numeric",
6586
- year: "numeric",
6587
- timeZone
6588
- }).formatToParts(d);
6589
- const month = parts.find((p) => p.type === "month")?.value ?? "";
6590
- const day = Number(parts.find((p) => p.type === "day")?.value ?? "1");
6591
- const year = parts.find((p) => p.type === "year")?.value ?? "";
6592
- return `${month} ${ordinal$1(day)}, ${year}`;
6593
- }
6594
- function formatTime$1(ts, timeZone) {
6595
- return (/* @__PURE__ */ new Date(ts * 1e3)).toLocaleString("en-US", {
6596
- hour: "numeric",
6597
- minute: "2-digit",
6598
- timeZone
6599
- });
7954
+ timeZone: "UTC"
7955
+ })} ${ordinal$1(d.getUTCDate())}, ${d.getUTCFullYear()}`;
6600
7956
  }
7957
+ /**
7958
+ * Slack's datetimepicker: a date field and a time field side by side, each changing the value on
7959
+ * its own. The date field opens the datepicker's calendar and the time field the timepicker's
7960
+ * hourly list (230px wide here). Picking either part fills in the other if it's empty (the time
7961
+ * now, or today) and sends the action straight away; "Clear selection" in either empties both.
7962
+ */
6601
7963
  function DateTimePicker({ element, blockId }) {
6602
7964
  const { setValue, dispatch, timeZone } = useBlockKit();
6603
7965
  const { ask, dialog } = useConfirm(element.confirm);
6604
- const [value, setValueState] = useState(element.initial_date_time);
6605
- const [open, setOpen] = useState(false);
6606
7966
  const zone = timeZone ?? "UTC";
6607
- const initial = element.initial_date_time !== void 0 ? wallClock(element.initial_date_time, zone) : void 0;
6608
- const [draftDate, setDraftDate] = useState(initial?.date);
6609
- const [draftTime, setDraftTime] = useState(initial?.time ?? "12:00");
6610
- const rootRef = useRef(null);
6611
- const focusRef = useFocusOnLoad(element);
7967
+ const [value, setValueState] = useState(() => element.initial_date_time !== void 0 ? wallClock(element.initial_date_time, zone) : void 0);
7968
+ const [calendarOpen, setCalendarOpen] = useState(false);
7969
+ const dateRef = useFocusOnLoad(element);
7970
+ const popupRef = useRef(null);
7971
+ const timeFieldRef = useRef(null);
7972
+ const listRef = useRef(null);
7973
+ const focusCalendar = useRef(false);
6612
7974
  const actionId = element.action_id ?? "";
7975
+ const now = () => wallClock(Math.floor(Date.now() / 1e3), zone);
6613
7976
  useEffect(() => {
6614
- if (value !== void 0) setValue(blockId, actionId, {
7977
+ if (element.initial_date_time !== void 0) setValue(blockId, actionId, {
6615
7978
  type: "datetimepicker",
6616
- selected_date_time: value
7979
+ selected_date_time: element.initial_date_time
6617
7980
  });
6618
7981
  }, []);
6619
- async function apply() {
6620
- if (!draftDate) return;
7982
+ async function change(next) {
6621
7983
  if (!await ask()) return;
6622
- const ts = fromWallClock(draftDate, draftTime, zone);
6623
- setValueState(ts);
6624
- setOpen(false);
7984
+ setValueState(next);
7985
+ const selected = next ? fromWallClock(next.date, next.time, zone) : null;
6625
7986
  setValue(blockId, actionId, {
6626
7987
  type: "datetimepicker",
6627
- selected_date_time: ts
7988
+ selected_date_time: selected
6628
7989
  });
6629
7990
  dispatch({
6630
7991
  type: "datetimepicker",
6631
7992
  action_id: actionId,
6632
7993
  block_id: blockId,
6633
- selected_date_time: ts
7994
+ selected_date_time: selected,
7995
+ ...element.initial_date_time !== void 0 ? { initial_date_time: element.initial_date_time } : {}
7996
+ });
7997
+ }
7998
+ function closeCalendar() {
7999
+ setCalendarOpen(false);
8000
+ if (document.activeElement !== dateRef.current) dateRef.current?.focus();
8001
+ }
8002
+ function pickDate(date) {
8003
+ closeCalendar();
8004
+ change({
8005
+ date,
8006
+ time: value?.time ?? now().time
8007
+ });
8008
+ }
8009
+ function pickTime(time) {
8010
+ combo.setOpen(false);
8011
+ change({
8012
+ date: value?.date ?? now().date,
8013
+ time
6634
8014
  });
6635
8015
  }
8016
+ function clear() {
8017
+ setCalendarOpen(false);
8018
+ combo.setOpen(false);
8019
+ change(void 0);
8020
+ }
8021
+ const [query, setQuery] = useState("");
8022
+ const rows = [...value && !query.trim() ? [CLEAR] : [], ...TIMES];
8023
+ const combo = useCombobox({
8024
+ query,
8025
+ onQueryChange: setQuery,
8026
+ count: rows.length,
8027
+ initialIndex: value ? rows.indexOf(value.time) : -1,
8028
+ onChoose: (i) => {
8029
+ const row = rows[i];
8030
+ if (row === CLEAR) clear();
8031
+ else if (row) pickTime(row);
8032
+ },
8033
+ onSubmitQuery: (typed) => {
8034
+ const time = parseTime(typed);
8035
+ if (time) pickTime(time);
8036
+ },
8037
+ listRef,
8038
+ highlightOnType: false
8039
+ });
8040
+ const timeDisplay = value ? formatTime(value.time) : void 0;
8041
+ const input = combo.inputProps(timeDisplay, "Time");
8042
+ const overlay = timeDisplay !== void 0 && !combo.open;
6636
8043
  return /* @__PURE__ */ jsxs("div", {
6637
8044
  className: "sbk-datetimepicker",
6638
- ref: rootRef,
6639
8045
  children: [
6640
8046
  /* @__PURE__ */ jsxs("div", {
6641
8047
  className: "sbk-datetimepicker__row",
6642
8048
  children: [/* @__PURE__ */ jsx("div", {
6643
8049
  className: "sbk-datetimepicker__column",
6644
8050
  children: /* @__PURE__ */ jsxs("button", {
6645
- ref: focusRef,
8051
+ ref: dateRef,
6646
8052
  type: "button",
6647
8053
  className: "sbk-datetimepicker__control sbk-datetimepicker__control--date",
6648
- onClick: () => setOpen((o) => !o),
8054
+ onClick: () => {
8055
+ if (calendarOpen) {
8056
+ focusCalendarDay(popupRef.current);
8057
+ return;
8058
+ }
8059
+ focusCalendar.current = true;
8060
+ setCalendarOpen(true);
8061
+ },
6649
8062
  children: [
6650
8063
  /* @__PURE__ */ jsx(CalendarIcon, { className: "sbk-datetimepicker__icon" }),
6651
8064
  /* @__PURE__ */ jsx("span", {
6652
- className: value !== void 0 ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
6653
- children: value !== void 0 ? formatDate(value, zone) : "Select a date"
8065
+ className: value ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
8066
+ children: value ? formatDay(value.date, now().date) : "Select a date"
6654
8067
  }),
6655
8068
  /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datetimepicker__chevron" })
6656
8069
  ]
6657
8070
  })
6658
8071
  }), /* @__PURE__ */ jsxs("div", {
6659
8072
  className: "sbk-datetimepicker__column",
6660
- children: [/* @__PURE__ */ jsxs("button", {
6661
- type: "button",
8073
+ children: [/* @__PURE__ */ jsxs("label", {
8074
+ ref: timeFieldRef,
6662
8075
  className: "sbk-datetimepicker__control sbk-datetimepicker__control--time",
6663
- onClick: () => setOpen((o) => !o),
6664
8076
  children: [
6665
8077
  /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-datetimepicker__icon" }),
6666
- /* @__PURE__ */ jsx("span", {
6667
- className: value !== void 0 ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
6668
- children: value !== void 0 ? formatTime$1(value, zone) : "Select a time"
8078
+ /* @__PURE__ */ jsx("input", {
8079
+ ...input,
8080
+ className: `sbk-datetimepicker__input${overlay ? " sbk-datetimepicker__input--behind" : ""}`
6669
8081
  }),
6670
- /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datetimepicker__chevron" })
8082
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datetimepicker__chevron" }),
8083
+ overlay && /* @__PURE__ */ jsx("span", {
8084
+ className: "sbk-datetimepicker__content",
8085
+ "aria-hidden": "true",
8086
+ children: /* @__PURE__ */ jsx("span", {
8087
+ className: "sbk-datetimepicker__content-text",
8088
+ children: timeDisplay
8089
+ })
8090
+ })
6671
8091
  ]
6672
- }), /* @__PURE__ */ jsxs("p", {
8092
+ }), /* @__PURE__ */ jsx("p", {
6673
8093
  className: "sbk-datetimepicker__timezone",
6674
- children: ["Time zone: ", timeZoneLabel(zone)]
8094
+ children: `Time zone: ${timeZoneLabel(zone)}`
6675
8095
  })]
6676
8096
  })]
6677
8097
  }),
6678
- open && /* @__PURE__ */ jsx(Popover, {
6679
- anchorRef: rootRef,
6680
- onDismiss: () => setOpen(false),
6681
- children: /* @__PURE__ */ jsxs("div", {
6682
- className: "sbk-datetimepicker__popup",
6683
- children: [/* @__PURE__ */ jsx(Calendar, {
6684
- value: draftDate,
6685
- onSelect: setDraftDate
6686
- }), /* @__PURE__ */ jsxs("div", {
6687
- className: "sbk-datetimepicker__time-row",
6688
- children: [/* @__PURE__ */ jsx("input", {
6689
- type: "time",
6690
- className: "sbk-datetimepicker__time-input",
6691
- value: draftTime,
6692
- onChange: (e) => setDraftTime(e.target.value)
6693
- }), /* @__PURE__ */ jsx("button", {
6694
- type: "button",
6695
- className: "sbk-datetimepicker__apply",
6696
- onClick: apply,
6697
- disabled: !draftDate,
6698
- children: "Apply"
6699
- })]
6700
- })]
8098
+ calendarOpen && /* @__PURE__ */ jsx(CalendarPopover, {
8099
+ anchorRef: dateRef,
8100
+ popupRef,
8101
+ value: value?.date,
8102
+ onSelect: pickDate,
8103
+ onClear: value ? clear : void 0,
8104
+ onDismiss: () => setCalendarOpen(false),
8105
+ onEscape: closeCalendar,
8106
+ focusOnOpen: () => {
8107
+ const focus = focusCalendar.current;
8108
+ focusCalendar.current = false;
8109
+ return focus;
8110
+ },
8111
+ offsetX: -4
8112
+ }),
8113
+ combo.open && /* @__PURE__ */ jsx(Popover, {
8114
+ anchorRef: timeFieldRef,
8115
+ onDismiss: () => combo.setOpen(false),
8116
+ offsetX: -12,
8117
+ gap: -4,
8118
+ children: /* @__PURE__ */ jsx("div", {
8119
+ className: "sbk-timepicker__menu sbk-timepicker__menu--wide",
8120
+ role: "listbox",
8121
+ id: combo.listId,
8122
+ ref: listRef,
8123
+ children: rows.map((row, i) => /* @__PURE__ */ jsx("div", {
8124
+ role: "option",
8125
+ "aria-selected": row === value?.time,
8126
+ className: `sbk-timepicker__option${row === CLEAR ? " sbk-timepicker__option--clear" : ""}${row === value?.time ? " sbk-timepicker__option--selected" : ""}`,
8127
+ onMouseDown: (e) => e.preventDefault(),
8128
+ onClick: () => row === CLEAR ? clear() : pickTime(row),
8129
+ ...combo.optionProps(i),
8130
+ children: row === CLEAR ? "Clear selection" : formatTime(row)
8131
+ }, row))
6701
8132
  })
6702
8133
  }),
6703
8134
  dialog
@@ -6706,20 +8137,23 @@ function DateTimePicker({ element, blockId }) {
6706
8137
  }
6707
8138
  //#endregion
6708
8139
  //#region src/elements/FeedbackButtons.tsx
6709
- /** Positive/negative feedback buttons, as seen on AI agent messages. Only one of the two can be
6710
- * picked, matching Slack's own toggle behavior. */
8140
+ /**
8141
+ * Positive/negative feedback buttons, as seen on AI agent messages. As in Slack, the pair is a
8142
+ * radio group labelled "Rating": the picked button stays checked and shows a filled thumb, picking
8143
+ * it again sends the action again, and the arrow keys move and pick. Slack keeps feedback out of
8144
+ * `state.values`.
8145
+ */
6711
8146
  function FeedbackButtons({ element, blockId }) {
6712
- const { setValue, dispatch } = useBlockKit();
8147
+ const { dispatch } = useBlockKit();
6713
8148
  const [picked, setPicked] = useState();
8149
+ const refs = {
8150
+ positive: useRef(null),
8151
+ negative: useRef(null)
8152
+ };
6714
8153
  const actionId = element.action_id ?? "";
6715
8154
  function choose(kind) {
6716
- const next = picked === kind ? void 0 : kind;
6717
- setPicked(next);
8155
+ setPicked(kind);
6718
8156
  const button = kind === "positive" ? element.positive_button : element.negative_button;
6719
- setValue(blockId, actionId, next ? {
6720
- type: "feedback_buttons",
6721
- value: button.value
6722
- } : void 0);
6723
8157
  dispatch({
6724
8158
  type: "feedback_buttons",
6725
8159
  action_id: actionId,
@@ -6731,29 +8165,44 @@ function FeedbackButtons({ element, blockId }) {
6731
8165
  }
6732
8166
  });
6733
8167
  }
6734
- return /* @__PURE__ */ jsxs("div", {
6735
- className: "sbk-feedback-buttons",
6736
- children: [/* @__PURE__ */ jsx(Tooltip, {
6737
- label: element.positive_button.text.text,
6738
- children: /* @__PURE__ */ jsx("button", {
6739
- type: "button",
6740
- className: `sbk-icon-button${picked === "positive" ? " sbk-icon-button--active" : ""}`,
6741
- onClick: () => choose("positive"),
6742
- "aria-label": element.positive_button.accessibility_label ?? element.positive_button.text.text,
6743
- "aria-pressed": picked === "positive",
6744
- children: /* @__PURE__ */ jsx(ThumbsUpIcon, {})
6745
- })
6746
- }), /* @__PURE__ */ jsx(Tooltip, {
6747
- label: element.negative_button.text.text,
8168
+ function onKeyDown(e) {
8169
+ if (![
8170
+ "ArrowLeft",
8171
+ "ArrowRight",
8172
+ "ArrowUp",
8173
+ "ArrowDown"
8174
+ ].includes(e.key)) return;
8175
+ e.preventDefault();
8176
+ const next = picked === "positive" || !picked && e.currentTarget === refs.positive.current ? "negative" : "positive";
8177
+ choose(next);
8178
+ refs[next].current?.focus();
8179
+ }
8180
+ const tabStop = picked ?? "positive";
8181
+ function button(kind) {
8182
+ const spec = kind === "positive" ? element.positive_button : element.negative_button;
8183
+ const checked = picked === kind;
8184
+ const Icon = kind === "positive" ? checked ? ThumbsUpFilledIcon : ThumbsUpIcon : checked ? ThumbsDownFilledIcon : ThumbsDownIcon;
8185
+ return /* @__PURE__ */ jsx(Tooltip, {
8186
+ label: spec.text.text,
6748
8187
  children: /* @__PURE__ */ jsx("button", {
8188
+ ref: refs[kind],
6749
8189
  type: "button",
6750
- className: `sbk-icon-button${picked === "negative" ? " sbk-icon-button--active" : ""}`,
6751
- onClick: () => choose("negative"),
6752
- "aria-label": element.negative_button.accessibility_label ?? element.negative_button.text.text,
6753
- "aria-pressed": picked === "negative",
6754
- children: /* @__PURE__ */ jsx(ThumbsDownIcon, {})
8190
+ role: "radio",
8191
+ className: "sbk-icon-button",
8192
+ onClick: () => choose(kind),
8193
+ onKeyDown,
8194
+ "aria-label": spec.accessibility_label ?? spec.text.text,
8195
+ "aria-checked": checked,
8196
+ tabIndex: tabStop === kind ? 0 : -1,
8197
+ children: /* @__PURE__ */ jsx(Icon, {})
6755
8198
  })
6756
- })]
8199
+ });
8200
+ }
8201
+ return /* @__PURE__ */ jsxs("div", {
8202
+ className: "sbk-feedback-buttons",
8203
+ role: "radiogroup",
8204
+ "aria-label": "Rating",
8205
+ children: [button("positive"), button("negative")]
6757
8206
  });
6758
8207
  }
6759
8208
  //#endregion
@@ -6838,7 +8287,9 @@ function IconButton({ element, blockId }) {
6838
8287
  type: "icon_button",
6839
8288
  action_id: element.action_id ?? "",
6840
8289
  block_id: blockId,
6841
- ...element.value !== void 0 ? { value: element.value } : {}
8290
+ ...element.value !== void 0 ? { value: element.value } : {},
8291
+ icon: element.icon,
8292
+ text: element.text
6842
8293
  });
6843
8294
  }
6844
8295
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tooltip, {
@@ -6854,85 +8305,18 @@ function IconButton({ element, blockId }) {
6854
8305
  }
6855
8306
  //#endregion
6856
8307
  //#region src/elements/ImageElement.tsx
8308
+ /** An image element; a `slack_file` one shows through `resolvers.slackFile`, like the image block. */
6857
8309
  function ImageElement({ element }) {
8310
+ const { resolvers } = useBlockKit();
8311
+ const slackFile = element.slack_file;
8312
+ const src = "image_url" in element ? element.image_url : slackFile ? resolvers.slackFile?.(slackFile)?.url : void 0;
6858
8313
  return /* @__PURE__ */ jsx("img", {
6859
8314
  className: "sbk-image-element",
6860
- src: "image_url" in element ? element.image_url : void 0,
8315
+ src,
6861
8316
  alt: element.alt_text
6862
8317
  });
6863
8318
  }
6864
8319
  //#endregion
6865
- //#region src/elements/useMenuNavigation.ts
6866
- /**
6867
- * Slack's menus and option lists keep one "active" row that follows both the pointer and the
6868
- * arrow keys (`c-menu_item--highlighted` / `c-select_options_list__option--active`), so hover and
6869
- * keyboard highlight share one style. Returns the active index plus handlers for the list.
6870
- */
6871
- function useMenuNavigation({ open, count, initialIndex = -1, onChoose, onClose, onOpen, listRef }) {
6872
- const [active, setActive] = useState(-1);
6873
- useEffect(() => {
6874
- setActive(open ? initialIndex : -1);
6875
- }, [open]);
6876
- useEffect(() => {
6877
- if (active >= count) setActive(count - 1);
6878
- }, [active, count]);
6879
- useEffect(() => {
6880
- (listRef?.current?.querySelector("[data-active]"))?.scrollIntoView?.({ block: "nearest" });
6881
- }, [active, listRef]);
6882
- function onKeyDown(e) {
6883
- if (!open) {
6884
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
6885
- e.preventDefault();
6886
- onOpen?.();
6887
- }
6888
- return;
6889
- }
6890
- switch (e.key) {
6891
- case "ArrowDown":
6892
- e.preventDefault();
6893
- setActive((i) => count === 0 ? -1 : (i + 1) % count);
6894
- break;
6895
- case "ArrowUp":
6896
- e.preventDefault();
6897
- setActive((i) => count === 0 ? -1 : i <= 0 ? count - 1 : i - 1);
6898
- break;
6899
- case "Home":
6900
- e.preventDefault();
6901
- setActive(count > 0 ? 0 : -1);
6902
- break;
6903
- case "End":
6904
- e.preventDefault();
6905
- setActive(count - 1);
6906
- break;
6907
- case "Enter":
6908
- if (active >= 0 && active < count) {
6909
- e.preventDefault();
6910
- onChoose(active);
6911
- }
6912
- break;
6913
- case "Escape":
6914
- e.preventDefault();
6915
- onClose();
6916
- }
6917
- }
6918
- /** Props for the row at `index`: pointer movement moves the highlight, like Slack's lists. */
6919
- function itemProps(index) {
6920
- return {
6921
- onMouseEnter: () => setActive(index),
6922
- onMouseMove: () => {
6923
- if (active !== index) setActive(index);
6924
- },
6925
- "data-active": active === index || void 0
6926
- };
6927
- }
6928
- return {
6929
- active,
6930
- setActive,
6931
- onKeyDown,
6932
- itemProps
6933
- };
6934
- }
6935
- //#endregion
6936
8320
  //#region src/elements/Overflow.tsx
6937
8321
  function Overflow({ element, blockId }) {
6938
8322
  const { dispatch } = useBlockKit();
@@ -6942,14 +8326,19 @@ function Overflow({ element, blockId }) {
6942
8326
  const triggerRef = useRef(null);
6943
8327
  const listRef = useRef(null);
6944
8328
  const actionId = element.action_id ?? "";
8329
+ const idPrefix = useId();
6945
8330
  async function choose(option) {
6946
8331
  if (!await ask()) return;
6947
8332
  setOpen(false);
8333
+ triggerRef.current?.focus();
6948
8334
  dispatch({
6949
8335
  type: "overflow",
6950
8336
  action_id: actionId,
6951
8337
  block_id: blockId,
6952
- selected_option: option
8338
+ selected_option: {
8339
+ text: option.text,
8340
+ ...option.value !== void 0 ? { value: option.value } : {}
8341
+ }
6953
8342
  });
6954
8343
  if (option.url) window.open(option.url, "_blank", "noopener,noreferrer");
6955
8344
  }
@@ -6985,16 +8374,23 @@ function Overflow({ element, blockId }) {
6985
8374
  open && /* @__PURE__ */ jsx(Popover, {
6986
8375
  anchorRef: rootRef,
6987
8376
  onDismiss: () => setOpen(false),
8377
+ onPlaced: () => listRef.current?.focus(),
6988
8378
  children: /* @__PURE__ */ jsx("div", {
6989
8379
  className: "sbk-overflow__menu",
6990
8380
  role: "menu",
6991
8381
  ref: listRef,
8382
+ tabIndex: -1,
8383
+ "aria-activedescendant": nav.active >= 0 ? `${idPrefix}-${nav.active}` : void 0,
6992
8384
  children: element.options.map((option, i) => /* @__PURE__ */ jsx("div", {
8385
+ id: `${idPrefix}-${i}`,
6993
8386
  role: "menuitem",
6994
8387
  className: "sbk-overflow__option",
6995
8388
  onClick: () => choose(option),
6996
8389
  ...nav.itemProps(i),
6997
- children: option.text.text
8390
+ children: /* @__PURE__ */ jsx("span", {
8391
+ className: "sbk-overflow__label",
8392
+ children: option.text.text
8393
+ })
6998
8394
  }, option.value ?? i))
6999
8395
  })
7000
8396
  }),
@@ -7028,7 +8424,8 @@ function RadioButtons({ element, blockId }) {
7028
8424
  type: "radio_buttons",
7029
8425
  action_id: actionId,
7030
8426
  block_id: blockId,
7031
- selected_option: option
8427
+ selected_option: option,
8428
+ ...element.initial_option !== void 0 ? { initial_option: element.initial_option } : {}
7032
8429
  });
7033
8430
  };
7034
8431
  return /* @__PURE__ */ jsxs("div", {
@@ -7229,20 +8626,37 @@ function RichTextInput({ element, blockId }) {
7229
8626
  rich_text_value: element.initial_value
7230
8627
  });
7231
8628
  }, []);
8629
+ const config = element;
8630
+ const dispatchEnabled = config.__dispatchAction === true;
8631
+ const triggers = config.dispatch_action_config?.trigger_actions_on ?? ["on_enter_pressed"];
8632
+ const typing = useRef(void 0);
8633
+ useEffect(() => () => clearTimeout(typing.current), []);
8634
+ function fire() {
8635
+ clearTimeout(typing.current);
8636
+ dispatch({
8637
+ type: "rich_text_input",
8638
+ action_id: actionId,
8639
+ block_id: blockId,
8640
+ rich_text_value: toRichText(ref.current?.innerText ?? "")
8641
+ });
8642
+ }
7232
8643
  function onInput() {
7233
8644
  const text = ref.current?.innerText ?? "";
7234
8645
  setEmpty(text.trim() === "");
7235
- const richText = toRichText(text);
7236
8646
  setValue(blockId, actionId, {
7237
8647
  type: "rich_text_input",
7238
- rich_text_value: richText
7239
- });
7240
- dispatch({
7241
- type: "rich_text_input",
7242
- action_id: actionId,
7243
- block_id: blockId,
7244
- rich_text_value: richText
8648
+ rich_text_value: toRichText(text)
7245
8649
  });
8650
+ if (dispatchEnabled && triggers.includes("on_character_entered")) {
8651
+ clearTimeout(typing.current);
8652
+ typing.current = setTimeout(fire, 300);
8653
+ }
8654
+ }
8655
+ function onKeyDown(e) {
8656
+ if (e.key !== "Enter" || e.shiftKey || !dispatchEnabled) return;
8657
+ if (!triggers.includes("on_enter_pressed")) return;
8658
+ e.preventDefault();
8659
+ fire();
7246
8660
  }
7247
8661
  const placeholder = element.placeholder?.text;
7248
8662
  return /* @__PURE__ */ jsxs("div", {
@@ -7261,6 +8675,7 @@ function RichTextInput({ element, blockId }) {
7261
8675
  ...invalid,
7262
8676
  "aria-label": placeholder ?? actionId,
7263
8677
  onInput,
8678
+ onKeyDown,
7264
8679
  style: {
7265
8680
  minHeight: element.min_lines ? `${element.min_lines * 22}px` : void 0,
7266
8681
  maxHeight: element.max_lines ? `${element.max_lines * 22}px` : void 0
@@ -7280,6 +8695,160 @@ function RichTextInput({ element, blockId }) {
7280
8695
  });
7281
8696
  }
7282
8697
  //#endregion
8698
+ //#region src/elements/accessoryContext.ts
8699
+ /**
8700
+ * True for an element rendered as a section block's accessory. Slack draws some elements
8701
+ * differently there: a multi-select becomes a small button that opens a "Select options" dialog.
8702
+ */
8703
+ const SectionAccessoryContext = createContext(false);
8704
+ //#endregion
8705
+ //#region src/elements/SelectDialog.tsx
8706
+ const optionId = (option) => option.value ?? option.text.text;
8707
+ /**
8708
+ * Slack's selection dialog, which a multi-select in a section accessory opens instead of an inline
8709
+ * menu, titled with the select's placeholder. Picks are a draft: the option list stays open while
8710
+ * choosing, and nothing is sent until Confirm. Cancel, the close button and Escape discard the
8711
+ * draft. A users, conversations or channels select has no options to list; `addTyped` turns what's
8712
+ * typed into one on Enter.
8713
+ */
8714
+ function SelectDialog({ options, initial, placeholder, addTyped, onConfirm, onCancel }) {
8715
+ const { theme } = useBlockKit();
8716
+ const titleId = useId();
8717
+ const [draft, setDraft] = useState(initial);
8718
+ const [query, setQuery] = useState("");
8719
+ const dialogRef = useRef(null);
8720
+ const fieldRef = useRef(null);
8721
+ const listRef = useRef(null);
8722
+ const rows = options.filter((o) => o.text.text.toLowerCase().includes(query.toLowerCase()));
8723
+ const chosen = (option) => draft.some((d) => optionId(d) === optionId(option));
8724
+ const toggle = (option) => setDraft((current) => chosen(option) ? current.filter((d) => optionId(d) !== optionId(option)) : [...current, option]);
8725
+ const combo = useCombobox({
8726
+ query,
8727
+ onQueryChange: setQuery,
8728
+ count: rows.length,
8729
+ initialIndex: 0,
8730
+ onChoose: (i) => {
8731
+ const option = rows[i];
8732
+ if (option) toggle(option);
8733
+ },
8734
+ onSubmitQuery: addTyped ? (typed) => {
8735
+ const option = addTyped(typed);
8736
+ if (!chosen(option)) setDraft((current) => [...current, option]);
8737
+ setQuery("");
8738
+ } : void 0,
8739
+ listRef
8740
+ });
8741
+ useEffect(() => {
8742
+ dialogRef.current?.focus();
8743
+ }, []);
8744
+ if (typeof document === "undefined") return null;
8745
+ return createPortal(/* @__PURE__ */ jsx("div", {
8746
+ className: "sbk-root sbk-select-dialog__overlay",
8747
+ "data-theme": theme,
8748
+ children: /* @__PURE__ */ jsxs("div", {
8749
+ ref: dialogRef,
8750
+ className: "sbk-select-dialog",
8751
+ tabIndex: -1,
8752
+ role: "dialog",
8753
+ "aria-modal": "true",
8754
+ "aria-labelledby": titleId,
8755
+ onKeyDown: (e) => {
8756
+ if (e.key !== "Escape") return;
8757
+ e.preventDefault();
8758
+ e.stopPropagation();
8759
+ onCancel();
8760
+ },
8761
+ children: [
8762
+ /* @__PURE__ */ jsxs("div", {
8763
+ className: "sbk-select-dialog__header",
8764
+ children: [/* @__PURE__ */ jsx("h2", {
8765
+ className: "sbk-select-dialog__title",
8766
+ id: titleId,
8767
+ children: placeholder
8768
+ }), /* @__PURE__ */ jsx("button", {
8769
+ type: "button",
8770
+ className: "sbk-select-dialog__close",
8771
+ "aria-label": "Close",
8772
+ onClick: onCancel,
8773
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
8774
+ })]
8775
+ }),
8776
+ /* @__PURE__ */ jsxs("div", {
8777
+ className: "sbk-select-dialog__body",
8778
+ children: [/* @__PURE__ */ jsxs("div", {
8779
+ className: "sbk-select-dialog__field",
8780
+ ref: fieldRef,
8781
+ children: [
8782
+ draft.map((option) => /* @__PURE__ */ jsxs("span", {
8783
+ className: "sbk-select__chip",
8784
+ children: [/* @__PURE__ */ jsx("span", {
8785
+ className: "sbk-select__chip-label",
8786
+ children: /* @__PURE__ */ jsx("span", {
8787
+ className: "sbk-select__chip-text",
8788
+ children: /* @__PURE__ */ jsx("span", { children: option.text.text })
8789
+ })
8790
+ }), /* @__PURE__ */ jsx("button", {
8791
+ type: "button",
8792
+ className: "sbk-select__chip-remove",
8793
+ "aria-label": `Remove ${option.text.text}`,
8794
+ onClick: () => toggle(option),
8795
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
8796
+ })]
8797
+ }, optionId(option))),
8798
+ draft.length === 0 && query === "" && /* @__PURE__ */ jsx("span", {
8799
+ className: "sbk-select-dialog__placeholder",
8800
+ "aria-hidden": "true",
8801
+ children: placeholder
8802
+ }),
8803
+ /* @__PURE__ */ jsx("input", {
8804
+ ...combo.inputProps(void 0, placeholder),
8805
+ placeholder: "",
8806
+ className: "sbk-select-dialog__input"
8807
+ })
8808
+ ]
8809
+ }), combo.open && /* @__PURE__ */ jsx(Popover, {
8810
+ anchorRef: fieldRef,
8811
+ onDismiss: () => combo.setOpen(false),
8812
+ children: /* @__PURE__ */ jsx("div", {
8813
+ className: "sbk-select__menu sbk-select-dialog__menu",
8814
+ role: "listbox",
8815
+ "aria-multiselectable": "true",
8816
+ id: combo.listId,
8817
+ ref: listRef,
8818
+ children: rows.map((option, i) => /* @__PURE__ */ jsx("div", {
8819
+ role: "option",
8820
+ "aria-selected": chosen(option),
8821
+ className: `sbk-select__option${chosen(option) ? " sbk-select__option--selected" : ""}`,
8822
+ onMouseDown: (e) => e.preventDefault(),
8823
+ onClick: () => toggle(option),
8824
+ ...combo.optionProps(i),
8825
+ children: /* @__PURE__ */ jsx("span", {
8826
+ className: "sbk-select__option-text",
8827
+ children: /* @__PURE__ */ jsx(Text, { text: option.text })
8828
+ })
8829
+ }, optionId(option)))
8830
+ })
8831
+ })]
8832
+ }),
8833
+ /* @__PURE__ */ jsxs("div", {
8834
+ className: "sbk-select-dialog__footer",
8835
+ children: [/* @__PURE__ */ jsx("button", {
8836
+ type: "button",
8837
+ className: "sbk-modal__button",
8838
+ onClick: onCancel,
8839
+ children: "Cancel"
8840
+ }), /* @__PURE__ */ jsx("button", {
8841
+ type: "button",
8842
+ className: "sbk-modal__button sbk-modal__button--primary",
8843
+ onClick: () => onConfirm(draft),
8844
+ children: "Confirm"
8845
+ })]
8846
+ })
8847
+ ]
8848
+ })
8849
+ }), document.body);
8850
+ }
8851
+ //#endregion
7283
8852
  //#region src/elements/Select.tsx
7284
8853
  function parseType(type) {
7285
8854
  const multi = type.startsWith("multi_");
@@ -7334,6 +8903,31 @@ function responseGroups(response) {
7334
8903
  }));
7335
8904
  return [{ options: response.options }];
7336
8905
  }
8906
+ /**
8907
+ * Whether a conversations select's `filter` lets an entry through, as Slack applies it: `include`
8908
+ * names the kinds listed (`im` for people, `private` and `public` for channels; group DMs aren't in
8909
+ * the directory), and `exclude_bot_users` leaves out bots.
8910
+ */
8911
+ function matchesConversationFilter(entry, filter) {
8912
+ if (!filter) return true;
8913
+ if (entry.type === "user" && entry.bot && filter.exclude_bot_users) return false;
8914
+ const include = filter.include;
8915
+ if (!include || include.length === 0) return true;
8916
+ const kind = entry.type === "user" ? "im" : entry.private ? "private" : "public";
8917
+ return include.includes(kind);
8918
+ }
8919
+ /**
8920
+ * Whether `text` matches what's typed, as Slack's lists filter: case-insensitively, at the start of
8921
+ * the text or of any word in it (after a space, hyphen, bracket or other punctuation), never in the
8922
+ * middle of a word.
8923
+ */
8924
+ function matchesQuery(text, query) {
8925
+ const q = query.toLowerCase();
8926
+ if (!q) return true;
8927
+ const t = text.toLowerCase();
8928
+ for (let i = t.indexOf(q); i !== -1; i = t.indexOf(q, i + 1)) if (i === 0 || !/[\p{L}\p{N}]/u.test(t[i - 1])) return true;
8929
+ return false;
8930
+ }
7337
8931
  /** Slack's default `min_query_length` for an `external_select`. */
7338
8932
  const DEFAULT_MIN_QUERY_LENGTH = 3;
7339
8933
  /** How long typing must pause before an `external_select` asks the app for options. */
@@ -7349,7 +8943,6 @@ function Select({ element, blockId }) {
7349
8943
  const [query, setQuery] = useState("");
7350
8944
  const rootRef = useRef(null);
7351
8945
  const triggerRef = useRef(null);
7352
- useFocusOnLoad(element, triggerRef);
7353
8946
  const invalid = useInvalidProps();
7354
8947
  const listRef = useRef(null);
7355
8948
  const remote = source === "external" && loadOptions !== void 0;
@@ -7388,7 +8981,11 @@ function Select({ element, blockId }) {
7388
8981
  actionId,
7389
8982
  blockId
7390
8983
  ]);
8984
+ const entries = source === "users" || source === "conversations" || source === "channels" ? (resolvers.directory?.(source) ?? []).filter((e) => source !== "conversations" || matchesConversationFilter(e, element.filter)) : [];
8985
+ const entryById = new Map(entries.map((e) => [e.id, e]));
7391
8986
  const resolveLabel = (id) => {
8987
+ const known = entryById.get(id);
8988
+ if (known) return known.name;
7392
8989
  return (source === "users" ? resolvers.user : source === "channels" || source === "conversations" ? resolvers.channel : void 0)?.(id) ?? id;
7393
8990
  };
7394
8991
  /** Slack's own Builder preview can't resolve a fabricated id against the real directory, and
@@ -7437,6 +9034,7 @@ function Select({ element, blockId }) {
7437
9034
  action_id: actionId,
7438
9035
  block_id: blockId,
7439
9036
  ...fields,
9037
+ ...!multi && element.initial_option ? { initial_option: element.initial_option } : {},
7440
9038
  ...element.placeholder ? { placeholder: element.placeholder } : {}
7441
9039
  });
7442
9040
  }
@@ -7462,54 +9060,269 @@ function Select({ element, blockId }) {
7462
9060
  async function removeItem(id) {
7463
9061
  await commit(items.filter((i) => i.id !== id));
7464
9062
  }
7465
- const filteredOptions = source === "static" ? allOptions(element).filter((o) => o.text.text.toLowerCase().includes(query.toLowerCase())) : [];
7466
- const matches = (o) => o.text.text.toLowerCase().includes(query.toLowerCase());
7467
- const visibleGroups = remote ? remoteGroups ?? [] : source !== "static" ? [] : element.option_groups ? element.option_groups.map((group) => ({
9063
+ const matches = (o) => matchesQuery(o.text.text, query);
9064
+ const filteredOptions = source === "static" ? allOptions(element).filter(matches) : [];
9065
+ const directoryOptions = entries.filter((e) => matchesQuery(e.name, query) || e.type === "user" && e.realName !== void 0 && matchesQuery(e.realName, query)).map((e) => ({
9066
+ text: {
9067
+ type: "plain_text",
9068
+ text: e.name
9069
+ },
9070
+ value: e.id
9071
+ }));
9072
+ const chosen = !multi && entries.length > 0 && !query ? items[0] : void 0;
9073
+ const pinned = chosen && !entryById.has(chosen.id) ? chosen : void 0;
9074
+ if (pinned) directoryOptions.unshift({
9075
+ text: {
9076
+ type: "plain_text",
9077
+ text: pinned.label || resolveLabel(pinned.id)
9078
+ },
9079
+ value: pinned.id
9080
+ });
9081
+ const visibleGroups = remote ? remoteGroups ?? [] : source !== "static" ? directoryOptions.length > 0 || entries.length > 0 ? [{ options: directoryOptions }] : [] : element.option_groups ? element.option_groups.map((group) => ({
7468
9082
  label: group.label.text,
7469
9083
  options: group.options.filter(matches)
7470
- })) : [{ options: filteredOptions }];
9084
+ })).filter((group) => group.options.length > 0) : [{ options: filteredOptions }];
7471
9085
  const flatOptions = visibleGroups.flatMap((g) => g.options);
7472
9086
  const isSelected = (option) => items.some((i) => i.id === (option.value ?? option.text.text));
7473
- const nav = useMenuNavigation({
9087
+ const showSearchOnly = source !== "static";
9088
+ const buttonSelect = source === "static" && !multi;
9089
+ const typeable = !multi && !buttonSelect;
9090
+ const inAccessory = useContext(SectionAccessoryContext);
9091
+ const inInputBlock = useInInputBlock();
9092
+ const dialogMode = inAccessory && multi && source !== "external";
9093
+ const directory = source === "users" || source === "conversations" || source === "channels";
9094
+ /** An item as a dialog option: a directory id becomes an option labelled as it's shown. */
9095
+ const itemToOption = (item) => item.option ?? {
9096
+ text: {
9097
+ type: "plain_text",
9098
+ text: item.label || resolveLabel(item.id)
9099
+ },
9100
+ value: item.id
9101
+ };
9102
+ const [dialogOpen, setDialogOpen] = useState(false);
9103
+ const placeholder = element.placeholder?.text ?? "Select an item";
9104
+ const placeholderRow = buttonSelect && items.length === 0;
9105
+ /** The rows before the options: the button select's "--placeholder--" row. */
9106
+ const lead = placeholderRow ? 1 : 0;
9107
+ const combo = useCombobox({
7474
9108
  open,
7475
- count: flatOptions.length,
7476
- initialIndex: Math.max(0, flatOptions.findIndex(isSelected)),
9109
+ onOpenChange: setOpen,
9110
+ query,
9111
+ onQueryChange: setQuery,
9112
+ count: flatOptions.length + lead,
9113
+ initialIndex: typeable || placeholderRow ? 0 : Math.max(0, flatOptions.findIndex(isSelected)),
9114
+ holdFirstArrow: flatOptions.findIndex(isSelected) < 0 || query.trim() !== "",
7477
9115
  onChoose: (i) => {
7478
- const option = flatOptions[i];
9116
+ if (i < lead) {
9117
+ combo.setOpen(false);
9118
+ return;
9119
+ }
9120
+ const option = flatOptions[i - lead];
7479
9121
  if (option) selectItem(optionToItem(option));
7480
9122
  },
7481
- onClose: () => {
7482
- setOpen(false);
7483
- triggerRef.current?.focus();
9123
+ onSubmitQuery: (typed) => {
9124
+ if (showSearchOnly && !remote) selectItem({
9125
+ id: typed,
9126
+ label: resolveLabel(typed)
9127
+ });
7484
9128
  },
7485
- onOpen: () => setOpen(true),
7486
- listRef
9129
+ listRef,
9130
+ returnFocusRef: typeable ? void 0 : triggerRef,
9131
+ keepQuery: typeable,
9132
+ typedHighlight: !buttonSelect,
9133
+ chosenAsPlaceholder: !typeable
7487
9134
  });
9135
+ useFocusOnLoad(element, typeable ? combo.inputRef : triggerRef);
7488
9136
  const closedLabel = multi ? void 0 : items[0] ? items[0].label || resolveLabel(items[0].id) : void 0;
7489
- const placeholder = element.placeholder?.text ?? "Select an item";
7490
- const showSearchOnly = source !== "static";
9137
+ const belowMinimum = source === "external" && minQueryLength > 0 && query.length < minQueryLength;
9138
+ const emptyMessage = belowMinimum ? `Type a minimum of ${minQueryLength} characters to see options.` : !loading && flatOptions.length === 0 && (!remote || remoteGroups !== void 0) ? "nothing" : void 0;
9139
+ const wideList = (multi || typeable || buttonSelect) && inInputBlock;
9140
+ const menu = open && /* @__PURE__ */ jsx(Popover, {
9141
+ anchorRef: rootRef,
9142
+ onDismiss: () => combo.setOpen(false),
9143
+ offsetX: typeable || buttonSelect || wideList ? -12 : 0,
9144
+ gap: -4,
9145
+ children: /* @__PURE__ */ jsxs("div", {
9146
+ className: `sbk-select__menu${typeable || buttonSelect ? " sbk-select__menu--typeable" : ""}${wideList ? " sbk-select__menu--wide" : ""}`,
9147
+ role: "listbox",
9148
+ id: combo.listId,
9149
+ ref: listRef,
9150
+ children: [
9151
+ !typeable && !buttonSelect && (showSearchOnly && entries.length === 0 || (entries.length > 0 ? entries.length : allOptions(element).length) > 8) && /* @__PURE__ */ jsxs("div", {
9152
+ className: "sbk-select__search",
9153
+ children: [/* @__PURE__ */ jsx(SearchIcon$1, {}), /* @__PURE__ */ jsx("input", {
9154
+ autoFocus: true,
9155
+ className: "sbk-select__search-input",
9156
+ placeholder: source === "static" || source === "external" ? "Search options" : `Search ${source}`,
9157
+ value: query,
9158
+ onChange: (e) => setQuery(e.target.value)
9159
+ })]
9160
+ }),
9161
+ placeholderRow && /* @__PURE__ */ jsx(SelectOption, {
9162
+ option: { text: {
9163
+ type: "plain_text",
9164
+ text: `--${placeholder}--`
9165
+ } },
9166
+ selected: true,
9167
+ check: true,
9168
+ onSelect: () => combo.setOpen(false),
9169
+ navProps: combo.optionProps(0)
9170
+ }),
9171
+ !belowMinimum && (() => {
9172
+ let index = lead;
9173
+ return visibleGroups.map((group, gi) => {
9174
+ const rows = group.options.map((option) => {
9175
+ const i = index++;
9176
+ return /* @__PURE__ */ jsx(SelectOption, {
9177
+ option,
9178
+ entry: option.value !== void 0 ? entryById.get(option.value) : void 0,
9179
+ content: pinned && option.value === pinned.id && isUnresolved(pinned) ? unresolvedValue(pinned.id) : void 0,
9180
+ selected: isSelected(option),
9181
+ check: (multi || typeable || buttonSelect) && isSelected(option),
9182
+ typed: combo.typed && combo.active === i,
9183
+ onSelect: () => selectItem(optionToItem(option)),
9184
+ navProps: combo.optionProps(i)
9185
+ }, option.value ?? i);
9186
+ });
9187
+ return group.label === void 0 ? rows : /* @__PURE__ */ jsxs("div", {
9188
+ className: "sbk-select__group",
9189
+ children: [
9190
+ gi > 0 && /* @__PURE__ */ jsx("div", {
9191
+ className: "sbk-select__divider",
9192
+ role: "separator"
9193
+ }),
9194
+ /* @__PURE__ */ jsx("div", {
9195
+ className: "sbk-select__group-label",
9196
+ children: group.label
9197
+ }),
9198
+ rows
9199
+ ]
9200
+ }, gi);
9201
+ });
9202
+ })(),
9203
+ remote && loading && !belowMinimum && /* @__PURE__ */ jsx("div", {
9204
+ className: "sbk-select__status",
9205
+ children: "Loading…"
9206
+ }),
9207
+ emptyMessage && /* @__PURE__ */ jsx("div", {
9208
+ className: "sbk-select__option sbk-select__option--disabled",
9209
+ role: "option",
9210
+ "aria-disabled": "true",
9211
+ "aria-selected": "false",
9212
+ children: /* @__PURE__ */ jsx("span", {
9213
+ className: "sbk-select__no-results",
9214
+ children: emptyMessage === "nothing" ? "😕 Nothing could be found." : emptyMessage
9215
+ })
9216
+ }),
9217
+ showSearchOnly && items.length > 0 && multi && /* @__PURE__ */ jsx("div", {
9218
+ className: "sbk-select__group-label",
9219
+ children: "Selected"
9220
+ }),
9221
+ showSearchOnly && multi && items.map((item) => /* @__PURE__ */ jsx("div", {
9222
+ className: "sbk-select__option sbk-select__option--selected",
9223
+ children: item.label || resolveLabel(item.id)
9224
+ }, item.id))
9225
+ ]
9226
+ })
9227
+ });
9228
+ if (dialogMode) {
9229
+ const closeDialog = () => {
9230
+ setDialogOpen(false);
9231
+ triggerRef.current?.focus();
9232
+ };
9233
+ return /* @__PURE__ */ jsxs("div", {
9234
+ className: `sbk-select sbk-select--multi${sizeClass}`,
9235
+ ref: rootRef,
9236
+ children: [
9237
+ /* @__PURE__ */ jsx("button", {
9238
+ ref: triggerRef,
9239
+ type: "button",
9240
+ className: "sbk-select__control",
9241
+ onClick: () => setDialogOpen(true),
9242
+ "aria-haspopup": "dialog",
9243
+ "aria-expanded": dialogOpen,
9244
+ ...invalid,
9245
+ children: /* @__PURE__ */ jsx("span", {
9246
+ className: "sbk-select__placeholder",
9247
+ children: items.length > 0 ? `${items.length} selected` : placeholder
9248
+ })
9249
+ }),
9250
+ dialogOpen && /* @__PURE__ */ jsx(SelectDialog, {
9251
+ options: directory ? [] : allOptions(element),
9252
+ initial: items.map(itemToOption),
9253
+ placeholder,
9254
+ addTyped: directory ? (typed) => itemToOption({
9255
+ id: typed,
9256
+ label: resolveLabel(typed)
9257
+ }) : void 0,
9258
+ onCancel: closeDialog,
9259
+ onConfirm: async (selected) => {
9260
+ closeDialog();
9261
+ if (!await ask()) return;
9262
+ await commit(selected.map((o) => directory ? {
9263
+ id: o.value ?? o.text.text,
9264
+ label: ""
9265
+ } : optionToItem(o)));
9266
+ }
9267
+ }),
9268
+ dialog
9269
+ ]
9270
+ });
9271
+ }
9272
+ if (typeable) {
9273
+ const unresolved = items[0] !== void 0 && isUnresolved(items[0]);
9274
+ const display = unresolved ? void 0 : closedLabel;
9275
+ const overlay = (display !== void 0 || unresolved) && !open && !query;
9276
+ return /* @__PURE__ */ jsxs("div", {
9277
+ className: `sbk-select sbk-select--typeable${sizeClass}`,
9278
+ ref: rootRef,
9279
+ children: [
9280
+ /* @__PURE__ */ jsxs("label", {
9281
+ className: "sbk-select__control",
9282
+ children: [
9283
+ /* @__PURE__ */ jsx("input", {
9284
+ ...combo.inputProps(display, placeholder),
9285
+ ...invalid,
9286
+ className: `sbk-select__input${overlay ? " sbk-select__input--behind" : ""}`
9287
+ }),
9288
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-select__chevron" }),
9289
+ overlay && /* @__PURE__ */ jsx("span", {
9290
+ className: "sbk-select__content",
9291
+ "aria-hidden": "true",
9292
+ children: unresolved && items[0] ? unresolvedValue(items[0].id) : /* @__PURE__ */ jsx("span", {
9293
+ className: "sbk-select__content-text",
9294
+ children: display
9295
+ })
9296
+ })
9297
+ ]
9298
+ }),
9299
+ menu,
9300
+ dialog
9301
+ ]
9302
+ });
9303
+ }
7491
9304
  return /* @__PURE__ */ jsxs("div", {
7492
- className: `sbk-select${multi ? " sbk-select--multi" : ""}${multi && items.length > 0 ? " sbk-select--chips" : ""}${sizeClass}`,
9305
+ className: `sbk-select${buttonSelect ? " sbk-select--button" : ""}${multi ? " sbk-select--multi" : ""}${multi && items.length > 0 ? " sbk-select--chips" : ""}${sizeClass}`,
7493
9306
  ref: rootRef,
7494
- onKeyDown: nav.onKeyDown,
9307
+ onKeyDown: combo.onKeyDown,
7495
9308
  children: [
7496
- multi && element.max_selected_items && /* @__PURE__ */ jsxs("p", {
9309
+ multi && element.max_selected_items && /* @__PURE__ */ jsx("p", {
7497
9310
  className: "sbk-select__max-info",
7498
- children: [
7499
- "You can select up to ",
7500
- element.max_selected_items,
7501
- " ",
7502
- element.max_selected_items === 1 ? "item" : "items",
7503
- "."
7504
- ]
9311
+ children: `You can select up to ${element.max_selected_items} ${element.max_selected_items === 1 ? "item" : "items"}.`
7505
9312
  }),
7506
9313
  /* @__PURE__ */ jsxs("button", {
7507
9314
  ref: triggerRef,
7508
9315
  type: "button",
7509
9316
  className: "sbk-select__control",
7510
- onClick: () => setOpen((o) => !o),
9317
+ onClick: (e) => {
9318
+ if (buttonSelect) e.currentTarget.focus();
9319
+ setOpen((o) => !o);
9320
+ },
9321
+ role: buttonSelect ? "combobox" : void 0,
7511
9322
  "aria-haspopup": "listbox",
7512
9323
  "aria-expanded": open,
9324
+ "aria-controls": buttonSelect && open ? combo.listId : void 0,
9325
+ "aria-activedescendant": buttonSelect && open && combo.active >= 0 ? combo.optionId(combo.active) : void 0,
7513
9326
  ...invalid,
7514
9327
  children: [multi ? items.length > 0 ? /* @__PURE__ */ jsx("span", {
7515
9328
  className: "sbk-select__chips",
@@ -7545,97 +9358,165 @@ function Select({ element, blockId }) {
7545
9358
  }) : closedLabel ? items[0] && isUnresolved(items[0]) ? unresolvedValue(items[0].id) : /* @__PURE__ */ jsx("span", {
7546
9359
  className: "sbk-select__value",
7547
9360
  children: closedLabel
9361
+ }) : buttonSelect ? /* @__PURE__ */ jsx("span", {
9362
+ className: "sbk-select__value",
9363
+ children: `--${placeholder}--`
7548
9364
  }) : /* @__PURE__ */ jsx("span", {
7549
9365
  className: "sbk-select__placeholder",
7550
9366
  children: placeholder
7551
- }), /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-select__chevron" })]
7552
- }),
7553
- open && /* @__PURE__ */ jsx(Popover, {
7554
- anchorRef: rootRef,
7555
- onDismiss: () => setOpen(false),
7556
- children: /* @__PURE__ */ jsxs("div", {
7557
- className: "sbk-select__menu",
7558
- role: "listbox",
7559
- ref: listRef,
7560
- children: [
7561
- (showSearchOnly || allOptions(element).length > 8) && /* @__PURE__ */ jsxs("div", {
7562
- className: "sbk-select__search",
7563
- children: [/* @__PURE__ */ jsx(SearchIcon$1, {}), /* @__PURE__ */ jsx("input", {
7564
- autoFocus: true,
7565
- className: "sbk-select__search-input",
7566
- placeholder: source === "static" || source === "external" ? "Search options" : `Search ${source}`,
7567
- value: query,
7568
- onChange: (e) => setQuery(e.target.value),
7569
- onKeyDown: (e) => {
7570
- if (e.key === "Enter" && query.trim() && showSearchOnly && !remote) selectItem({
7571
- id: query.trim(),
7572
- label: resolveLabel(query.trim())
7573
- });
7574
- }
7575
- })]
7576
- }),
7577
- (() => {
7578
- let index = 0;
7579
- return visibleGroups.map((group, gi) => {
7580
- const rows = group.options.map((option) => {
7581
- const i = index++;
7582
- return /* @__PURE__ */ jsx(SelectOption, {
7583
- option,
7584
- selected: isSelected(option),
7585
- onSelect: () => selectItem(optionToItem(option)),
7586
- navProps: nav.itemProps(i)
7587
- }, option.value ?? i);
7588
- });
7589
- return group.label === void 0 ? rows : /* @__PURE__ */ jsxs("div", {
7590
- className: "sbk-select__group",
7591
- children: [/* @__PURE__ */ jsx("div", {
7592
- className: "sbk-select__group-label",
7593
- children: group.label
7594
- }), rows]
7595
- }, gi);
7596
- });
7597
- })(),
7598
- remote && loading && /* @__PURE__ */ jsx("div", {
7599
- className: "sbk-select__status",
7600
- children: "Loading…"
7601
- }),
7602
- remote && !loading && remoteGroups !== void 0 && flatOptions.length === 0 && /* @__PURE__ */ jsx("div", {
7603
- className: "sbk-select__status",
7604
- children: "No results"
7605
- }),
7606
- showSearchOnly && items.length > 0 && multi && /* @__PURE__ */ jsx("div", {
7607
- className: "sbk-select__group-label",
7608
- children: "Selected"
7609
- }),
7610
- showSearchOnly && multi && items.map((item) => /* @__PURE__ */ jsx("div", {
7611
- className: "sbk-select__option sbk-select__option--selected",
7612
- children: item.label || resolveLabel(item.id)
7613
- }, item.id))
7614
- ]
7615
- })
9367
+ }), buttonSelect ? /* @__PURE__ */ jsx(CaretDownIcon$1, { className: "sbk-select__chevron" }) : /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-select__chevron" })]
7616
9368
  }),
9369
+ menu,
7617
9370
  dialog
7618
9371
  ]
7619
9372
  });
7620
9373
  }
7621
- function SelectOption({ option, selected, onSelect, navProps }) {
9374
+ /** Slack's presence and channel icons, 20×20 paths copied from Block Kit Builder. */
9375
+ const PRESENCE = {
9376
+ active: {
9377
+ label: "Active",
9378
+ d: "M14.5 10a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0"
9379
+ },
9380
+ snoozed: {
9381
+ label: "Active, notifications snoozed",
9382
+ evenOdd: true,
9383
+ d: "M11.25 3.5a.75.75 0 0 0 0 1.5h1.847l-2.411 2.756A.75.75 0 0 0 11.25 9h3.5a.75.75 0 0 0 0-1.5h-1.847l2.411-2.756A.75.75 0 0 0 14.75 3.5zM9.557 6.768C10.18 6.055 10 5.5 9.406 5.54a4.5 4.5 0 1 0 5.067 4.96H11.25a2.25 2.25 0 0 1-1.693-3.73"
9384
+ },
9385
+ slackbot: {
9386
+ label: "Active",
9387
+ evenOdd: true,
9388
+ d: "M9.73 6.173q.15.203.27.422.12-.218.27-.422C10.72 5.557 11.425 5 12.352 5c1.045 0 1.73.492 2.136 1.122.389.605.511 1.32.511 1.82 0 .486-.044 1.377-.681 2.513-.633 1.127-1.833 2.463-4.083 3.888a.44.44 0 0 1-.472 0c-2.25-1.425-3.45-2.76-4.083-3.888C5.044 9.319 5 8.428 5 7.94c0-.499.122-1.214.511-1.82C5.916 5.492 6.601 5 7.647 5c.927 0 1.632.557 2.084 1.173"
9389
+ }
9390
+ };
9391
+ const CHANNEL_HASH = "M9.74 2.878a.75.75 0 1 0-1.48-.255L7.68 6H3.75a.75.75 0 0 0 0 1.5h3.67L6.472 13H2.75a.75.75 0 0 0 0 1.5h3.463l-.452 2.623a.75.75 0 0 0 1.478.255l.496-2.878h3.228l-.452 2.623a.75.75 0 0 0 1.478.255l.496-2.878h3.765a.75.75 0 0 0 0-1.5h-3.506l.948-5.5h3.558a.75.75 0 0 0 0-1.5h-3.3l.54-3.122a.75.75 0 0 0-1.48-.255L12.43 6H9.2zM11.221 13l.948-5.5H8.942L7.994 13z";
9392
+ const CHANNEL_LOCK = "M10 1.5A4.5 4.5 0 0 0 5.5 6v1.5h-.25A2.25 2.25 0 0 0 3 9.75v6.5c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0 0 17 16.25v-6.5a2.25 2.25 0 0 0-2.25-2.25h-.25V6A4.5 4.5 0 0 0 10 1.5m3 6V6a3 3 0 1 0-6 0v1.5zM4.5 9.75A.75.75 0 0 1 5.25 9h9.5a.75.75 0 0 1 .75.75v6.5a.25.25 0 0 1-.25.25H4.75a.25.25 0 0 1-.25-.25z";
9393
+ /**
9394
+ * A person or channel in a directory-backed list, laid out as Slack's `c-member` and
9395
+ * `c-small_channel_entity` rows: a 20px avatar or 18px channel icon, 8px apart from the bold name,
9396
+ * then the person's "(you)" or badge, presence icon and full name.
9397
+ */
9398
+ function DirectoryRow({ entry }) {
9399
+ if (entry.type === "channel") return /* @__PURE__ */ jsxs("span", {
9400
+ className: "sbk-select__member sbk-select__member--channel",
9401
+ children: [/* @__PURE__ */ jsx("svg", {
9402
+ className: "sbk-select__channel-icon",
9403
+ "data-icon": entry.private ? "lock" : "hash",
9404
+ viewBox: "0 0 20 20",
9405
+ "aria-hidden": "true",
9406
+ children: /* @__PURE__ */ jsx("path", {
9407
+ fill: "currentColor",
9408
+ fillRule: "evenodd",
9409
+ clipRule: "evenodd",
9410
+ d: entry.private ? CHANNEL_LOCK : CHANNEL_HASH
9411
+ })
9412
+ }), /* @__PURE__ */ jsx("span", {
9413
+ className: "sbk-select__member-text",
9414
+ children: /* @__PURE__ */ jsx("span", {
9415
+ className: "sbk-select__member-name",
9416
+ children: entry.name
9417
+ })
9418
+ })]
9419
+ });
9420
+ const presence = entry.presence ? PRESENCE[entry.presence] : void 0;
9421
+ return /* @__PURE__ */ jsxs("span", {
9422
+ className: "sbk-select__member",
9423
+ children: [/* @__PURE__ */ jsx("span", {
9424
+ className: "sbk-select__avatar",
9425
+ children: entry.avatarUrl && /* @__PURE__ */ jsx("img", {
9426
+ src: entry.avatarUrl,
9427
+ alt: ""
9428
+ })
9429
+ }), /* @__PURE__ */ jsxs("span", {
9430
+ className: "sbk-select__member-text",
9431
+ children: [
9432
+ /* @__PURE__ */ jsx("span", {
9433
+ className: "sbk-select__member-name",
9434
+ children: /* @__PURE__ */ jsxs("strong", { children: [
9435
+ entry.name,
9436
+ entry.self && /* @__PURE__ */ jsx("span", {
9437
+ className: "sbk-select__member-you",
9438
+ children: "(you)"
9439
+ }),
9440
+ entry.badge && /* @__PURE__ */ jsx("span", {
9441
+ className: "sbk-select__member-badge",
9442
+ children: entry.badge
9443
+ })
9444
+ ] })
9445
+ }),
9446
+ presence && /* @__PURE__ */ jsx("svg", {
9447
+ className: "sbk-select__presence",
9448
+ viewBox: "0 0 20 20",
9449
+ role: "img",
9450
+ "aria-label": presence.label,
9451
+ children: /* @__PURE__ */ jsx("path", {
9452
+ fill: "currentColor",
9453
+ fillRule: presence.evenOdd ? "evenodd" : void 0,
9454
+ clipRule: presence.evenOdd ? "evenodd" : void 0,
9455
+ d: presence.d
9456
+ })
9457
+ }),
9458
+ entry.realName && /* @__PURE__ */ jsx("span", {
9459
+ className: "sbk-select__member-secondary",
9460
+ children: entry.realName
9461
+ })
9462
+ ]
9463
+ })]
9464
+ });
9465
+ }
9466
+ function SelectOption({ option, entry, content, selected, check, typed, onSelect, navProps }) {
7622
9467
  return /* @__PURE__ */ jsxs("div", {
7623
9468
  className: `sbk-select__option${selected ? " sbk-select__option--selected" : ""}`,
7624
9469
  role: "option",
7625
9470
  "aria-selected": selected,
9471
+ "data-typed": typed || void 0,
7626
9472
  onClick: onSelect,
7627
9473
  ...navProps,
7628
- children: [/* @__PURE__ */ jsx("span", {
7629
- className: "sbk-select__option-text",
7630
- children: /* @__PURE__ */ jsx(Text, { text: option.text })
7631
- }), option.description && /* @__PURE__ */ jsx("span", {
7632
- className: "sbk-select__option-description",
7633
- children: /* @__PURE__ */ jsx(Text, { text: option.description })
7634
- })]
9474
+ children: [
9475
+ check && /* @__PURE__ */ jsx("svg", {
9476
+ className: "sbk-select__check",
9477
+ viewBox: "0 0 16 22",
9478
+ width: "16",
9479
+ height: "22",
9480
+ "aria-hidden": "true",
9481
+ children: /* @__PURE__ */ jsx("path", {
9482
+ d: "M4.5 11.2 7 13.7l4.5-5.1",
9483
+ fill: "none",
9484
+ stroke: "currentColor",
9485
+ strokeWidth: "1.3",
9486
+ strokeLinecap: "round",
9487
+ strokeLinejoin: "round"
9488
+ })
9489
+ }),
9490
+ /* @__PURE__ */ jsxs("span", {
9491
+ className: "sbk-select__option-label",
9492
+ children: [content !== void 0 ? content : entry ? /* @__PURE__ */ jsx(DirectoryRow, { entry }) : /* @__PURE__ */ jsx("span", {
9493
+ className: "sbk-select__option-text",
9494
+ children: /* @__PURE__ */ jsx(Text, { text: option.text })
9495
+ }), option.description && /* @__PURE__ */ jsx("span", {
9496
+ className: "sbk-select__option-description",
9497
+ children: /* @__PURE__ */ jsx(Text, { text: option.description })
9498
+ })]
9499
+ }),
9500
+ typed && /* @__PURE__ */ jsx("span", {
9501
+ className: "sbk-select__shortcut",
9502
+ "aria-hidden": "true",
9503
+ children: /* @__PURE__ */ jsx("span", {
9504
+ className: "sbk-select__keycap",
9505
+ children: "Enter"
9506
+ })
9507
+ })
9508
+ ]
7635
9509
  });
7636
9510
  }
7637
9511
  //#endregion
7638
9512
  //#region src/elements/TextInput.tsx
9513
+ /** The Builder's placeholder for a field that sets none. Slack's email default isn't recorded yet. */
9514
+ const DEFAULT_PLACEHOLDER = {
9515
+ plain_text_input: "Write something",
9516
+ email_text_input: "Write something",
9517
+ url_text_input: "Enter a URL",
9518
+ number_input: "Enter a number"
9519
+ };
7639
9520
  const HTML_TYPE = {
7640
9521
  plain_text_input: "text",
7641
9522
  email_text_input: "email",
@@ -7664,6 +9545,7 @@ function TextInput({ element, blockId }) {
7664
9545
  });
7665
9546
  }
7666
9547
  function fire(next) {
9548
+ clearTimeout(typing.current);
7667
9549
  dispatch({
7668
9550
  type: element.type,
7669
9551
  action_id: actionId,
@@ -7671,6 +9553,12 @@ function TextInput({ element, blockId }) {
7671
9553
  value: next
7672
9554
  });
7673
9555
  }
9556
+ const typing = useRef(void 0);
9557
+ useEffect(() => () => clearTimeout(typing.current), []);
9558
+ function fireAfterPause(next) {
9559
+ clearTimeout(typing.current);
9560
+ typing.current = setTimeout(() => fire(next), 300);
9561
+ }
7674
9562
  function onChange(next) {
7675
9563
  if (element.type === "number_input") {
7676
9564
  const pattern = element.is_decimal_allowed ? /^-?\d*\.?\d*$/ : /^-?\d*$/;
@@ -7678,148 +9566,73 @@ function TextInput({ element, blockId }) {
7678
9566
  }
7679
9567
  setLocalValue(next);
7680
9568
  report(next);
7681
- if (dispatchEnabled && triggers.includes("on_character_entered")) fire(next);
9569
+ if (dispatchEnabled && triggers.includes("on_character_entered")) fireAfterPause(next);
7682
9570
  }
9571
+ const length = [...value].length;
9572
+ const tooLong = element.max_length !== void 0 && length > element.max_length;
9573
+ const tooShort = element.min_length !== void 0 && length > 0 && length < element.min_length;
9574
+ const outOfBounds = tooLong || tooShort;
9575
+ const remaining = element.max_length !== void 0 && length > 0 ? element.max_length - length : void 0;
9576
+ const countRef = useRef(null);
9577
+ const [countWidth, setCountWidth] = useState(void 0);
9578
+ useClientLayoutEffect(() => {
9579
+ setCountWidth(countRef.current?.getBoundingClientRect().width);
9580
+ }, [remaining]);
9581
+ const countId = useId();
7683
9582
  function onKeyDown(e) {
7684
- if (e.key === "Enter" && !element.multiline && dispatchEnabled && triggers.includes("on_enter_pressed")) fire(value);
9583
+ if (e.key === "Enter" && !element.multiline && dispatchEnabled && triggers.includes("on_enter_pressed") && !outOfBounds) fire(value);
7685
9584
  }
7686
- const placeholder = element.placeholder?.text ?? "Write something";
9585
+ const placeholder = element.placeholder?.text ?? DEFAULT_PLACEHOLDER[element.type];
9586
+ const invalidDescribed = invalid["aria-describedby"];
7687
9587
  const commonProps = {
7688
- className: `sbk-text-input${sizeClass}`,
9588
+ className: `sbk-text-input${sizeClass}${remaining !== void 0 ? " sbk-text-input--counted" : ""}`,
7689
9589
  value,
7690
9590
  placeholder,
7691
- onKeyDown,
7692
- ref: focusRef,
7693
- "aria-label": element.action_id,
7694
- ...invalid
7695
- };
7696
- const leadingIcon = element.type === "email_text_input" ? /* @__PURE__ */ jsx(EmailIcon, { className: "sbk-text-input__icon" }) : element.type === "url_text_input" ? /* @__PURE__ */ jsx(LinkGlyphIcon, { className: "sbk-text-input__icon" }) : null;
7697
- if (element.multiline) return /* @__PURE__ */ jsx("textarea", {
7698
- ...commonProps,
7699
- className: `sbk-text-input sbk-text-input--multiline${sizeClass}`,
7700
- onChange: (e) => onChange(e.target.value)
7701
- });
7702
- const input = /* @__PURE__ */ jsx("input", {
7703
- ...commonProps,
7704
- type: HTML_TYPE[element.type],
7705
- inputMode: element.type === "number_input" ? element.is_decimal_allowed ? "decimal" : "numeric" : void 0,
7706
- minLength: element.min_length,
7707
- maxLength: element.max_length,
7708
- onChange: (e) => onChange(e.target.value)
7709
- });
7710
- if (!leadingIcon) return input;
7711
- return /* @__PURE__ */ jsxs("div", {
7712
- className: "sbk-text-input__wrap",
7713
- children: [leadingIcon, input]
7714
- });
7715
- }
7716
- //#endregion
7717
- //#region src/elements/TimePicker.tsx
7718
- /** Formats `HH:mm` the way Slack's closed timepicker shows it, e.g. "1:37 PM". */
7719
- function formatTime(time) {
7720
- const [h = 0, m = 0] = time.split(":").map(Number);
7721
- const period = h >= 12 ? "PM" : "AM";
7722
- return `${h % 12 === 0 ? 12 : h % 12}:${String(m).padStart(2, "0")} ${period}`;
7723
- }
7724
- /** Every 30 minutes, as Slack's timepicker list offers. */
7725
- function timeOptions() {
7726
- const times = [];
7727
- for (let h = 0; h < 24; h++) for (const m of [0, 30]) times.push(`${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`);
7728
- return times;
7729
- }
7730
- function TimePicker({ element, blockId }) {
7731
- const { setValue, dispatch } = useBlockKit();
7732
- const { ask, dialog } = useConfirm(element.confirm);
7733
- const [time, setTime] = useState(element.initial_time);
7734
- const [open, setOpen] = useState(false);
7735
- const rootRef = useRef(null);
7736
- const triggerRef = useRef(null);
7737
- useFocusOnLoad(element, triggerRef);
7738
- const invalid = useInvalidProps();
7739
- const listRef = useRef(null);
7740
- const actionId = element.action_id ?? "";
7741
- useEffect(() => {
7742
- if (time) setValue(blockId, actionId, {
7743
- type: "timepicker",
7744
- selected_time: time
7745
- });
7746
- }, []);
7747
- async function pick(next) {
7748
- if (!await ask()) return;
7749
- setTime(next);
7750
- setOpen(false);
7751
- setValue(blockId, actionId, {
7752
- type: "timepicker",
7753
- selected_time: next
7754
- });
7755
- dispatch({
7756
- type: "timepicker",
7757
- action_id: actionId,
7758
- block_id: blockId,
7759
- selected_time: next,
7760
- ...element.initial_time !== void 0 ? { initial_time: element.initial_time } : {}
7761
- });
7762
- }
7763
- const times = timeOptions();
7764
- const nav = useMenuNavigation({
7765
- open,
7766
- count: times.length,
7767
- initialIndex: time ? times.indexOf(time) : -1,
7768
- onChoose: (i) => {
7769
- const t = times[i];
7770
- if (t) pick(t);
7771
- },
7772
- onClose: () => {
7773
- setOpen(false);
7774
- triggerRef.current?.focus();
7775
- },
7776
- onOpen: () => setOpen(true),
7777
- listRef
9591
+ onKeyDown,
9592
+ ref: focusRef,
9593
+ "aria-label": element.action_id,
9594
+ ...invalid,
9595
+ ...outOfBounds ? { "aria-invalid": true } : {},
9596
+ ...remaining !== void 0 ? {
9597
+ "aria-describedby": [invalidDescribed, countId].filter(Boolean).join(" "),
9598
+ style: countWidth !== void 0 ? { paddingRight: countWidth } : void 0
9599
+ } : {}
9600
+ };
9601
+ const count = remaining !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
9602
+ ref: countRef,
9603
+ className: `sbk-text-input__count${remaining < 0 ? " sbk-text-input__count--over" : ""}`,
9604
+ "aria-hidden": "true",
9605
+ children: remaining
9606
+ }), /* @__PURE__ */ jsx("span", {
9607
+ id: countId,
9608
+ className: "sbk-visually-hidden",
9609
+ children: `${remaining} characters remaining`
9610
+ })] }) : null;
9611
+ const leadingIcon = element.type === "email_text_input" ? /* @__PURE__ */ jsx(EmailIcon, { className: "sbk-text-input__icon" }) : element.type === "url_text_input" ? /* @__PURE__ */ jsx(LinkGlyphIcon, { className: "sbk-text-input__icon" }) : null;
9612
+ if (element.multiline) {
9613
+ const textarea = /* @__PURE__ */ jsx("textarea", {
9614
+ ...commonProps,
9615
+ className: `${commonProps.className} sbk-text-input--multiline`,
9616
+ onChange: (e) => onChange(e.target.value)
9617
+ });
9618
+ return element.max_length !== void 0 ? /* @__PURE__ */ jsxs("div", {
9619
+ className: "sbk-text-input__wrap sbk-text-input__wrap--multiline",
9620
+ children: [textarea, count]
9621
+ }) : textarea;
9622
+ }
9623
+ const input = /* @__PURE__ */ jsx("input", {
9624
+ ...commonProps,
9625
+ type: HTML_TYPE[element.type],
9626
+ inputMode: element.type === "number_input" ? element.is_decimal_allowed ? "decimal" : "numeric" : void 0,
9627
+ onChange: (e) => onChange(e.target.value)
7778
9628
  });
9629
+ if (!leadingIcon && element.max_length === void 0) return input;
7779
9630
  return /* @__PURE__ */ jsxs("div", {
7780
- className: "sbk-timepicker",
7781
- ref: rootRef,
7782
- onKeyDown: nav.onKeyDown,
9631
+ className: `sbk-text-input__wrap${leadingIcon ? " sbk-text-input__wrap--icon" : ""}`,
7783
9632
  children: [
7784
- /* @__PURE__ */ jsxs("button", {
7785
- ref: triggerRef,
7786
- type: "button",
7787
- className: "sbk-timepicker__control",
7788
- onClick: () => setOpen((o) => !o),
7789
- "aria-haspopup": "listbox",
7790
- "aria-expanded": open,
7791
- ...invalid,
7792
- children: [
7793
- /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-timepicker__icon" }),
7794
- /* @__PURE__ */ jsx("span", {
7795
- className: time ? "sbk-timepicker__value" : "sbk-timepicker__placeholder",
7796
- children: time ? formatTime(time) : element.placeholder?.text ?? "Select time"
7797
- }),
7798
- /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-timepicker__chevron" })
7799
- ]
7800
- }),
7801
- open && /* @__PURE__ */ jsx(Popover, {
7802
- anchorRef: rootRef,
7803
- onDismiss: () => setOpen(false),
7804
- children: /* @__PURE__ */ jsx("div", {
7805
- className: "sbk-timepicker__menu",
7806
- role: "listbox",
7807
- ref: listRef,
7808
- children: times.map((t, i) => /* @__PURE__ */ jsx("div", {
7809
- role: "option",
7810
- "aria-selected": t === time,
7811
- className: `sbk-timepicker__option${t === time ? " sbk-timepicker__option--selected" : ""}`,
7812
- onClick: () => pick(t),
7813
- ...nav.itemProps(i),
7814
- children: formatTime(t)
7815
- }, t))
7816
- })
7817
- }),
7818
- element.timezone && /* @__PURE__ */ jsxs("div", {
7819
- className: "sbk-timepicker__hint",
7820
- children: ["Timezone: ", element.timezone]
7821
- }),
7822
- dialog
9633
+ leadingIcon,
9634
+ input,
9635
+ count
7823
9636
  ]
7824
9637
  });
7825
9638
  }
@@ -8904,23 +10717,135 @@ function DataTable({ block }) {
8904
10717
  });
8905
10718
  }
8906
10719
  //#endregion
10720
+ //#region src/charts/ChartTooltip.tsx
10721
+ /** Space between the pointer and the tooltip, on both axes (ECharts' default). */
10722
+ const GAP = 20;
10723
+ /** Hidden for longer than this, the tooltip comes back with the fade only (ECharts' long hide). */
10724
+ const LONG_HIDE_MS = 500;
10725
+ /**
10726
+ * The tooltip Slack's charts show under the pointer (an ECharts tooltip): the category, then each
10727
+ * series' dot, name and value. It sits 20px right of and below the pointer, and on the other side
10728
+ * of it where it would run past the chart's right or bottom edge. Portaled to `<body>` like the
10729
+ * popovers, so the chart card, which clips its content, doesn't cut it off.
10730
+ *
10731
+ * It moves as ECharts' does: at once the first time, then sliding after the pointer (0.4s) and
10732
+ * fading in and out (0.2s); back after a hide longer than 500ms, it only fades in, where the
10733
+ * pointer is. With reduced motion it only fades (DataVisualization.css).
10734
+ */
10735
+ function ChartTooltip({ open, pointer, box, title, rows }) {
10736
+ const { theme } = useBlockKit();
10737
+ const ref = useRef(null);
10738
+ const [size, setSize] = useState(null);
10739
+ const [placed, setPlaced] = useState(false);
10740
+ const [fadeOnly, setFadeOnly] = useState(false);
10741
+ const [longHidden, setLongHidden] = useState(false);
10742
+ const [wasOpen, setWasOpen] = useState(open);
10743
+ if (open !== wasOpen) {
10744
+ setWasOpen(open);
10745
+ if (open) {
10746
+ setFadeOnly(longHidden);
10747
+ setLongHidden(false);
10748
+ }
10749
+ }
10750
+ useEffect(() => {
10751
+ if (open) return;
10752
+ const timer = setTimeout(() => setLongHidden(true), LONG_HIDE_MS);
10753
+ return () => clearTimeout(timer);
10754
+ }, [open]);
10755
+ useEffect(() => {
10756
+ if (!size || placed) return;
10757
+ const frame = requestAnimationFrame(() => setPlaced(true));
10758
+ return () => cancelAnimationFrame(frame);
10759
+ }, [size, placed]);
10760
+ useClientLayoutEffect(() => {
10761
+ const el = ref.current;
10762
+ if (!el) return;
10763
+ const { width, height } = el.getBoundingClientRect();
10764
+ if (width !== size?.width || height !== size?.height) setSize({
10765
+ width,
10766
+ height
10767
+ });
10768
+ });
10769
+ let left = pointer.x + GAP;
10770
+ let top = pointer.y + GAP;
10771
+ if (size) {
10772
+ if (pointer.x - box.left + GAP + size.width + 2 > box.width) left = pointer.x - GAP - size.width;
10773
+ if (pointer.y - box.top + GAP + size.height > box.height) top = pointer.y - GAP - size.height;
10774
+ }
10775
+ if (typeof document === "undefined") return null;
10776
+ return createPortal(/* @__PURE__ */ jsxs("div", {
10777
+ ref,
10778
+ className: [
10779
+ "sbk-root sbk-chart-tooltip",
10780
+ open && "sbk-chart-tooltip--open",
10781
+ placed && "sbk-chart-tooltip--animated",
10782
+ fadeOnly && "sbk-chart-tooltip--fade-only"
10783
+ ].filter(Boolean).join(" "),
10784
+ "data-theme": theme,
10785
+ role: "tooltip",
10786
+ "aria-hidden": open ? void 0 : true,
10787
+ style: {
10788
+ transform: `translate3d(${left}px, ${top}px, 0)`,
10789
+ visibility: size ? void 0 : "hidden"
10790
+ },
10791
+ children: [title !== void 0 ? /* @__PURE__ */ jsx("div", {
10792
+ className: "sbk-chart-tooltip__title",
10793
+ children: title
10794
+ }) : null, /* @__PURE__ */ jsx("div", {
10795
+ className: "sbk-chart-tooltip__rows",
10796
+ children: rows.map((row) => /* @__PURE__ */ jsxs("div", {
10797
+ className: "sbk-chart-tooltip__row",
10798
+ children: [
10799
+ /* @__PURE__ */ jsx("span", {
10800
+ className: "sbk-chart-tooltip__dot",
10801
+ style: { backgroundColor: row.color }
10802
+ }),
10803
+ /* @__PURE__ */ jsx("span", {
10804
+ className: "sbk-chart-tooltip__name",
10805
+ children: row.name
10806
+ }),
10807
+ /* @__PURE__ */ jsx("span", {
10808
+ className: "sbk-chart-tooltip__value",
10809
+ children: row.value.toLocaleString("en-US")
10810
+ })
10811
+ ]
10812
+ }, row.name))
10813
+ })]
10814
+ }), document.body);
10815
+ }
10816
+ //#endregion
10817
+ //#region src/charts/useLastValue.ts
10818
+ /**
10819
+ * The value, or while it's null the last value it had: what a tooltip that fades out keeps showing
10820
+ * after the pointer has left.
10821
+ */
10822
+ function useLastValue(value) {
10823
+ const [last, setLast] = useState(value);
10824
+ if (value !== null && value !== last) setLast(value);
10825
+ return value ?? last;
10826
+ }
10827
+ //#endregion
8907
10828
  //#region src/charts/palette.ts
8908
- /** Series/segment colours, in order, measured from Slack's Builder charts. */
8909
- const CHART_COLORS = [
8910
- "#e96825",
8911
- "#20a271",
8912
- "#c474d3",
8913
- "#0e9dd3"
8914
- ];
10829
+ /** How many series colours Slack cycles through. */
10830
+ const COLORS = 4;
10831
+ /**
10832
+ * A series or segment colour, as a token per theme (DataVisualization.css, and Message.css for
10833
+ * dark): Slack's dark charts use a deeper set. SVG presentation attributes don't resolve `var()`,
10834
+ * so apply it through `style`.
10835
+ */
10836
+ function colorForIndex(i) {
10837
+ return `var(--sbk-chart-${i % COLORS + 1})`;
10838
+ }
10839
+ /** The colour a hovered mark lifts to: each channel of the series colour x 1.1, rounded down. */
10840
+ function liftedColorForIndex(i) {
10841
+ return `var(--sbk-chart-${i % COLORS + 1}-lift)`;
10842
+ }
8915
10843
  /**
8916
10844
  * Fill of a series' area, as a token per theme (DataVisualization.css, and Message.css for dark):
8917
10845
  * a light tint of the series colour, or a dark one.
8918
10846
  */
8919
10847
  function areaFillForIndex(i) {
8920
- return `var(--sbk-chart-area-${i % CHART_COLORS.length + 1})`;
8921
- }
8922
- function colorForIndex(i) {
8923
- return CHART_COLORS[i % CHART_COLORS.length] ?? "#e96825";
10848
+ return `var(--sbk-chart-area-${i % COLORS + 1})`;
8924
10849
  }
8925
10850
  //#endregion
8926
10851
  //#region src/charts/paths.ts
@@ -9046,8 +10971,11 @@ function useContainerWidth(fallback) {
9046
10971
  //#endregion
9047
10972
  //#region src/charts/CartesianChart.tsx
9048
10973
  const HEIGHT$1 = 360;
10974
+ const PLOT_TOP = 9;
9049
10975
  const PLOT_BOTTOM = 334;
9050
10976
  const LABELS_Y = 342;
10977
+ /** The dashed line Slack's charts draw at the category under the pointer (ECharts' axis pointer). */
10978
+ const POINTER_COLOR = "#b7b9be";
9051
10979
  function leftMarginFor(ticks) {
9052
10980
  const maxLen = Math.max(...ticks.map((t) => formatTick(t).length), 1);
9053
10981
  return Math.max(74, 34 + maxLen * 8);
@@ -9071,16 +10999,48 @@ function CartesianChart({ type, series, categories }) {
9071
10999
  const barGapRatio = .1;
9072
11000
  const barWidth = series.length > 0 ? Math.max(1, groupWidth / (series.length + (series.length - 1) * barGapRatio)) : 0;
9073
11001
  const barGap = barWidth * barGapRatio;
9074
- return /* @__PURE__ */ jsx("div", {
11002
+ const [hover, setHover] = useState(null);
11003
+ const categoryX = (i) => type === "bar" ? bandCenterX(i) : pointX(i);
11004
+ function onPointerMove(e) {
11005
+ if (e.pointerType === "touch" || n === 0) return;
11006
+ const rect = e.currentTarget.getBoundingClientRect();
11007
+ const px = (e.clientX - rect.left) * width / rect.width;
11008
+ const py = (e.clientY - rect.top) * HEIGHT$1 / rect.height;
11009
+ if (px < plotLeft || px > plotRight || py < PLOT_TOP || py > PLOT_BOTTOM) {
11010
+ setHover(null);
11011
+ return;
11012
+ }
11013
+ const step = type === "bar" ? bandWidth : n > 1 ? plotWidth / (n - 1) : plotWidth;
11014
+ const raw = (px - plotLeft) / step;
11015
+ const index = Math.max(0, Math.min(n - 1, type === "bar" ? Math.floor(raw) : Math.round(raw)));
11016
+ setHover({
11017
+ index,
11018
+ pointer: {
11019
+ x: e.clientX,
11020
+ y: e.clientY
11021
+ },
11022
+ box: {
11023
+ left: rect.left,
11024
+ top: rect.top,
11025
+ width: rect.width,
11026
+ height: rect.height
11027
+ }
11028
+ });
11029
+ }
11030
+ const hovered = hover?.index;
11031
+ const shown = useLastValue(hover);
11032
+ return /* @__PURE__ */ jsxs("div", {
9075
11033
  className: "sbk-chart",
9076
11034
  ref,
9077
- children: /* @__PURE__ */ jsx("svg", {
11035
+ children: [/* @__PURE__ */ jsx("svg", {
9078
11036
  className: "sbk-chart__svg",
9079
11037
  width,
9080
11038
  height: HEIGHT$1,
9081
11039
  viewBox: `0 0 ${width} ${HEIGHT$1}`,
9082
11040
  role: "img",
9083
11041
  "aria-label": "Chart",
11042
+ onPointerMove,
11043
+ onPointerLeave: () => setHover(null),
9084
11044
  children: /* @__PURE__ */ jsxs("g", { children: [
9085
11045
  scale.ticks.map((t) => /* @__PURE__ */ jsx("line", {
9086
11046
  x1: plotLeft,
@@ -9119,62 +11079,171 @@ function CartesianChart({ type, series, categories }) {
9119
11079
  const top = positive ? y(d.value) : zeroY;
9120
11080
  const bottom = positive ? zeroY : y(d.value);
9121
11081
  return /* @__PURE__ */ jsx("path", {
11082
+ className: "sbk-chart__bar",
9122
11083
  d: roundedBarPath(x, top, barWidth, bottom, 4, positive),
9123
- fill: colorForIndex(si)
11084
+ style: { fill: i === hovered ? liftedColorForIndex(si) : colorForIndex(si) }
9124
11085
  }, `${si}-${i}`);
9125
11086
  })) : series.map((s, si) => {
9126
11087
  const points = s.data.map((d, i) => ({
9127
11088
  x: pointX(i),
9128
11089
  y: y(d.value)
9129
11090
  }));
9130
- const color = colorForIndex(si);
11091
+ const color = type === "line" && hovered !== void 0 ? liftedColorForIndex(si) : colorForIndex(si);
9131
11092
  return /* @__PURE__ */ jsxs("g", { children: [type === "area" ? /* @__PURE__ */ jsx("path", {
9132
11093
  d: `${smoothLinePath(points)}L${points[points.length - 1]?.x ?? 0} ${zeroY}L${points[0]?.x ?? 0} ${zeroY}Z`,
9133
11094
  style: { fill: areaFillForIndex(si) },
9134
11095
  fillOpacity: .7
9135
11096
  }) : null, /* @__PURE__ */ jsx("path", {
11097
+ className: "sbk-chart__line",
9136
11098
  d: smoothLinePath(points),
9137
11099
  fill: "none",
9138
- stroke: color,
11100
+ style: { stroke: color },
9139
11101
  strokeWidth: 2,
9140
11102
  strokeLinejoin: "bevel"
9141
11103
  })] }, si);
9142
- })
11104
+ }),
11105
+ hovered !== void 0 ? /* @__PURE__ */ jsx("line", {
11106
+ className: "sbk-chart__pointer",
11107
+ x1: crispLine(categoryX(hovered)),
11108
+ x2: crispLine(categoryX(hovered)),
11109
+ y1: PLOT_TOP,
11110
+ y2: PLOT_BOTTOM,
11111
+ stroke: POINTER_COLOR,
11112
+ strokeDasharray: "4 2"
11113
+ }) : null
9143
11114
  ] })
9144
- })
11115
+ }), shown ? /* @__PURE__ */ jsx(ChartTooltip, {
11116
+ open: hover !== null,
11117
+ pointer: shown.pointer,
11118
+ box: shown.box,
11119
+ title: categories[shown.index],
11120
+ rows: series.map((s, si) => ({
11121
+ name: s.name,
11122
+ value: s.data.find((d) => d.label === categories[shown.index])?.value ?? s.data[shown.index]?.value ?? 0,
11123
+ color: colorForIndex(si)
11124
+ }))
11125
+ }) : null]
9145
11126
  });
9146
11127
  }
9147
11128
  //#endregion
11129
+ //#region src/charts/ChartTableModal.tsx
11130
+ /**
11131
+ * What a chart's "View as table" opens in Slack: a modal (`c-sk-modal`) that fills the window but
11132
+ * for 28px on each side, up to 1280px wide, titled with the chart's title and holding the data as a
11133
+ * `table` block, without its hover actions. Escape, the close button and a click on the dimmed
11134
+ * overlay close it. Slack leaves focus on the page when it closes; this returns it to whatever
11135
+ * opened it (the chart's "More actions" button), so keyboard users keep their place.
11136
+ */
11137
+ function ChartTableModal({ title, rows, onClose }) {
11138
+ const { theme } = useBlockKit();
11139
+ const titleId = useId();
11140
+ const dialogRef = useRef(null);
11141
+ const closeRef = useRef(null);
11142
+ const [header, ...body] = rows;
11143
+ useEffect(() => {
11144
+ const opener = document.activeElement;
11145
+ dialogRef.current?.focus();
11146
+ return () => {
11147
+ if (opener?.isConnected) opener.focus();
11148
+ };
11149
+ }, []);
11150
+ function onKeyDown(event) {
11151
+ if (event.key === "Escape") {
11152
+ event.stopPropagation();
11153
+ onClose();
11154
+ } else if (event.key === "Tab") {
11155
+ event.preventDefault();
11156
+ closeRef.current?.focus();
11157
+ }
11158
+ }
11159
+ if (typeof document === "undefined") return null;
11160
+ return createPortal(/* @__PURE__ */ jsx("div", {
11161
+ className: "sbk-root sbk-chart-table-layer",
11162
+ "data-theme": theme,
11163
+ role: "presentation",
11164
+ onClick: onClose,
11165
+ children: /* @__PURE__ */ jsxs("div", {
11166
+ ref: dialogRef,
11167
+ className: "sbk-chart-table",
11168
+ role: "dialog",
11169
+ "aria-modal": "true",
11170
+ "aria-labelledby": titleId,
11171
+ tabIndex: -1,
11172
+ onKeyDown,
11173
+ onClick: (event) => event.stopPropagation(),
11174
+ children: [/* @__PURE__ */ jsxs("div", {
11175
+ className: "sbk-chart-table__header",
11176
+ children: [/* @__PURE__ */ jsx("h1", {
11177
+ id: titleId,
11178
+ className: "sbk-chart-table__title",
11179
+ children: title
11180
+ }), /* @__PURE__ */ jsx("button", {
11181
+ ref: closeRef,
11182
+ type: "button",
11183
+ className: "sbk-chart-table__close",
11184
+ "aria-label": "Close",
11185
+ onClick: onClose,
11186
+ children: /* @__PURE__ */ jsx(CloseIcon, {
11187
+ width: "20",
11188
+ height: "20"
11189
+ })
11190
+ })]
11191
+ }), /* @__PURE__ */ jsx("div", {
11192
+ className: "sbk-chart-table__content",
11193
+ children: /* @__PURE__ */ jsxs("table", {
11194
+ className: "sbk-table__table sbk-chart-table__table",
11195
+ children: [header ? /* @__PURE__ */ jsx("thead", {
11196
+ className: "sbk-table__thead",
11197
+ children: /* @__PURE__ */ jsx("tr", { children: header.map((cell, i) => /* @__PURE__ */ jsx("th", {
11198
+ className: "sbk-table__th",
11199
+ children: cell
11200
+ }, i)) })
11201
+ }) : null, /* @__PURE__ */ jsx("tbody", {
11202
+ className: "sbk-table__tbody",
11203
+ children: body.map((row, r) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, i) => /* @__PURE__ */ jsx("td", {
11204
+ className: "sbk-table__td",
11205
+ children: cell
11206
+ }, i)) }, r))
11207
+ })]
11208
+ })
11209
+ })]
11210
+ })
11211
+ }), document.body);
11212
+ }
11213
+ //#endregion
9148
11214
  //#region src/charts/PieChart.tsx
11215
+ /**
11216
+ * Builder's pie plot area is as wide as the card's content (406px in a desktop message, 374px in a
11217
+ * phone's 400px card) and 360px tall, with a pie of radius 144 centred in it.
11218
+ */
9149
11219
  const WIDTH = 406;
9150
- /** Builder's pie plot area is 406×360, not square, with the pie centred in it. */
9151
11220
  const HEIGHT = 360;
9152
- const CENTER = {
9153
- x: 203,
9154
- y: 180
9155
- };
11221
+ const CENTER_Y = 180;
9156
11222
  const RADIUS = 144;
9157
11223
  /** Point on the pie's circle for a given angle in degrees, measured clockwise from 12 o'clock. */
9158
- function pointAt(angleDeg) {
11224
+ function pointAt(cx, angleDeg, radius = RADIUS) {
9159
11225
  const rad = (angleDeg - 90) * Math.PI / 180;
9160
11226
  return {
9161
- x: CENTER.x + RADIUS * Math.cos(rad),
9162
- y: CENTER.y + RADIUS * Math.sin(rad)
11227
+ x: cx + radius * Math.cos(rad),
11228
+ y: CENTER_Y + radius * Math.sin(rad)
9163
11229
  };
9164
11230
  }
9165
- function segmentPath(startAngle, endAngle) {
11231
+ function segmentPath(cx, startAngle, endAngle, radius = RADIUS) {
9166
11232
  const sweep = endAngle - startAngle;
9167
- const start = pointAt(startAngle);
9168
- const end = pointAt(endAngle);
11233
+ const start = pointAt(cx, startAngle, radius);
11234
+ const end = pointAt(cx, endAngle, radius);
9169
11235
  const largeArc = sweep > 180 ? 1 : 0;
9170
11236
  if (sweep >= 359.999) {
9171
- const stop = pointAt(startAngle + 360 - 1e-4 * 180 / Math.PI);
11237
+ const stop = pointAt(cx, startAngle + 360 - 1e-4 * 180 / Math.PI, radius);
9172
11238
  const r4 = (n) => Math.round(n * 1e4) / 1e4;
9173
- return `M${r4(start.x)} ${r4(start.y)}A${RADIUS} ${RADIUS} 0 1 1 ${r4(stop.x)} ${r4(stop.y)}Z`;
11239
+ return `M${r4(start.x)} ${r4(start.y)}A${radius} ${radius} 0 1 1 ${r4(stop.x)} ${r4(stop.y)}Z`;
9174
11240
  }
9175
- return `M${CENTER.x} ${CENTER.y}L${start.x} ${start.y}A${RADIUS} ${RADIUS} 0 ${largeArc} 1 ${end.x} ${end.y}Z`;
11241
+ return `M${cx} ${CENTER_Y}L${start.x} ${start.y}A${radius} ${radius} 0 ${largeArc} 1 ${end.x} ${end.y}Z`;
9176
11242
  }
9177
11243
  function PieChart({ segments }) {
11244
+ const [hover, setHover] = useState(null);
11245
+ const { ref, width } = useContainerWidth(WIDTH);
11246
+ const cx = width / 2;
9178
11247
  const total = segments.reduce((sum, s) => sum + Math.max(0, s.value), 0);
9179
11248
  let angle = 0;
9180
11249
  const arcs = segments.map((segment, i) => {
@@ -9183,26 +11252,73 @@ function PieChart({ segments }) {
9183
11252
  const start = angle;
9184
11253
  const end = angle + sweep;
9185
11254
  angle = end;
11255
+ const hovered = hover?.index === i;
9186
11256
  return {
9187
11257
  key: i,
9188
- d: segmentPath(start, end),
9189
- color: colorForIndex(i)
11258
+ hovered,
11259
+ d: segmentPath(cx, start, end, hovered ? 149 : RADIUS),
11260
+ color: hovered ? liftedColorForIndex(i) : colorForIndex(i)
9190
11261
  };
9191
11262
  });
9192
- return /* @__PURE__ */ jsx("svg", {
9193
- className: "sbk-chart__svg sbk-chart__svg--pie",
9194
- width: WIDTH,
9195
- height: HEIGHT,
9196
- viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
9197
- role: "img",
9198
- "aria-label": "Pie chart",
9199
- children: arcs.map((arc) => /* @__PURE__ */ jsx("path", {
9200
- d: arc.d,
9201
- fill: arc.color,
9202
- stroke: "#fff",
9203
- strokeWidth: 2,
9204
- strokeLinejoin: "round"
9205
- }, arc.key))
11263
+ const ordered = [...arcs.filter((a) => !a.hovered), ...arcs.filter((a) => a.hovered)];
11264
+ function onPointerMove(e) {
11265
+ if (e.pointerType === "touch") return;
11266
+ const index = e.target.getAttribute("data-index");
11267
+ if (index === null) {
11268
+ setHover(null);
11269
+ return;
11270
+ }
11271
+ const rect = e.currentTarget.getBoundingClientRect();
11272
+ setHover({
11273
+ index: Number(index),
11274
+ pointer: {
11275
+ x: e.clientX,
11276
+ y: e.clientY
11277
+ },
11278
+ box: {
11279
+ left: rect.left,
11280
+ top: rect.top,
11281
+ width: rect.width,
11282
+ height: rect.height
11283
+ }
11284
+ });
11285
+ }
11286
+ const shown = useLastValue(hover);
11287
+ const segment = shown ? segments[shown.index] : void 0;
11288
+ return /* @__PURE__ */ jsxs("div", {
11289
+ className: "sbk-chart",
11290
+ ref,
11291
+ children: [/* @__PURE__ */ jsx("svg", {
11292
+ className: "sbk-chart__svg sbk-chart__svg--pie",
11293
+ width,
11294
+ height: HEIGHT,
11295
+ viewBox: `0 0 ${width} ${HEIGHT}`,
11296
+ role: "img",
11297
+ "aria-label": "Pie chart",
11298
+ onPointerMove,
11299
+ onPointerLeave: () => setHover(null),
11300
+ children: ordered.map((arc) => /* @__PURE__ */ jsx("path", {
11301
+ className: "sbk-chart__slice",
11302
+ "data-index": arc.key,
11303
+ "data-hovered": arc.hovered || void 0,
11304
+ d: arc.d,
11305
+ style: {
11306
+ fill: arc.color,
11307
+ stroke: "var(--sbk-bg)"
11308
+ },
11309
+ strokeWidth: 2,
11310
+ strokeLinejoin: "round"
11311
+ }, arc.key))
11312
+ }), shown && segment ? /* @__PURE__ */ jsx(ChartTooltip, {
11313
+ open: hover !== null,
11314
+ pointer: shown.pointer,
11315
+ box: shown.box,
11316
+ rows: [{
11317
+ name: segment.label,
11318
+ value: segment.value,
11319
+ color: colorForIndex(shown.index)
11320
+ }]
11321
+ }) : null]
9206
11322
  });
9207
11323
  }
9208
11324
  //#endregion
@@ -9212,13 +11328,14 @@ function PieChart({ segments }) {
9212
11328
  * 12px-radius pill of 32px icon buttons that fades in while its host is hovered or focused.
9213
11329
  * The host needs the `sbk-hover-actions-host` class.
9214
11330
  */
9215
- function HoverActions({ actions, className }) {
11331
+ function HoverActions({ actions, className, menuSide = "left" }) {
9216
11332
  return /* @__PURE__ */ jsx("div", {
9217
11333
  className: `sbk-hover-actions${className ? ` ${className}` : ""}`,
9218
11334
  role: "group",
9219
11335
  children: actions.map((action) => action.menu ? /* @__PURE__ */ jsx(MenuAction, {
9220
11336
  action,
9221
- items: action.menu
11337
+ items: action.menu,
11338
+ side: menuSide
9222
11339
  }, action.label) : /* @__PURE__ */ jsx(Tooltip, {
9223
11340
  label: action.label,
9224
11341
  children: action.href ? /* @__PURE__ */ jsx("a", {
@@ -9238,22 +11355,18 @@ function HoverActions({ actions, className }) {
9238
11355
  }, action.label))
9239
11356
  });
9240
11357
  }
9241
- function MenuAction({ action, items }) {
11358
+ const BUTTON_SIZE = 32;
11359
+ function MenuAction({ action, items, side }) {
9242
11360
  const [open, setOpen] = useState(false);
9243
- const rootRef = useRef(null);
9244
11361
  const triggerRef = useRef(null);
9245
11362
  const listRef = useRef(null);
9246
- useEffect(() => {
9247
- if (!open) return;
9248
- const onDocClick = (e) => {
9249
- if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
9250
- };
9251
- document.addEventListener("mousedown", onDocClick);
9252
- return () => document.removeEventListener("mousedown", onDocClick);
9253
- }, [open]);
11363
+ const idPrefix = useId();
9254
11364
  function choose(i) {
11365
+ const item = items[i];
11366
+ if (!item || item.disabled) return;
9255
11367
  setOpen(false);
9256
- items[i]?.onSelect();
11368
+ triggerRef.current?.focus();
11369
+ item.onSelect();
9257
11370
  }
9258
11371
  const nav = useMenuNavigation({
9259
11372
  open,
@@ -9268,7 +11381,6 @@ function MenuAction({ action, items }) {
9268
11381
  });
9269
11382
  return /* @__PURE__ */ jsxs("div", {
9270
11383
  className: "sbk-hover-actions__menu-root",
9271
- ref: rootRef,
9272
11384
  onKeyDown: nav.onKeyDown,
9273
11385
  children: [/* @__PURE__ */ jsx(Tooltip, {
9274
11386
  label: open ? "" : action.label,
@@ -9282,17 +11394,34 @@ function MenuAction({ action, items }) {
9282
11394
  onClick: () => setOpen((o) => !o),
9283
11395
  children: action.icon
9284
11396
  })
9285
- }), open && /* @__PURE__ */ jsx("div", {
9286
- className: "sbk-overflow__menu sbk-hover-actions__menu",
9287
- role: "menu",
9288
- ref: listRef,
9289
- children: items.map((item, i) => /* @__PURE__ */ jsx("div", {
9290
- role: "menuitem",
9291
- className: "sbk-overflow__option",
9292
- onClick: () => choose(i),
9293
- ...nav.itemProps(i),
9294
- children: item.label
9295
- }, item.label))
11397
+ }), open && /* @__PURE__ */ jsx(Popover, {
11398
+ anchorRef: triggerRef,
11399
+ onDismiss: () => setOpen(false),
11400
+ gap: -32,
11401
+ offsetX: side === "right" ? BUTTON_SIZE : -304,
11402
+ onPlaced: () => listRef.current?.focus(),
11403
+ children: /* @__PURE__ */ jsx("div", {
11404
+ className: "sbk-overflow__menu sbk-hover-actions__menu",
11405
+ role: "menu",
11406
+ ref: listRef,
11407
+ tabIndex: -1,
11408
+ "aria-activedescendant": nav.active >= 0 ? `${idPrefix}-${nav.active}` : void 0,
11409
+ children: items.map((item, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [item.separatorBefore ? /* @__PURE__ */ jsx("hr", { className: "sbk-hover-actions__separator" }) : null, /* @__PURE__ */ jsxs("div", {
11410
+ id: `${idPrefix}-${i}`,
11411
+ role: "menuitem",
11412
+ "aria-disabled": item.disabled || void 0,
11413
+ className: item.disabled ? "sbk-overflow__option sbk-hover-actions__item sbk-hover-actions__item--disabled" : "sbk-overflow__option sbk-hover-actions__item",
11414
+ onClick: () => choose(i),
11415
+ ...nav.itemProps(i),
11416
+ children: [item.icon ? /* @__PURE__ */ jsx("span", {
11417
+ className: "sbk-hover-actions__icon",
11418
+ children: item.icon
11419
+ }) : null, /* @__PURE__ */ jsx("span", {
11420
+ className: "sbk-hover-actions__label",
11421
+ children: item.label
11422
+ })]
11423
+ })] }, item.label))
11424
+ })
9296
11425
  })]
9297
11426
  });
9298
11427
  }
@@ -9335,8 +11464,11 @@ function TableActions({ tableRef }) {
9335
11464
  onClick: () => copyText(toTsv(tableRows(tableRef.current)))
9336
11465
  }] });
9337
11466
  }
9338
- /** Slack's `p-attachment_image_actions`: open the full image in a new window, plus a menu. */
9339
- function ImageActions({ url }) {
11467
+ /**
11468
+ * Slack's `p-attachment_image_actions`: open the full image in a new window, plus a menu to copy
11469
+ * its link or hide it (as the caret beside its title does).
11470
+ */
11471
+ function ImageActions({ url, onHide }) {
9340
11472
  return /* @__PURE__ */ jsx(HoverActions, {
9341
11473
  className: "sbk-hover-actions--image",
9342
11474
  actions: [{
@@ -9347,47 +11479,69 @@ function ImageActions({ url }) {
9347
11479
  label: "More actions",
9348
11480
  icon: /* @__PURE__ */ jsx(EllipsisVerticalIcon, {}),
9349
11481
  menu: [{
9350
- label: "Copy link to image",
11482
+ label: "Copy link",
9351
11483
  onSelect: () => copyText(url)
9352
11484
  }, {
9353
- label: "Download image",
9354
- onSelect: () => {
9355
- const link = document.createElement("a");
9356
- link.href = url;
9357
- link.download = "";
9358
- link.target = "_blank";
9359
- link.rel = "noopener noreferrer";
9360
- link.click();
9361
- }
11485
+ label: "Hide image",
11486
+ onSelect: onHide
9362
11487
  }]
9363
11488
  }]
9364
11489
  });
9365
11490
  }
9366
- /** A chart's single "More actions" menu, exporting its underlying data. */
9367
- function ChartActions({ rows, title }) {
9368
- const name = `${(title || "chart").replace(/[^\w-]+/g, "_")}.csv`;
11491
+ /**
11492
+ * Slack's file name for a chart's data: the title with its accents dropped, lowercased, and each
11493
+ * run of other characters than letters, digits and `_` turned into a hyphen.
11494
+ */
11495
+ function chartFileName(title) {
11496
+ return `${(title ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9_]+/g, "-").replace(/^-+|-+$/g, "") || "chart"}.tsv`;
11497
+ }
11498
+ /**
11499
+ * A chart's "More actions" menu, as Slack lists it: "View as table" opens the data in a modal,
11500
+ * "Download chart data" saves it as a TSV, and "Copy as image" shows disabled, as in Block Kit
11501
+ * Builder. None of them reaches the app.
11502
+ */
11503
+ function ChartActions({ rows, title, onViewTable }) {
9369
11504
  return /* @__PURE__ */ jsx(HoverActions, {
9370
11505
  className: "sbk-hover-actions--chart",
11506
+ menuSide: "right",
9371
11507
  actions: [{
9372
11508
  label: "More actions",
9373
11509
  icon: /* @__PURE__ */ jsx(EllipsisVerticalIcon, {}),
9374
- menu: [{
9375
- label: "Copy data",
9376
- onSelect: () => copyText(toTsv(rows))
9377
- }, {
9378
- label: "Download as CSV",
9379
- onSelect: () => downloadText(name, toCsv(rows))
9380
- }]
11510
+ menu: [
11511
+ {
11512
+ label: "View as table",
11513
+ icon: /* @__PURE__ */ jsx(TableIcon, {}),
11514
+ onSelect: onViewTable
11515
+ },
11516
+ {
11517
+ label: "Download chart data",
11518
+ icon: /* @__PURE__ */ jsx(DownloadIcon, {
11519
+ width: "15",
11520
+ height: "15"
11521
+ }),
11522
+ onSelect: () => downloadText(chartFileName(title), toTsv(rows), "text/tab-separated-values")
11523
+ },
11524
+ {
11525
+ label: "Copy as image",
11526
+ icon: /* @__PURE__ */ jsx(ImageIcon, {}),
11527
+ separatorBefore: true,
11528
+ disabled: true,
11529
+ onSelect: () => {}
11530
+ }
11531
+ ]
9381
11532
  }]
9382
11533
  });
9383
11534
  }
9384
11535
  //#endregion
9385
11536
  //#region src/blocks/DataVisualization.tsx
9386
- /** The chart's data as a table: a label column plus one column per series (or a value column). */
9387
- function chartRows(chart, categories) {
11537
+ /**
11538
+ * The chart's data as Slack tabulates it for "View as table" and "Download chart data": a pie's
11539
+ * segments under "Label" and "Value", otherwise one row per series under "Series" and the
11540
+ * categories. Values are written as given ("1500", not "1,500").
11541
+ */
11542
+ function chartTable(chart, categories) {
9388
11543
  if (chart.type === "pie") return [["Label", "Value"], ...(chart.segments ?? []).map((s) => [s.label, String(s.value)])];
9389
- const series = chart.series ?? [];
9390
- return [[chart.axis_config?.x_label ?? "", ...series.map((s) => s.name)], ...categories.map((label, i) => [label, ...series.map((s) => String(s.data.find((d) => d.label === label)?.value ?? s.data[i]?.value ?? ""))])];
11544
+ return [["Series", ...categories], ...(chart.series ?? []).map((s) => [s.name, ...categories.map((label, i) => String(s.data.find((d) => d.label === label)?.value ?? s.data[i]?.value ?? ""))])];
9391
11545
  }
9392
11546
  function legendEntries(chart) {
9393
11547
  if (chart.type === "pie") return (chart.segments ?? []).map((s, i) => ({
@@ -9400,47 +11554,63 @@ function legendEntries(chart) {
9400
11554
  }));
9401
11555
  }
9402
11556
  function DataVisualization({ block }) {
11557
+ const [tableOpen, setTableOpen] = useState(false);
9403
11558
  const chart = block.chart;
9404
11559
  if (!chart) return null;
9405
11560
  const isPie = chart.type === "pie";
9406
11561
  const categories = chart.axis_config?.categories ?? chart.series?.[0]?.data.map((d) => d.label) ?? [];
9407
11562
  const legend = legendEntries(chart);
11563
+ const rows = chartTable(chart, categories);
9408
11564
  return /* @__PURE__ */ jsxs("div", {
9409
- className: "sbk-dataviz sbk-hover-actions-host",
11565
+ className: "sbk-dataviz-frame sbk-hover-actions-host",
9410
11566
  children: [
9411
- block.title ? /* @__PURE__ */ jsx("h3", {
9412
- className: "sbk-dataviz__title",
9413
- children: block.title
9414
- }) : null,
9415
- /* @__PURE__ */ jsxs("div", {
9416
- className: isPie ? "sbk-dataviz__body sbk-dataviz__body--pie" : "sbk-dataviz__body",
9417
- children: [!isPie && chart.axis_config?.y_label ? /* @__PURE__ */ jsx("div", {
9418
- className: "sbk-dataviz__y-label",
9419
- children: chart.axis_config.y_label
9420
- }) : null, chart.type === "pie" ? /* @__PURE__ */ jsx(PieChart, { segments: chart.segments ?? [] }) : /* @__PURE__ */ jsx(CartesianChart, {
9421
- type: chart.type,
9422
- series: chart.series ?? [],
9423
- categories
9424
- })]
11567
+ /* @__PURE__ */ jsx("div", {
11568
+ className: "sbk-dataviz-scroll",
11569
+ children: /* @__PURE__ */ jsxs("div", {
11570
+ className: "sbk-dataviz",
11571
+ children: [
11572
+ block.title ? /* @__PURE__ */ jsx("h3", {
11573
+ className: "sbk-dataviz__title",
11574
+ children: block.title
11575
+ }) : null,
11576
+ /* @__PURE__ */ jsxs("div", {
11577
+ className: isPie ? "sbk-dataviz__body sbk-dataviz__body--pie" : "sbk-dataviz__body",
11578
+ children: [!isPie && chart.axis_config?.y_label ? /* @__PURE__ */ jsx("div", {
11579
+ className: "sbk-dataviz__y-label",
11580
+ children: chart.axis_config.y_label
11581
+ }) : null, chart.type === "pie" ? /* @__PURE__ */ jsx(PieChart, { segments: chart.segments ?? [] }) : /* @__PURE__ */ jsx(CartesianChart, {
11582
+ type: chart.type,
11583
+ series: chart.series ?? [],
11584
+ categories
11585
+ })]
11586
+ }),
11587
+ !isPie && chart.axis_config?.x_label ? /* @__PURE__ */ jsx("div", {
11588
+ className: "sbk-dataviz__x-label",
11589
+ children: chart.axis_config.x_label
11590
+ }) : null,
11591
+ legend.length > 0 ? /* @__PURE__ */ jsx("div", {
11592
+ className: isPie ? "sbk-dataviz__legend sbk-dataviz__legend--pie" : "sbk-dataviz__legend",
11593
+ children: legend.map((entry) => /* @__PURE__ */ jsxs("span", {
11594
+ className: "sbk-dataviz__legend-entry",
11595
+ children: [/* @__PURE__ */ jsx("span", {
11596
+ className: "sbk-dataviz__legend-dot",
11597
+ style: { backgroundColor: entry.color }
11598
+ }), entry.name]
11599
+ }, entry.name))
11600
+ }) : null
11601
+ ]
11602
+ })
9425
11603
  }),
9426
- !isPie && chart.axis_config?.x_label ? /* @__PURE__ */ jsx("div", {
9427
- className: "sbk-dataviz__x-label",
9428
- children: chart.axis_config.x_label
9429
- }) : null,
9430
- legend.length > 0 ? /* @__PURE__ */ jsx("div", {
9431
- className: isPie ? "sbk-dataviz__legend sbk-dataviz__legend--pie" : "sbk-dataviz__legend",
9432
- children: legend.map((entry) => /* @__PURE__ */ jsxs("span", {
9433
- className: "sbk-dataviz__legend-entry",
9434
- children: [/* @__PURE__ */ jsx("span", {
9435
- className: "sbk-dataviz__legend-dot",
9436
- style: { backgroundColor: entry.color }
9437
- }), entry.name]
9438
- }, entry.name))
9439
- }) : null,
9440
11604
  /* @__PURE__ */ jsx(ChartActions, {
9441
- rows: chartRows(chart, categories),
9442
- title: block.title
9443
- })
11605
+ rows,
11606
+ title: block.title,
11607
+ onViewTable: () => setTableOpen(true)
11608
+ }),
11609
+ tableOpen ? /* @__PURE__ */ jsx(ChartTableModal, {
11610
+ title: block.title,
11611
+ rows,
11612
+ onClose: () => setTableOpen(false)
11613
+ }) : null
9444
11614
  ]
9445
11615
  });
9446
11616
  }
@@ -9558,25 +11728,39 @@ function ExpandCaret({ expanded }) {
9558
11728
  });
9559
11729
  }
9560
11730
  /**
9561
- * `image_url` renders directly. `slack_file` references a file in a workspace's file store —
9562
- * without an authenticated `files.info` call we can't resolve it to a URL, so we render the
9563
- * documented fallback (alt text in a placeholder box) instead of a broken `<img>`.
11731
+ * `image_url` renders directly. `slack_file` references a file in a workspace's file store, which
11732
+ * the app resolves through `resolvers.slackFile` (e.g. with `files.info`); a resolved file renders
11733
+ * like an `image_url`, and without a resolver (or for a file it doesn't know) the documented
11734
+ * fallback shows instead (alt text in a placeholder box) rather than a broken `<img>`.
9564
11735
  *
9565
11736
  * Slack always shows a title row above the image with an expand caret and a "(N kB)" file-size
9566
- * caption — the caption comes from fetching the file's real bytes, which isn't available to us
9567
- * without a network round-trip, so we render the row (title + caret) but omit the byte count.
9568
- * Keeping the row's height/spacing matches Slack's vertical rhythm even where the text differs.
11737
+ * caption, the size coming from the file's bytes. We show it when a resolver gives the size
11738
+ * (`slackFile` for a workspace file, `imageSize` for an `image_url`); we don't download the image
11739
+ * to measure it, so without one the row keeps its height and spacing without the caption.
11740
+ *
11741
+ * Slack draws a workspace file from its 800px thumbnail at 2x, so a resolved `slack_file` shows at
11742
+ * most 400 x 400; an `image_url` fills the message's width.
9569
11743
  *
9570
11744
  * The caret is a toggle, as in Slack: pressing it hides the image (instantly, no animation) and
9571
11745
  * leaves only the title row, and pressing it again brings the image back.
9572
11746
  */
9573
11747
  function Image({ block }) {
9574
11748
  const json = block;
9575
- const imageUrl = "image_url" in block ? block.image_url : void 0;
11749
+ const directUrl = "image_url" in block ? block.image_url : void 0;
9576
11750
  const slackFile = json.slack_file;
11751
+ const { resolvers } = useBlockKit();
11752
+ const resolved = slackFile ? resolvers.slackFile?.(slackFile) : void 0;
11753
+ const imageUrl = directUrl ?? resolved?.url;
9577
11754
  const [expanded, setExpanded] = useState(true);
11755
+ const toggleRef = useRef(null);
11756
+ const hide = () => {
11757
+ setExpanded(false);
11758
+ toggleRef.current?.focus();
11759
+ };
11760
+ const isFile = directUrl === void 0 && imageUrl !== void 0;
11761
+ const size = directUrl !== void 0 ? resolvers.imageSize?.(directUrl) : resolved?.size;
9578
11762
  return /* @__PURE__ */ jsxs("figure", {
9579
- className: "sbk-image",
11763
+ className: isFile ? "sbk-image sbk-image--file" : "sbk-image",
9580
11764
  children: [imageUrl && /* @__PURE__ */ jsxs("div", {
9581
11765
  className: "sbk-image__title",
9582
11766
  children: [block.title && /* @__PURE__ */ jsx("span", {
@@ -9584,15 +11768,20 @@ function Image({ block }) {
9584
11768
  children: /* @__PURE__ */ jsx(Text, { text: block.title })
9585
11769
  }), /* @__PURE__ */ jsxs("span", {
9586
11770
  className: "sbk-image__trigger",
9587
- children: [" ", /* @__PURE__ */ jsx("button", {
9588
- type: "button",
9589
- className: "sbk-image__toggle",
9590
- "aria-label": "image",
9591
- "aria-expanded": expanded,
9592
- title: expanded ? "Collapse" : "Expand",
9593
- onClick: () => setExpanded((value) => !value),
9594
- children: /* @__PURE__ */ jsx(ExpandCaret, { expanded })
9595
- })]
11771
+ children: [
11772
+ size !== void 0 && ` (${formatFileSize(size)})`,
11773
+ " ",
11774
+ /* @__PURE__ */ jsx("button", {
11775
+ ref: toggleRef,
11776
+ type: "button",
11777
+ className: "sbk-image__toggle",
11778
+ "aria-label": "image",
11779
+ "aria-expanded": expanded,
11780
+ title: expanded ? "Collapse" : "Expand",
11781
+ onClick: () => setExpanded((value) => !value),
11782
+ children: /* @__PURE__ */ jsx(ExpandCaret, { expanded })
11783
+ })
11784
+ ]
9596
11785
  })]
9597
11786
  }), expanded && /* @__PURE__ */ jsxs("div", {
9598
11787
  className: "sbk-image__frame sbk-hover-actions-host",
@@ -9609,26 +11798,71 @@ function Image({ block }) {
9609
11798
  className: "sbk-image__fallback-text",
9610
11799
  children: slackFile ? "Slack file image unavailable" : block.alt_text
9611
11800
  })
9612
- }), imageUrl && /* @__PURE__ */ jsx(ImageActions, { url: imageUrl })]
11801
+ }), imageUrl && /* @__PURE__ */ jsx(ImageActions, {
11802
+ url: imageUrl,
11803
+ onHide: hide
11804
+ })]
9613
11805
  })]
9614
11806
  });
9615
11807
  }
11808
+ /**
11809
+ * Slack's file-size caption: whole decimal kilobytes, as measured ("(400 kB)" for 400,401 bytes).
11810
+ * Larger files read in megabytes with one decimal; no Builder sample shows that case.
11811
+ */
11812
+ function formatFileSize(bytes) {
11813
+ const kb = bytes / 1e3;
11814
+ return kb < 1e3 ? `${Math.round(kb)} kB` : `${(kb / 1e3).toFixed(1)} MB`;
11815
+ }
11816
+ //#endregion
11817
+ //#region src/elements/emptyState.ts
11818
+ /**
11819
+ * The value Slack reports in `view.state.values` for an input the user hasn't touched. The view
11820
+ * state holds every block with a stateful element, and `@slack/bolt`'s `ViewStateValue` types each
11821
+ * value as `| null`, or as a list for multi-value elements; Block Kit Builder's modal state lists
11822
+ * untouched inputs the same way (a rich text input as just its type).
11823
+ */
11824
+ const EMPTY = {
11825
+ plain_text_input: { value: null },
11826
+ number_input: { value: null },
11827
+ email_text_input: { value: null },
11828
+ url_text_input: { value: null },
11829
+ static_select: { selected_option: null },
11830
+ external_select: { selected_option: null },
11831
+ multi_static_select: { selected_options: [] },
11832
+ multi_external_select: { selected_options: [] },
11833
+ users_select: { selected_user: null },
11834
+ multi_users_select: { selected_users: [] },
11835
+ conversations_select: { selected_conversation: null },
11836
+ multi_conversations_select: { selected_conversations: [] },
11837
+ channels_select: { selected_channel: null },
11838
+ multi_channels_select: { selected_channels: [] },
11839
+ datepicker: { selected_date: null },
11840
+ timepicker: { selected_time: null },
11841
+ datetimepicker: { selected_date_time: null },
11842
+ checkboxes: { selected_options: [] },
11843
+ radio_buttons: { selected_option: null },
11844
+ rich_text_input: {},
11845
+ file_input: { files: [] }
11846
+ };
11847
+ /** Slack's empty state for an element type, or undefined for an element without state. */
11848
+ function emptyState(type) {
11849
+ const empty = EMPTY[type];
11850
+ return empty === void 0 ? void 0 : {
11851
+ type,
11852
+ ...empty
11853
+ };
11854
+ }
9616
11855
  //#endregion
9617
11856
  //#region src/blocks/Input.tsx
9618
- const TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
9619
- "plain_text_input",
9620
- "email_text_input",
9621
- "url_text_input",
9622
- "number_input"
9623
- ]);
9624
- /** Slack's Builder preview synthesizes a "Press 'enter' to submit" hint below a single-line text
9625
- * input whose owning `input` block dispatches on the (default) `on_enter_pressed` trigger, when
9626
- * the block doesn't already carry its own explicit `hint`. It does NOT synthesize anything for
9627
- * `on_character_entered`-only dispatch, or for multiline inputs (Enter inserts a newline there). */
11857
+ /** Slack's Builder preview adds a "Press 'enter' to submit" hint below a single-line plain text
11858
+ * input whose owning `input` block dispatches on the (default) `on_enter_pressed` trigger; with
11859
+ * the block's own `hint`, both share one line. Nothing is added for `on_character_entered`-only
11860
+ * dispatch, for multiline inputs (Enter inserts a newline there), or for number, URL and email
11861
+ * inputs, which dispatch on Enter without it (extra/payloads/dispatch-inputs). */
9628
11862
  function dispatchHint(block) {
9629
- if (block.dispatch_action !== true || block.hint) return false;
11863
+ if (block.dispatch_action !== true) return false;
9630
11864
  const element = block.element;
9631
- if (typeof element.type !== "string" || !TEXT_INPUT_TYPES.has(element.type)) return false;
11865
+ if (element.type !== "plain_text_input") return false;
9632
11866
  if (element.multiline === true) return false;
9633
11867
  return (element.dispatch_action_config?.trigger_actions_on ?? ["on_enter_pressed"]).includes("on_enter_pressed");
9634
11868
  }
@@ -9640,11 +11874,24 @@ function dispatchHint(block) {
9640
11874
  * `__dispatchAction` field, since text inputs need it to pick their triggers.
9641
11875
  */
9642
11876
  function Input({ block, blockId }) {
9643
- const { errors, surface } = useBlockKit();
11877
+ const { errors, surface, seedValue } = useBlockKit();
9644
11878
  const element = {
9645
11879
  ...block.element,
9646
11880
  __dispatchAction: block.dispatch_action === true
9647
11881
  };
11882
+ const elementType = block.element.type;
11883
+ const actionId = block.element.action_id ?? "";
11884
+ useEffect(() => {
11885
+ if (surface === "message") return;
11886
+ const empty = emptyState(elementType);
11887
+ if (empty) seedValue(blockId, actionId, empty);
11888
+ }, [
11889
+ surface,
11890
+ elementType,
11891
+ blockId,
11892
+ actionId,
11893
+ seedValue
11894
+ ]);
9648
11895
  const error = errors[blockId];
9649
11896
  const errorId = useId();
9650
11897
  const showDispatchHint = dispatchHint(block);
@@ -9663,28 +11910,27 @@ function Input({ block, blockId }) {
9663
11910
  className: "sbk-input__element",
9664
11911
  children: /* @__PURE__ */ jsx(InputBlockContext.Provider, {
9665
11912
  value: true,
9666
- children: /* @__PURE__ */ jsx(InputErrorContext.Provider, {
9667
- value: error ? errorId : void 0,
9668
- children: block.dispatch_action === true ? /* @__PURE__ */ jsx(Element, {
9669
- element,
9670
- blockId
9671
- }) : /* @__PURE__ */ jsx(SuppressActions, { children: /* @__PURE__ */ jsx(Element, {
9672
- element,
9673
- blockId
9674
- }) })
11913
+ children: /* @__PURE__ */ jsx(InputOptionalContext.Provider, {
11914
+ value: block.optional === true,
11915
+ children: /* @__PURE__ */ jsx(InputErrorContext.Provider, {
11916
+ value: error ? errorId : void 0,
11917
+ children: block.dispatch_action === true ? /* @__PURE__ */ jsx(Element, {
11918
+ element,
11919
+ blockId
11920
+ }) : /* @__PURE__ */ jsx(SuppressActions, { children: /* @__PURE__ */ jsx(Element, {
11921
+ element,
11922
+ blockId
11923
+ }) })
11924
+ })
9675
11925
  })
9676
11926
  })
9677
11927
  }),
9678
- block.hint && /* @__PURE__ */ jsx("div", {
11928
+ (block.hint || showDispatchHint) && /* @__PURE__ */ jsxs("div", {
9679
11929
  className: "sbk-input__hint",
9680
- children: /* @__PURE__ */ jsx("span", {
11930
+ children: [block.hint && /* @__PURE__ */ jsxs("span", {
9681
11931
  className: "sbk-input__hint-text",
9682
- children: /* @__PURE__ */ jsx(Text, { text: block.hint })
9683
- })
9684
- }),
9685
- showDispatchHint && /* @__PURE__ */ jsx("div", {
9686
- className: "sbk-input__hint",
9687
- children: /* @__PURE__ */ jsxs("span", {
11932
+ children: [/* @__PURE__ */ jsx(Text, { text: block.hint }), showDispatchHint && !block.hint.text.trimEnd().endsWith(".") && "."]
11933
+ }), showDispatchHint && /* @__PURE__ */ jsxs(Fragment, { children: [block.hint && " ", /* @__PURE__ */ jsxs("span", {
9688
11934
  className: "sbk-input__hint-text sbk-input__hint-text--dispatch",
9689
11935
  children: [
9690
11936
  /* @__PURE__ */ jsx("span", {
@@ -9694,7 +11940,7 @@ function Input({ block, blockId }) {
9694
11940
  " ",
9695
11941
  "Press 'enter' to submit"
9696
11942
  ]
9697
- })
11943
+ })] })]
9698
11944
  }),
9699
11945
  error && /* @__PURE__ */ jsx("div", {
9700
11946
  className: "sbk-input__error",
@@ -10110,6 +12356,7 @@ function TaskRow({ task }) {
10110
12356
  const expandable = Boolean(task.details || task.output || sources.length > 0);
10111
12357
  return /* @__PURE__ */ jsxs("li", {
10112
12358
  className: "sbk-plan__task",
12359
+ tabIndex: 0,
10113
12360
  children: [/* @__PURE__ */ jsxs("span", {
10114
12361
  className: "sbk-plan__step",
10115
12362
  "aria-hidden": "true",
@@ -10303,7 +12550,24 @@ function renderSectionChildren(elements, ctx) {
10303
12550
  className: "sbk-rich-text__jumbo-emoji",
10304
12551
  children: elements.filter((el) => el.type === "emoji").map((el, i) => /* @__PURE__ */ jsx(Fragment$1, { children: renderLeaf(el, ctx, 32) }, i))
10305
12552
  });
10306
- return elements.map((el, i) => /* @__PURE__ */ jsx(Fragment$1, { children: renderLeaf(el, ctx, 22, i === 0) }, i));
12553
+ return mergeTextRuns(elements).map((el, i) => /* @__PURE__ */ jsx(Fragment$1, { children: renderLeaf(el, ctx, 22, i === 0) }, i));
12554
+ }
12555
+ /**
12556
+ * Slack writes consecutive `text` elements with the same style as one text node ("item 2: " and
12557
+ * "this is a list item" become "item 2: this is a list item"), so they render, wrap and select as
12558
+ * one run of text.
12559
+ */
12560
+ function mergeTextRuns(elements) {
12561
+ const merged = [];
12562
+ for (const el of elements) {
12563
+ const prev = merged[merged.length - 1];
12564
+ if (el.type === "text" && prev?.type === "text" && JSON.stringify(prev.style ?? {}) === JSON.stringify(el.style ?? {})) merged[merged.length - 1] = {
12565
+ ...prev,
12566
+ text: prev.text + el.text
12567
+ };
12568
+ else merged.push(el);
12569
+ }
12570
+ return merged;
10307
12571
  }
10308
12572
  function renderLeaf(el, ctx, emojiSize, isFirst = false) {
10309
12573
  let content;
@@ -10342,10 +12606,10 @@ function renderLeaf(el, ctx, emojiSize, isFirst = false) {
10342
12606
  }
10343
12607
  case "usergroup": {
10344
12608
  const name = ctx.resolvers.usergroup?.(el.usergroup_id);
10345
- content = name ? /* @__PURE__ */ jsxs(MentionLink, {
12609
+ content = name ? /* @__PURE__ */ jsx(MentionLink, {
10346
12610
  type: "usergroup",
10347
12611
  id: el.usergroup_id,
10348
- children: ["@", name]
12612
+ children: `@${name}`
10349
12613
  }) : /* @__PURE__ */ jsx("span", {
10350
12614
  className: "sbk-mention--loading",
10351
12615
  "aria-label": "Loading user group"
@@ -10354,10 +12618,10 @@ function renderLeaf(el, ctx, emojiSize, isFirst = false) {
10354
12618
  }
10355
12619
  case "channel": {
10356
12620
  const name = ctx.resolvers.channel?.(el.channel_id);
10357
- content = name ? /* @__PURE__ */ jsxs(MentionLink, {
12621
+ content = name ? /* @__PURE__ */ jsx(MentionLink, {
10358
12622
  type: "channel",
10359
12623
  id: el.channel_id,
10360
- children: ["#", name]
12624
+ children: `#${name}`
10361
12625
  }) : /* @__PURE__ */ jsxs("span", {
10362
12626
  className: "sbk-mention--private",
10363
12627
  children: [/* @__PURE__ */ jsx(LockIcon, {}), "Private channel"]
@@ -10365,9 +12629,9 @@ function renderLeaf(el, ctx, emojiSize, isFirst = false) {
10365
12629
  break;
10366
12630
  }
10367
12631
  case "broadcast":
10368
- content = /* @__PURE__ */ jsxs("span", {
12632
+ content = /* @__PURE__ */ jsx("span", {
10369
12633
  className: "sbk-mention--broadcast",
10370
- children: ["@", el.range]
12634
+ children: `@${el.range}`
10371
12635
  });
10372
12636
  break;
10373
12637
  case "date": {
@@ -10560,9 +12824,12 @@ function Section({ block, blockId }) {
10560
12824
  })]
10561
12825
  }), accessory && /* @__PURE__ */ jsx("div", {
10562
12826
  className: `sbk-section__accessory sbk-section__accessory--${accessory.type}`,
10563
- children: /* @__PURE__ */ jsx(Element, {
10564
- element: accessory,
10565
- blockId
12827
+ children: /* @__PURE__ */ jsx(SectionAccessoryContext.Provider, {
12828
+ value: true,
12829
+ children: /* @__PURE__ */ jsx(Element, {
12830
+ element: accessory,
12831
+ blockId
12832
+ })
10566
12833
  })
10567
12834
  })]
10568
12835
  });