@nkootstra/block-kit 0.4.0 → 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
@@ -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 };
@@ -261,6 +262,22 @@ function buildBlockSuggestionPayload({ actionId, blockId, value, container, stat
261
262
  }
262
263
  //#endregion
263
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
+ }
264
281
  function ChevronDownIcon(props) {
265
282
  return /* @__PURE__ */ jsx("svg", {
266
283
  viewBox: "0 0 20 20",
@@ -390,6 +407,38 @@ function ThumbsDownIcon(props) {
390
407
  })
391
408
  });
392
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
+ }
393
442
  function KebabIcon$1(props) {
394
443
  return /* @__PURE__ */ jsxs("svg", {
395
444
  viewBox: "0 0 20 20",
@@ -692,6 +741,38 @@ function EllipsisVerticalIcon(props) {
692
741
  })
693
742
  });
694
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
+ }
695
776
  //#endregion
696
777
  //#region src/validation.ts
697
778
  /**
@@ -927,6 +1008,7 @@ const BlockKitContext = createContext({
927
1008
  errors: {},
928
1009
  state: {},
929
1010
  setValue: noop,
1011
+ seedValue: noop,
930
1012
  dispatch: noop,
931
1013
  identity: {},
932
1014
  setContainer: noop,
@@ -1055,7 +1137,7 @@ function BlockKitProvider(props) {
1055
1137
  const setValue = useCallback((blockId, actionId, value) => {
1056
1138
  const block = { ...stateRef.current[blockId] };
1057
1139
  if (value === void 0) delete block[actionId];
1058
- else block[actionId] = value;
1140
+ else block[actionId] = normalizeEchoes(value);
1059
1141
  const next = {
1060
1142
  ...stateRef.current,
1061
1143
  [blockId]: block
@@ -1064,6 +1146,9 @@ function BlockKitProvider(props) {
1064
1146
  setState(next);
1065
1147
  onStateChange?.(next);
1066
1148
  }, [onStateChange]);
1149
+ const seedValue = useCallback((blockId, actionId, value) => {
1150
+ if (stateRef.current[blockId]?.[actionId] === void 0) setValue(blockId, actionId, value);
1151
+ }, [setValue]);
1067
1152
  const setContainer = useCallback((container) => {
1068
1153
  containerRef.current = container;
1069
1154
  }, []);
@@ -1131,6 +1216,7 @@ function BlockKitProvider(props) {
1131
1216
  timeZone,
1132
1217
  state,
1133
1218
  setValue,
1219
+ seedValue,
1134
1220
  dispatch,
1135
1221
  identity: identity ?? {},
1136
1222
  setContainer,
@@ -1153,6 +1239,7 @@ function BlockKitProvider(props) {
1153
1239
  timeZone,
1154
1240
  state,
1155
1241
  setValue,
1242
+ seedValue,
1156
1243
  dispatch,
1157
1244
  identity,
1158
1245
  setContainer,
@@ -5804,7 +5891,7 @@ function UserMention({ id, name }) {
5804
5891
  type: "user",
5805
5892
  id
5806
5893
  }) : void 0;
5807
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
5894
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
5808
5895
  ref: anchorRef,
5809
5896
  className: "sbk-mention sbk-mention--user",
5810
5897
  role: "button",
@@ -5814,7 +5901,7 @@ function UserMention({ id, name }) {
5814
5901
  "aria-controls": open ? cardId : void 0,
5815
5902
  onClick: () => setOpen((o) => !o),
5816
5903
  onKeyDown,
5817
- children: ["@", name]
5904
+ children: `@${name}`
5818
5905
  }), profile && typeof document !== "undefined" && createPortal(/* @__PURE__ */ jsxs("div", {
5819
5906
  ref: cardRef,
5820
5907
  id: cardId,
@@ -5919,14 +6006,14 @@ function renderNode(node, resolvers, timeZone, emojiSize) {
5919
6006
  return name ? /* @__PURE__ */ jsx(UserMention, {
5920
6007
  id: node.id,
5921
6008
  name
5922
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: ["@", node.id] });
6009
+ }) : /* @__PURE__ */ jsx(Fragment$1, { children: `@${node.id}` });
5923
6010
  }
5924
6011
  case "usergroup": {
5925
6012
  const name = resolvers.usergroup?.(node.id);
5926
- return name ? /* @__PURE__ */ jsxs(MentionLink, {
6013
+ return name ? /* @__PURE__ */ jsx(MentionLink, {
5927
6014
  type: "usergroup",
5928
6015
  id: node.id,
5929
- children: ["@", name]
6016
+ children: `@${name}`
5930
6017
  }) : /* @__PURE__ */ jsx("span", {
5931
6018
  className: "sbk-mention--loading",
5932
6019
  "aria-label": "Loading user group"
@@ -5934,18 +6021,18 @@ function renderNode(node, resolvers, timeZone, emojiSize) {
5934
6021
  }
5935
6022
  case "channel": {
5936
6023
  const name = resolvers.channel?.(node.id) ?? node.label;
5937
- return name ? /* @__PURE__ */ jsxs(MentionLink, {
6024
+ return name ? /* @__PURE__ */ jsx(MentionLink, {
5938
6025
  type: "channel",
5939
6026
  id: node.id,
5940
- children: ["#", name]
6027
+ children: `#${name}`
5941
6028
  }) : /* @__PURE__ */ jsxs("span", {
5942
6029
  className: "sbk-mention--private",
5943
6030
  children: [/* @__PURE__ */ jsx(LockIcon, {}), "Private channel"]
5944
6031
  });
5945
6032
  }
5946
- case "broadcast": return /* @__PURE__ */ jsxs("span", {
6033
+ case "broadcast": return /* @__PURE__ */ jsx("span", {
5947
6034
  className: "sbk-mention--broadcast",
5948
- children: ["@", node.range]
6035
+ children: `@${node.range}`
5949
6036
  });
5950
6037
  case "date": {
5951
6038
  const text = formatSlackDate(node.timestamp, node.format, { timeZone });
@@ -6254,6 +6341,776 @@ function Checkboxes({ element, blockId }) {
6254
6341
  });
6255
6342
  }
6256
6343
  //#endregion
6344
+ //#region src/elements/Popover.tsx
6345
+ /** Space between the anchor and the popover, as in Slack's menus. */
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;
6349
+ /**
6350
+ * The layer a menu or calendar opens in. Slack mounts its popovers at the top of the page, so a
6351
+ * message list, a modal body or a carousel never clips them; this portals to `<body>` the same way
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.
6355
+ * The layer is as wide as the anchor, so a child sized `width: 100%` matches the control it opened
6356
+ * from.
6357
+ */
6358
+ function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, offsetX = 0, onPlaced, children }) {
6359
+ const { theme } = useBlockKit();
6360
+ const layerRef = useRef(null);
6361
+ const [position, setPosition] = useState(null);
6362
+ const dismiss = useRef(onDismiss);
6363
+ useClientLayoutEffect(() => {
6364
+ dismiss.current = onDismiss;
6365
+ });
6366
+ useClientLayoutEffect(() => {
6367
+ function place() {
6368
+ const anchor = anchorRef.current;
6369
+ const layer = layerRef.current;
6370
+ if (!anchor || !layer) return;
6371
+ const a = anchor.getBoundingClientRect();
6372
+ const height = layer.firstElementChild?.offsetHeight ?? 0;
6373
+ const below = a.bottom + gap;
6374
+ const above = a.top - gap - height;
6375
+ const fitsBelow = below + height <= window.innerHeight;
6376
+ const fitsAbove = above >= 0;
6377
+ let top = !fitsBelow && fitsAbove ? above : below;
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
+ }
6389
+ setPosition({
6390
+ top,
6391
+ left,
6392
+ width: a.width
6393
+ });
6394
+ }
6395
+ place();
6396
+ window.addEventListener("scroll", place, true);
6397
+ window.addEventListener("resize", place);
6398
+ const content = layerRef.current?.firstElementChild;
6399
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(place);
6400
+ if (content) resize?.observe(content);
6401
+ return () => {
6402
+ window.removeEventListener("scroll", place, true);
6403
+ window.removeEventListener("resize", place);
6404
+ resize?.disconnect();
6405
+ };
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]);
6418
+ useEffect(() => {
6419
+ function onDocDown(e) {
6420
+ const target = e.target;
6421
+ if (anchorRef.current?.contains(target) || layerRef.current?.contains(target)) return;
6422
+ dismiss.current();
6423
+ }
6424
+ document.addEventListener("mousedown", onDocDown);
6425
+ return () => document.removeEventListener("mousedown", onDocDown);
6426
+ }, [anchorRef]);
6427
+ if (typeof document === "undefined") return null;
6428
+ return createPortal(/* @__PURE__ */ jsx("div", {
6429
+ ref: layerRef,
6430
+ className: "sbk-root sbk-popover",
6431
+ "data-theme": theme,
6432
+ style: position ?? {
6433
+ top: 0,
6434
+ left: 0,
6435
+ visibility: "hidden"
6436
+ },
6437
+ children
6438
+ }), document.body);
6439
+ }
6440
+ //#endregion
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
6257
7114
  //#region src/elements/calendar/Calendar.tsx
6258
7115
  function pad(n) {
6259
7116
  return String(n).padStart(2, "0");
@@ -6293,6 +7150,18 @@ function NavButton({ icon, label, onClick }) {
6293
7150
  })
6294
7151
  });
6295
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
+ }
6296
7165
  /** The day `delta` days after the `YYYY-MM-DD` date, as `YYYY-MM-DD`. */
6297
7166
  function addDays(iso, delta) {
6298
7167
  const d = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
@@ -6437,6 +7306,7 @@ function Calendar({ value, onSelect, onClear }) {
6437
7306
  "aria-pressed": iso === value,
6438
7307
  "aria-current": iso === today ? "date" : void 0,
6439
7308
  "data-date": iso,
7309
+ "aria-label": dayLabel(iso),
6440
7310
  tabIndex: iso === tabStop ? 0 : -1,
6441
7311
  onFocus: () => setFocused(iso),
6442
7312
  onClick: () => onSelect(iso),
@@ -6456,94 +7326,42 @@ function Calendar({ value, onSelect, onClear }) {
6456
7326
  })] });
6457
7327
  }
6458
7328
  //#endregion
6459
- //#region src/elements/dateFormat.ts
6460
- /** "1st", "2nd", "3rd", "11th"… as Slack writes the day in a long date ("January 1st, 2026"). */
6461
- function ordinal$1(day) {
6462
- if (day % 10 === 1 && day !== 11) return `${day}st`;
6463
- if (day % 10 === 2 && day !== 12) return `${day}nd`;
6464
- if (day % 10 === 3 && day !== 13) return `${day}rd`;
6465
- return `${day}th`;
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();
6466
7333
  }
6467
- /** Slack names time zones by their Windows display name (minus the UTC offset), not the IANA id. */
6468
- const TIME_ZONE_LABELS = {
6469
- "Europe/Amsterdam": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6470
- "Europe/Berlin": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6471
- "Europe/Zurich": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6472
- "Europe/Rome": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6473
- "Europe/Stockholm": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6474
- "Europe/Vienna": "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
6475
- "Europe/Brussels": "Brussels, Copenhagen, Madrid, Paris",
6476
- "Europe/Copenhagen": "Brussels, Copenhagen, Madrid, Paris",
6477
- "Europe/Madrid": "Brussels, Copenhagen, Madrid, Paris",
6478
- "Europe/Paris": "Brussels, Copenhagen, Madrid, Paris",
6479
- "Europe/Belgrade": "Belgrade, Bratislava, Budapest, Ljubljana, Prague",
6480
- "Europe/Budapest": "Belgrade, Bratislava, Budapest, Ljubljana, Prague",
6481
- "Europe/Prague": "Belgrade, Bratislava, Budapest, Ljubljana, Prague",
6482
- "Europe/Warsaw": "Sarajevo, Skopje, Warsaw, Zagreb",
6483
- "Europe/Dublin": "Dublin, Edinburgh, Lisbon, London",
6484
- "Europe/Lisbon": "Dublin, Edinburgh, Lisbon, London",
6485
- "Europe/London": "Dublin, Edinburgh, Lisbon, London",
6486
- "Europe/Athens": "Athens, Bucharest",
6487
- "Europe/Bucharest": "Athens, Bucharest",
6488
- "Europe/Helsinki": "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",
6489
- "Europe/Kyiv": "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",
6490
- "Europe/Istanbul": "Istanbul",
6491
- "Europe/Moscow": "Moscow, St. Petersburg",
6492
- "America/New_York": "Eastern Time (US and Canada)",
6493
- "America/Chicago": "Central Time (US and Canada)",
6494
- "America/Denver": "Mountain Time (US and Canada)",
6495
- "America/Phoenix": "Arizona",
6496
- "America/Los_Angeles": "Pacific Time (US and Canada)",
6497
- "America/Anchorage": "Alaska",
6498
- "Pacific/Honolulu": "Hawaii",
6499
- "America/Halifax": "Atlantic Time (Canada)",
6500
- "America/Sao_Paulo": "Brasilia",
6501
- "Asia/Dubai": "Abu Dhabi, Muscat",
6502
- "Asia/Kolkata": "Chennai, Kolkata, Mumbai, New Delhi",
6503
- "Asia/Shanghai": "Beijing, Chongqing, Hong Kong, Urumqi",
6504
- "Asia/Hong_Kong": "Beijing, Chongqing, Hong Kong, Urumqi",
6505
- "Asia/Singapore": "Kuala Lumpur, Singapore",
6506
- "Asia/Tokyo": "Osaka, Sapporo, Tokyo",
6507
- "Australia/Sydney": "Canberra, Melbourne, Sydney",
6508
- "Australia/Melbourne": "Canberra, Melbourne, Sydney",
6509
- "Pacific/Auckland": "Auckland, Wellington"
6510
- };
6511
- /** The label Slack shows under a datetimepicker; unknown zones fall back to their IANA id. */
6512
- function timeZoneLabel(timeZone) {
6513
- return TIME_ZONE_LABELS[timeZone] ?? timeZone;
6514
- }
6515
- /** A timestamp's wall-clock date ("2026-01-01") and time ("11:00") in `timeZone`. */
6516
- function wallClock(ts, timeZone) {
6517
- const parts = new Intl.DateTimeFormat("en-US", {
6518
- timeZone,
6519
- year: "numeric",
6520
- month: "2-digit",
6521
- day: "2-digit",
6522
- hour: "2-digit",
6523
- minute: "2-digit",
6524
- hourCycle: "h23"
6525
- }).formatToParts(/* @__PURE__ */ new Date(ts * 1e3));
6526
- const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
6527
- return {
6528
- date: `${get("year")}-${get("month")}-${get("day")}`,
6529
- time: `${get("hour")}:${get("minute")}`
6530
- };
6531
- }
6532
- /** The timestamp of a wall-clock date and time in `timeZone`. Around a daylight saving change it
6533
- * resolves like JavaScript's `Date`: a time the clocks skip moves forward by the gap, and a time
6534
- * they repeat takes its first occurrence. */
6535
- function fromWallClock(date, time, timeZone) {
6536
- const asUtc = Date.parse(`${date}T${time}:00Z`) / 1e3;
6537
- const offsetAt = (ts) => {
6538
- const wall = wallClock(ts, timeZone);
6539
- return Date.parse(`${wall.date}T${wall.time}:00Z`) / 1e3 - ts;
6540
- };
6541
- const before = asUtc - offsetAt(asUtc - 86400);
6542
- const matches = [before, asUtc - offsetAt(asUtc + 86400)].filter((ts) => {
6543
- const wall = wallClock(ts, timeZone);
6544
- return wall.date === date && wall.time === time;
6545
- });
6546
- return matches.length > 0 ? Math.min(...matches) : before;
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
+ });
6547
7365
  }
6548
7366
  //#endregion
6549
7367
  //#region src/elements/inputBlockContext.ts
@@ -6554,8 +7372,16 @@ function useInInputBlock() {
6554
7372
  }
6555
7373
  /** True while rendering the element of an `input` block marked `optional`, which Slack lets the user empty. */
6556
7374
  const InputOptionalContext = createContext(false);
6557
- function useInOptionalInput() {
6558
- return useContext(InputOptionalContext);
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");
6559
7385
  }
6560
7386
  /** The id of the error shown under an `input` block's element, while it has one. */
6561
7387
  const InputErrorContext = createContext(void 0);
@@ -6571,109 +7397,12 @@ function useInvalidProps() {
6571
7397
  } : {};
6572
7398
  }
6573
7399
  //#endregion
6574
- //#region src/elements/Popover.tsx
6575
- /** Space between the anchor and the popover, as in Slack's menus. */
6576
- const DEFAULT_GAP = 4;
6577
- /** The least space kept between a popover and the window's left and right edges. */
6578
- const EDGE = 8;
6579
- /**
6580
- * The layer a menu or calendar opens in. Slack mounts its popovers at the top of the page, so a
6581
- * message list, a modal body or a carousel never clips them; this portals to `<body>` the same way
6582
- * and keeps the popover pinned to its anchor as the page scrolls or resizes, or its content does.
6583
- * Where keeping its alignment would run it past the window's left or right edge (a narrow phone
6584
- * screen), it shifts just enough to stay inside.
6585
- * The layer is as wide as the anchor, so a child sized `width: 100%` matches the control it opened
6586
- * from.
6587
- */
6588
- function Popover({ anchorRef, onDismiss, gap = DEFAULT_GAP, offsetX = 0, onPlaced, children }) {
6589
- const { theme } = useBlockKit();
6590
- const layerRef = useRef(null);
6591
- const [position, setPosition] = useState(null);
6592
- const dismiss = useRef(onDismiss);
6593
- useClientLayoutEffect(() => {
6594
- dismiss.current = onDismiss;
6595
- });
6596
- useClientLayoutEffect(() => {
6597
- function place() {
6598
- const anchor = anchorRef.current;
6599
- const layer = layerRef.current;
6600
- if (!anchor || !layer) return;
6601
- const a = anchor.getBoundingClientRect();
6602
- const height = layer.firstElementChild?.offsetHeight ?? 0;
6603
- const below = a.bottom + gap;
6604
- const above = a.top - gap - height;
6605
- const fitsBelow = below + height <= window.innerHeight;
6606
- const fitsAbove = above >= 0;
6607
- let top = !fitsBelow && fitsAbove ? above : below;
6608
- if (!fitsBelow && !fitsAbove) top = Math.max(gap, Math.min(below, window.innerHeight - height - gap));
6609
- layer.style.width = `${a.width}px`;
6610
- const content = layer.firstElementChild?.getBoundingClientRect();
6611
- const layerLeft = layer.getBoundingClientRect().left;
6612
- let left = a.left + offsetX;
6613
- if (content) {
6614
- const contentLeft = left + content.left - layerLeft;
6615
- const room = document.documentElement.clientWidth - EDGE;
6616
- const shift = Math.max(EDGE - contentLeft, Math.min(0, room - (contentLeft + content.width)));
6617
- left += shift;
6618
- }
6619
- setPosition({
6620
- top,
6621
- left,
6622
- width: a.width
6623
- });
6624
- }
6625
- place();
6626
- window.addEventListener("scroll", place, true);
6627
- window.addEventListener("resize", place);
6628
- const content = layerRef.current?.firstElementChild;
6629
- const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(place);
6630
- if (content) resize?.observe(content);
6631
- return () => {
6632
- window.removeEventListener("scroll", place, true);
6633
- window.removeEventListener("resize", place);
6634
- resize?.disconnect();
6635
- };
6636
- }, [
6637
- anchorRef,
6638
- gap,
6639
- offsetX
6640
- ]);
6641
- const placed = position !== null;
6642
- const notified = useRef(false);
6643
- useEffect(() => {
6644
- if (!placed || notified.current) return;
6645
- notified.current = true;
6646
- onPlaced?.();
6647
- }, [placed]);
6648
- useEffect(() => {
6649
- function onDocDown(e) {
6650
- const target = e.target;
6651
- if (anchorRef.current?.contains(target) || layerRef.current?.contains(target)) return;
6652
- dismiss.current();
6653
- }
6654
- document.addEventListener("mousedown", onDocDown);
6655
- return () => document.removeEventListener("mousedown", onDocDown);
6656
- }, [anchorRef]);
6657
- if (typeof document === "undefined") return null;
6658
- return createPortal(/* @__PURE__ */ jsx("div", {
6659
- ref: layerRef,
6660
- className: "sbk-root sbk-popover",
6661
- "data-theme": theme,
6662
- style: position ?? {
6663
- top: 0,
6664
- left: 0,
6665
- visibility: "hidden"
6666
- },
6667
- children
6668
- }), document.body);
6669
- }
6670
- //#endregion
6671
7400
  //#region src/elements/DatePicker.tsx
6672
7401
  /**
6673
7402
  * Formats `YYYY-MM-DD` the way Slack's closed datepicker control shows it: "04/28/1990" in an
6674
7403
  * actions block or section accessory, and "April 28th, 1990" in an `input` block.
6675
7404
  */
6676
- function formatDate$1(date, long) {
7405
+ function formatDate(date, long) {
6677
7406
  const [y = 0, m = 1, d = 1] = date.split("-").map(Number);
6678
7407
  if (long) return `${new Date(Date.UTC(y, m - 1, 1)).toLocaleString("en-US", {
6679
7408
  month: "long",
@@ -6682,9 +7411,10 @@ function formatDate$1(date, long) {
6682
7411
  return `${String(m).padStart(2, "0")}/${String(d).padStart(2, "0")}/${y}`;
6683
7412
  }
6684
7413
  function DatePicker({ element, blockId }) {
6685
- const { setValue, dispatch } = useBlockKit();
7414
+ const { setValue, dispatch, surface } = useBlockKit();
7415
+ const medium = surface !== "message";
6686
7416
  const inInputBlock = useInInputBlock();
6687
- const clearable = useInOptionalInput();
7417
+ const clearable = useInClearableInput();
6688
7418
  const { ask, dialog } = useConfirm(element.confirm);
6689
7419
  const [date, setDate] = useState(element.initial_date);
6690
7420
  const [open, setOpen] = useState(false);
@@ -6695,8 +7425,9 @@ function DatePicker({ element, blockId }) {
6695
7425
  const focusCalendar = useRef(false);
6696
7426
  const popupRef = useRef(null);
6697
7427
  const actionId = element.action_id ?? "";
7428
+ const [text, setText] = useState(null);
6698
7429
  function focusDay() {
6699
- popupRef.current?.querySelector(".sbk-calendar__grid [tabindex='0']")?.focus();
7430
+ focusCalendarDay(popupRef.current);
6700
7431
  }
6701
7432
  function close() {
6702
7433
  setOpen(false);
@@ -6713,8 +7444,12 @@ function DatePicker({ element, blockId }) {
6713
7444
  }, []);
6714
7445
  async function pick(next) {
6715
7446
  if (!await ask()) return;
6716
- setDate(next);
6717
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);
6718
7453
  setValue(blockId, actionId, {
6719
7454
  type: "datepicker",
6720
7455
  selected_date: next
@@ -6727,6 +7462,17 @@ function DatePicker({ element, blockId }) {
6727
7462
  ...element.initial_date !== void 0 ? { initial_date: element.initial_date } : {}
6728
7463
  });
6729
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
+ }
6730
7476
  async function clear() {
6731
7477
  if (!await ask()) return;
6732
7478
  setDate(void 0);
@@ -6744,7 +7490,7 @@ function DatePicker({ element, blockId }) {
6744
7490
  });
6745
7491
  }
6746
7492
  return /* @__PURE__ */ jsxs("div", {
6747
- className: "sbk-datepicker",
7493
+ className: `sbk-datepicker${medium ? " sbk-datepicker--medium" : ""}`,
6748
7494
  ref: rootRef,
6749
7495
  children: [
6750
7496
  /* @__PURE__ */ jsxs("div", {
@@ -6753,17 +7499,26 @@ function DatePicker({ element, blockId }) {
6753
7499
  /* @__PURE__ */ jsx(CalendarIcon, { className: "sbk-datepicker__icon" }),
6754
7500
  /* @__PURE__ */ jsx("input", {
6755
7501
  className: "sbk-datepicker__input",
6756
- readOnly: true,
6757
- value: date ? formatDate$1(date, inInputBlock) : "",
7502
+ type: "text",
7503
+ autoComplete: "off",
7504
+ spellCheck: false,
7505
+ value: text ?? (date ? formatDate(date, inInputBlock) : ""),
6758
7506
  placeholder: element.placeholder?.text ?? "Select a date",
6759
- "aria-haspopup": "dialog",
6760
- "aria-expanded": open,
7507
+ "aria-label": "Date",
6761
7508
  ...invalid,
6762
7509
  ref: inputRef,
7510
+ onChange: (e) => {
7511
+ setText(e.target.value);
7512
+ setOpen(true);
7513
+ },
6763
7514
  onFocus: () => {
6764
7515
  if (quietFocus.current) quietFocus.current = false;
6765
7516
  else setOpen(true);
6766
7517
  },
7518
+ onBlur: async () => {
7519
+ await takeTyped();
7520
+ setText(null);
7521
+ },
6767
7522
  onMouseDown: () => {
6768
7523
  focusCalendar.current = true;
6769
7524
  },
@@ -6773,7 +7528,14 @@ function DatePicker({ element, blockId }) {
6773
7528
  setOpen(true);
6774
7529
  },
6775
7530
  onKeyDown: (e) => {
6776
- if (e.key === "Escape") setOpen(false);
7531
+ if (e.key === "Escape") {
7532
+ setText(null);
7533
+ setOpen(false);
7534
+ }
7535
+ if (e.key === "Enter") {
7536
+ e.preventDefault();
7537
+ takeTyped();
7538
+ }
6777
7539
  if (e.key === "ArrowDown") {
6778
7540
  e.preventDefault();
6779
7541
  if (open) focusDay();
@@ -6784,162 +7546,589 @@ function DatePicker({ element, blockId }) {
6784
7546
  }
6785
7547
  }
6786
7548
  }),
6787
- /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datepicker__chevron" })
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
+ })
6788
7560
  ]
6789
7561
  }),
6790
- open && /* @__PURE__ */ jsx(Popover, {
7562
+ open && /* @__PURE__ */ jsx(CalendarPopover, {
6791
7563
  anchorRef: rootRef,
6792
- gap: -4,
7564
+ popupRef,
7565
+ offsetX: medium ? -4 : void 0,
7566
+ value: date,
7567
+ onSelect: pick,
7568
+ onClear: clearable ? clear : void 0,
6793
7569
  onDismiss: () => setOpen(false),
6794
- onPlaced: () => {
6795
- if (focusCalendar.current) focusDay();
7570
+ onEscape: close,
7571
+ focusOnOpen: () => {
7572
+ const focus = focusCalendar.current;
6796
7573
  focusCalendar.current = false;
6797
- },
6798
- children: /* @__PURE__ */ jsx("div", {
6799
- className: "sbk-datepicker__popup",
6800
- ref: popupRef,
6801
- role: "dialog",
6802
- "aria-label": "Choose a date",
6803
- onKeyDown: (e) => {
6804
- if (e.key !== "Escape") return;
6805
- e.preventDefault();
6806
- close();
6807
- },
6808
- children: /* @__PURE__ */ jsx(Calendar, {
6809
- value: date,
6810
- onSelect: pick,
6811
- onClear: clearable ? clear : void 0
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`);
7782
+ /**
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.
7786
+ */
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")}`;
7799
+ }
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";
7810
+ const { ask, dialog } = useConfirm(element.confirm);
7811
+ const [time, setTime] = useState(element.initial_time);
7812
+ const rootRef = useRef(null);
7813
+ const listRef = useRef(null);
7814
+ const invalid = useInvalidProps();
7815
+ const inInputBlock = useInInputBlock();
7816
+ const clearable = useInClearableInput();
7817
+ const actionId = element.action_id ?? "";
7818
+ useEffect(() => {
7819
+ if (time) setValue(blockId, actionId, {
7820
+ type: "timepicker",
7821
+ selected_time: time
7822
+ });
7823
+ }, []);
7824
+ async function pick(next) {
7825
+ combo.setOpen(false);
7826
+ if (!await ask()) return;
7827
+ setTime(next);
7828
+ setValue(blockId, actionId, {
7829
+ type: "timepicker",
7830
+ selected_time: next
7831
+ });
7832
+ dispatch({
7833
+ type: "timepicker",
7834
+ action_id: actionId,
7835
+ block_id: blockId,
7836
+ selected_time: next,
7837
+ ...element.initial_time !== void 0 ? { initial_time: element.initial_time } : {}
7838
+ });
7839
+ }
7840
+ async function clear() {
7841
+ combo.setOpen(false);
7842
+ if (!await ask()) return;
7843
+ setTime(void 0);
7844
+ setQuery("");
7845
+ setValue(blockId, actionId, {
7846
+ type: "timepicker",
7847
+ selected_time: null
7848
+ });
7849
+ dispatch({
7850
+ type: "timepicker",
7851
+ action_id: actionId,
7852
+ block_id: blockId,
7853
+ selected_time: null,
7854
+ ...element.initial_time !== void 0 ? { initial_time: element.initial_time } : {}
7855
+ });
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;
7891
+ return /* @__PURE__ */ jsxs("div", {
7892
+ className: `sbk-timepicker${medium ? " sbk-timepicker--medium" : ""}`,
7893
+ ref: rootRef,
7894
+ children: [
7895
+ /* @__PURE__ */ jsxs("label", {
7896
+ className: "sbk-timepicker__control",
7897
+ children: [
7898
+ /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-timepicker__icon" }),
7899
+ /* @__PURE__ */ jsx("input", {
7900
+ ...input,
7901
+ ...invalid,
7902
+ className: `sbk-timepicker__input${overlay ? " sbk-timepicker__input--behind" : ""}`
7903
+ }),
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
+ })
6812
7912
  })
7913
+ ]
7914
+ }),
7915
+ combo.open && /* @__PURE__ */ jsx(Popover, {
7916
+ anchorRef: rootRef,
7917
+ onDismiss: () => combo.setOpen(false),
7918
+ offsetX: -12,
7919
+ gap: -4,
7920
+ children: /* @__PURE__ */ jsx("div", {
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))
6813
7934
  })
6814
7935
  }),
7936
+ element.timezone && /* @__PURE__ */ jsx("div", {
7937
+ className: "sbk-timepicker__hint",
7938
+ children: `Time zone: ${timeZoneLabel(element.timezone)}`
7939
+ }),
6815
7940
  dialog
6816
7941
  ]
6817
7942
  });
6818
7943
  }
6819
7944
  //#endregion
6820
7945
  //#region src/elements/DateTimePicker.tsx
6821
- /** Slack's Builder splits a datetimepicker's closed control into a date box ("January 1st,
6822
- * 2026") and a separate time box ("11:00 AM"), rather than one combined string. */
6823
- function formatDate(ts, timeZone) {
6824
- const d = /* @__PURE__ */ new Date(ts * 1e3);
6825
- 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", {
6826
7953
  month: "long",
6827
- day: "numeric",
6828
- year: "numeric",
6829
- timeZone
6830
- }).formatToParts(d);
6831
- const month = parts.find((p) => p.type === "month")?.value ?? "";
6832
- const day = Number(parts.find((p) => p.type === "day")?.value ?? "1");
6833
- const year = parts.find((p) => p.type === "year")?.value ?? "";
6834
- return `${month} ${ordinal$1(day)}, ${year}`;
6835
- }
6836
- function formatTime$1(ts, timeZone) {
6837
- return (/* @__PURE__ */ new Date(ts * 1e3)).toLocaleString("en-US", {
6838
- hour: "numeric",
6839
- minute: "2-digit",
6840
- timeZone
6841
- });
7954
+ timeZone: "UTC"
7955
+ })} ${ordinal$1(d.getUTCDate())}, ${d.getUTCFullYear()}`;
6842
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
+ */
6843
7963
  function DateTimePicker({ element, blockId }) {
6844
7964
  const { setValue, dispatch, timeZone } = useBlockKit();
6845
7965
  const { ask, dialog } = useConfirm(element.confirm);
6846
- const [value, setValueState] = useState(element.initial_date_time);
6847
- const [open, setOpen] = useState(false);
6848
7966
  const zone = timeZone ?? "UTC";
6849
- const initial = element.initial_date_time !== void 0 ? wallClock(element.initial_date_time, zone) : void 0;
6850
- const [draftDate, setDraftDate] = useState(initial?.date);
6851
- const [draftTime, setDraftTime] = useState(initial?.time ?? "12:00");
6852
- const rootRef = useRef(null);
6853
- 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);
6854
7974
  const actionId = element.action_id ?? "";
7975
+ const now = () => wallClock(Math.floor(Date.now() / 1e3), zone);
6855
7976
  useEffect(() => {
6856
- if (value !== void 0) setValue(blockId, actionId, {
7977
+ if (element.initial_date_time !== void 0) setValue(blockId, actionId, {
6857
7978
  type: "datetimepicker",
6858
- selected_date_time: value
7979
+ selected_date_time: element.initial_date_time
6859
7980
  });
6860
7981
  }, []);
6861
- async function apply() {
6862
- if (!draftDate) return;
7982
+ async function change(next) {
6863
7983
  if (!await ask()) return;
6864
- const ts = fromWallClock(draftDate, draftTime, zone);
6865
- setValueState(ts);
6866
- setOpen(false);
7984
+ setValueState(next);
7985
+ const selected = next ? fromWallClock(next.date, next.time, zone) : null;
6867
7986
  setValue(blockId, actionId, {
6868
7987
  type: "datetimepicker",
6869
- selected_date_time: ts
7988
+ selected_date_time: selected
6870
7989
  });
6871
7990
  dispatch({
6872
7991
  type: "datetimepicker",
6873
7992
  action_id: actionId,
6874
7993
  block_id: blockId,
6875
- 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
6876
8014
  });
6877
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;
6878
8043
  return /* @__PURE__ */ jsxs("div", {
6879
8044
  className: "sbk-datetimepicker",
6880
- ref: rootRef,
6881
8045
  children: [
6882
8046
  /* @__PURE__ */ jsxs("div", {
6883
8047
  className: "sbk-datetimepicker__row",
6884
8048
  children: [/* @__PURE__ */ jsx("div", {
6885
8049
  className: "sbk-datetimepicker__column",
6886
8050
  children: /* @__PURE__ */ jsxs("button", {
6887
- ref: focusRef,
8051
+ ref: dateRef,
6888
8052
  type: "button",
6889
8053
  className: "sbk-datetimepicker__control sbk-datetimepicker__control--date",
6890
- onClick: () => setOpen((o) => !o),
8054
+ onClick: () => {
8055
+ if (calendarOpen) {
8056
+ focusCalendarDay(popupRef.current);
8057
+ return;
8058
+ }
8059
+ focusCalendar.current = true;
8060
+ setCalendarOpen(true);
8061
+ },
6891
8062
  children: [
6892
8063
  /* @__PURE__ */ jsx(CalendarIcon, { className: "sbk-datetimepicker__icon" }),
6893
8064
  /* @__PURE__ */ jsx("span", {
6894
- className: value !== void 0 ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
6895
- 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"
6896
8067
  }),
6897
8068
  /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datetimepicker__chevron" })
6898
8069
  ]
6899
8070
  })
6900
8071
  }), /* @__PURE__ */ jsxs("div", {
6901
8072
  className: "sbk-datetimepicker__column",
6902
- children: [/* @__PURE__ */ jsxs("button", {
6903
- type: "button",
8073
+ children: [/* @__PURE__ */ jsxs("label", {
8074
+ ref: timeFieldRef,
6904
8075
  className: "sbk-datetimepicker__control sbk-datetimepicker__control--time",
6905
- onClick: () => setOpen((o) => !o),
6906
8076
  children: [
6907
8077
  /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-datetimepicker__icon" }),
6908
- /* @__PURE__ */ jsx("span", {
6909
- className: value !== void 0 ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
6910
- 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" : ""}`
6911
8081
  }),
6912
- /* @__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
+ })
6913
8091
  ]
6914
- }), /* @__PURE__ */ jsxs("p", {
8092
+ }), /* @__PURE__ */ jsx("p", {
6915
8093
  className: "sbk-datetimepicker__timezone",
6916
- children: ["Time zone: ", timeZoneLabel(zone)]
8094
+ children: `Time zone: ${timeZoneLabel(zone)}`
6917
8095
  })]
6918
8096
  })]
6919
8097
  }),
6920
- open && /* @__PURE__ */ jsx(Popover, {
6921
- anchorRef: rootRef,
6922
- onDismiss: () => setOpen(false),
6923
- children: /* @__PURE__ */ jsxs("div", {
6924
- className: "sbk-datetimepicker__popup",
6925
- children: [/* @__PURE__ */ jsx(Calendar, {
6926
- value: draftDate,
6927
- onSelect: setDraftDate
6928
- }), /* @__PURE__ */ jsxs("div", {
6929
- className: "sbk-datetimepicker__time-row",
6930
- children: [/* @__PURE__ */ jsx("input", {
6931
- type: "time",
6932
- className: "sbk-datetimepicker__time-input",
6933
- value: draftTime,
6934
- onChange: (e) => setDraftTime(e.target.value)
6935
- }), /* @__PURE__ */ jsx("button", {
6936
- type: "button",
6937
- className: "sbk-datetimepicker__apply",
6938
- onClick: apply,
6939
- disabled: !draftDate,
6940
- children: "Apply"
6941
- })]
6942
- })]
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))
6943
8132
  })
6944
8133
  }),
6945
8134
  dialog
@@ -6948,20 +8137,23 @@ function DateTimePicker({ element, blockId }) {
6948
8137
  }
6949
8138
  //#endregion
6950
8139
  //#region src/elements/FeedbackButtons.tsx
6951
- /** Positive/negative feedback buttons, as seen on AI agent messages. Only one of the two can be
6952
- * 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
+ */
6953
8146
  function FeedbackButtons({ element, blockId }) {
6954
- const { setValue, dispatch } = useBlockKit();
8147
+ const { dispatch } = useBlockKit();
6955
8148
  const [picked, setPicked] = useState();
8149
+ const refs = {
8150
+ positive: useRef(null),
8151
+ negative: useRef(null)
8152
+ };
6956
8153
  const actionId = element.action_id ?? "";
6957
8154
  function choose(kind) {
6958
- const next = picked === kind ? void 0 : kind;
6959
- setPicked(next);
8155
+ setPicked(kind);
6960
8156
  const button = kind === "positive" ? element.positive_button : element.negative_button;
6961
- setValue(blockId, actionId, next ? {
6962
- type: "feedback_buttons",
6963
- value: button.value
6964
- } : void 0);
6965
8157
  dispatch({
6966
8158
  type: "feedback_buttons",
6967
8159
  action_id: actionId,
@@ -6973,29 +8165,44 @@ function FeedbackButtons({ element, blockId }) {
6973
8165
  }
6974
8166
  });
6975
8167
  }
6976
- return /* @__PURE__ */ jsxs("div", {
6977
- className: "sbk-feedback-buttons",
6978
- children: [/* @__PURE__ */ jsx(Tooltip, {
6979
- label: element.positive_button.text.text,
6980
- children: /* @__PURE__ */ jsx("button", {
6981
- type: "button",
6982
- className: `sbk-icon-button${picked === "positive" ? " sbk-icon-button--active" : ""}`,
6983
- onClick: () => choose("positive"),
6984
- "aria-label": element.positive_button.accessibility_label ?? element.positive_button.text.text,
6985
- "aria-pressed": picked === "positive",
6986
- children: /* @__PURE__ */ jsx(ThumbsUpIcon, {})
6987
- })
6988
- }), /* @__PURE__ */ jsx(Tooltip, {
6989
- 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,
6990
8187
  children: /* @__PURE__ */ jsx("button", {
8188
+ ref: refs[kind],
6991
8189
  type: "button",
6992
- className: `sbk-icon-button${picked === "negative" ? " sbk-icon-button--active" : ""}`,
6993
- onClick: () => choose("negative"),
6994
- "aria-label": element.negative_button.accessibility_label ?? element.negative_button.text.text,
6995
- "aria-pressed": picked === "negative",
6996
- 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, {})
6997
8198
  })
6998
- })]
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")]
6999
8206
  });
7000
8207
  }
7001
8208
  //#endregion
@@ -7080,7 +8287,9 @@ function IconButton({ element, blockId }) {
7080
8287
  type: "icon_button",
7081
8288
  action_id: element.action_id ?? "",
7082
8289
  block_id: blockId,
7083
- ...element.value !== void 0 ? { value: element.value } : {}
8290
+ ...element.value !== void 0 ? { value: element.value } : {},
8291
+ icon: element.icon,
8292
+ text: element.text
7084
8293
  });
7085
8294
  }
7086
8295
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tooltip, {
@@ -7096,85 +8305,18 @@ function IconButton({ element, blockId }) {
7096
8305
  }
7097
8306
  //#endregion
7098
8307
  //#region src/elements/ImageElement.tsx
8308
+ /** An image element; a `slack_file` one shows through `resolvers.slackFile`, like the image block. */
7099
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;
7100
8313
  return /* @__PURE__ */ jsx("img", {
7101
8314
  className: "sbk-image-element",
7102
- src: "image_url" in element ? element.image_url : void 0,
8315
+ src,
7103
8316
  alt: element.alt_text
7104
8317
  });
7105
8318
  }
7106
8319
  //#endregion
7107
- //#region src/elements/useMenuNavigation.ts
7108
- /**
7109
- * Slack's menus and option lists keep one "active" row that follows both the pointer and the
7110
- * arrow keys (`c-menu_item--highlighted` / `c-select_options_list__option--active`), so hover and
7111
- * keyboard highlight share one style. Returns the active index plus handlers for the list.
7112
- */
7113
- function useMenuNavigation({ open, count, initialIndex = -1, onChoose, onClose, onOpen, listRef }) {
7114
- const [active, setActive] = useState(-1);
7115
- useEffect(() => {
7116
- setActive(open ? initialIndex : -1);
7117
- }, [open]);
7118
- useEffect(() => {
7119
- if (active >= count) setActive(count - 1);
7120
- }, [active, count]);
7121
- useEffect(() => {
7122
- (listRef?.current?.querySelector("[data-active]"))?.scrollIntoView?.({ block: "nearest" });
7123
- }, [active, listRef]);
7124
- function onKeyDown(e) {
7125
- if (!open) {
7126
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
7127
- e.preventDefault();
7128
- onOpen?.();
7129
- }
7130
- return;
7131
- }
7132
- switch (e.key) {
7133
- case "ArrowDown":
7134
- e.preventDefault();
7135
- setActive((i) => count === 0 ? -1 : (i + 1) % count);
7136
- break;
7137
- case "ArrowUp":
7138
- e.preventDefault();
7139
- setActive((i) => count === 0 ? -1 : i <= 0 ? count - 1 : i - 1);
7140
- break;
7141
- case "Home":
7142
- e.preventDefault();
7143
- setActive(count > 0 ? 0 : -1);
7144
- break;
7145
- case "End":
7146
- e.preventDefault();
7147
- setActive(count - 1);
7148
- break;
7149
- case "Enter":
7150
- if (active >= 0 && active < count) {
7151
- e.preventDefault();
7152
- onChoose(active);
7153
- }
7154
- break;
7155
- case "Escape":
7156
- e.preventDefault();
7157
- onClose();
7158
- }
7159
- }
7160
- /** Props for the row at `index`: pointer movement moves the highlight, like Slack's lists. */
7161
- function itemProps(index) {
7162
- return {
7163
- onMouseEnter: () => setActive(index),
7164
- onMouseMove: () => {
7165
- if (active !== index) setActive(index);
7166
- },
7167
- "data-active": active === index || void 0
7168
- };
7169
- }
7170
- return {
7171
- active,
7172
- setActive,
7173
- onKeyDown,
7174
- itemProps
7175
- };
7176
- }
7177
- //#endregion
7178
8320
  //#region src/elements/Overflow.tsx
7179
8321
  function Overflow({ element, blockId }) {
7180
8322
  const { dispatch } = useBlockKit();
@@ -7193,7 +8335,10 @@ function Overflow({ element, blockId }) {
7193
8335
  type: "overflow",
7194
8336
  action_id: actionId,
7195
8337
  block_id: blockId,
7196
- selected_option: option
8338
+ selected_option: {
8339
+ text: option.text,
8340
+ ...option.value !== void 0 ? { value: option.value } : {}
8341
+ }
7197
8342
  });
7198
8343
  if (option.url) window.open(option.url, "_blank", "noopener,noreferrer");
7199
8344
  }
@@ -7242,7 +8387,10 @@ function Overflow({ element, blockId }) {
7242
8387
  className: "sbk-overflow__option",
7243
8388
  onClick: () => choose(option),
7244
8389
  ...nav.itemProps(i),
7245
- children: option.text.text
8390
+ children: /* @__PURE__ */ jsx("span", {
8391
+ className: "sbk-overflow__label",
8392
+ children: option.text.text
8393
+ })
7246
8394
  }, option.value ?? i))
7247
8395
  })
7248
8396
  }),
@@ -7276,7 +8424,8 @@ function RadioButtons({ element, blockId }) {
7276
8424
  type: "radio_buttons",
7277
8425
  action_id: actionId,
7278
8426
  block_id: blockId,
7279
- selected_option: option
8427
+ selected_option: option,
8428
+ ...element.initial_option !== void 0 ? { initial_option: element.initial_option } : {}
7280
8429
  });
7281
8430
  };
7282
8431
  return /* @__PURE__ */ jsxs("div", {
@@ -7477,143 +8626,73 @@ function RichTextInput({ element, blockId }) {
7477
8626
  rich_text_value: element.initial_value
7478
8627
  });
7479
8628
  }, []);
7480
- function onInput() {
7481
- const text = ref.current?.innerText ?? "";
7482
- setEmpty(text.trim() === "");
7483
- const richText = toRichText(text);
7484
- setValue(blockId, actionId, {
7485
- type: "rich_text_input",
7486
- rich_text_value: richText
7487
- });
7488
- dispatch({
7489
- type: "rich_text_input",
7490
- action_id: actionId,
7491
- block_id: blockId,
7492
- rich_text_value: richText
7493
- });
7494
- }
7495
- const placeholder = element.placeholder?.text;
7496
- return /* @__PURE__ */ jsxs("div", {
7497
- className: "sbk-rich-text-input",
7498
- children: [
7499
- formatting && /* @__PURE__ */ jsx(RichTextToolbar, {}),
7500
- /* @__PURE__ */ jsxs("div", {
7501
- className: "sbk-rich-text-input__body",
7502
- children: [/* @__PURE__ */ jsx("div", {
7503
- ref,
7504
- className: "sbk-rich-text-input__editor",
7505
- contentEditable: true,
7506
- suppressContentEditableWarning: true,
7507
- role: "textbox",
7508
- "aria-multiline": "true",
7509
- ...invalid,
7510
- "aria-label": placeholder ?? actionId,
7511
- onInput,
7512
- style: {
7513
- minHeight: element.min_lines ? `${element.min_lines * 22}px` : void 0,
7514
- maxHeight: element.max_lines ? `${element.max_lines * 22}px` : void 0
7515
- },
7516
- children: initialText
7517
- }), placeholder && empty && /* @__PURE__ */ jsx("div", {
7518
- className: "sbk-rich-text-input__placeholder",
7519
- "aria-hidden": "true",
7520
- children: placeholder
7521
- })]
7522
- }),
7523
- /* @__PURE__ */ jsx(RichTextComposerActions, {
7524
- formatting,
7525
- onToggleFormatting: () => setFormatting((shown) => !shown)
7526
- })
7527
- ]
7528
- });
7529
- }
7530
- //#endregion
7531
- //#region src/elements/useCombobox.ts
7532
- /**
7533
- * The typeable trigger Slack's select and time pickers share (`c-select_input`): a text input with
7534
- * role="combobox" that shows the chosen value while closed, and the query while its list is open.
7535
- * Typing filters the list and highlights its first row; the arrow keys move the highlight, Enter
7536
- * picks it, Escape and Tab close the list and put the chosen value back.
7537
- */
7538
- function useCombobox({ query, onQueryChange, count, initialIndex, onChoose, onSubmitQuery, listRef, returnFocusRef, open: controlledOpen, onOpenChange }) {
7539
- const [ownOpen, setOwnOpen] = useState(false);
7540
- const open = controlledOpen ?? ownOpen;
7541
- const setOpenState = onOpenChange ?? setOwnOpen;
7542
- const inputRef = useRef(null);
7543
- const id = useId();
7544
- const listId = `${id}-list`;
7545
- const optionId = (index) => `${id}-option-${index}`;
7546
- function setOpen(next) {
7547
- setOpenState(next);
7548
- if (!next) onQueryChange("");
7549
- }
7550
- const nav = useMenuNavigation({
7551
- open,
7552
- count,
7553
- initialIndex,
7554
- onChoose,
7555
- onClose: () => {
7556
- setOpen(false);
7557
- (returnFocusRef ?? inputRef).current?.focus();
7558
- },
7559
- onOpen: () => setOpenState(true),
7560
- listRef
7561
- });
7562
- /** Keys for the input, or for a container around a button trigger and its list. */
7563
- function onKeyDown(e) {
7564
- if (e.key === "Tab") {
7565
- if (open) setOpen(false);
7566
- return;
7567
- }
7568
- if (open && e.key === "Enter" && nav.active < 0 && query.trim()) {
7569
- e.preventDefault();
7570
- onSubmitQuery?.(query.trim());
7571
- return;
7572
- }
7573
- nav.onKeyDown(e);
7574
- }
7575
- /** Props for the input. `display` is the chosen value, shown while the list is closed. */
7576
- function inputProps(display, placeholder) {
7577
- return {
7578
- ref: inputRef,
7579
- type: "text",
7580
- role: "combobox",
7581
- autoComplete: "off",
7582
- size: 1,
7583
- spellCheck: false,
7584
- "aria-autocomplete": "list",
7585
- "aria-expanded": open,
7586
- "aria-controls": open ? listId : void 0,
7587
- "aria-activedescendant": open && nav.active >= 0 ? optionId(nav.active) : void 0,
7588
- "aria-label": placeholder,
7589
- placeholder: open ? display ?? placeholder : placeholder,
7590
- value: open ? query : display ?? "",
7591
- onClick: () => setOpenState(true),
7592
- onChange: (e) => {
7593
- onQueryChange(e.target.value);
7594
- setOpenState(true);
7595
- nav.setActive(0);
7596
- },
7597
- onKeyDown
7598
- };
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
+ });
7599
8642
  }
7600
- /** Props for the row at `index`, tying it to the input's aria-activedescendant. */
7601
- function optionProps(index) {
7602
- return {
7603
- id: optionId(index),
7604
- ...nav.itemProps(index)
7605
- };
8643
+ function onInput() {
8644
+ const text = ref.current?.innerText ?? "";
8645
+ setEmpty(text.trim() === "");
8646
+ setValue(blockId, actionId, {
8647
+ type: "rich_text_input",
8648
+ rich_text_value: toRichText(text)
8649
+ });
8650
+ if (dispatchEnabled && triggers.includes("on_character_entered")) {
8651
+ clearTimeout(typing.current);
8652
+ typing.current = setTimeout(fire, 300);
8653
+ }
7606
8654
  }
7607
- return {
7608
- open,
7609
- setOpen,
7610
- onKeyDown,
7611
- listId,
7612
- inputRef,
7613
- inputProps,
7614
- optionProps,
7615
- active: nav.active
7616
- };
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();
8660
+ }
8661
+ const placeholder = element.placeholder?.text;
8662
+ return /* @__PURE__ */ jsxs("div", {
8663
+ className: "sbk-rich-text-input",
8664
+ children: [
8665
+ formatting && /* @__PURE__ */ jsx(RichTextToolbar, {}),
8666
+ /* @__PURE__ */ jsxs("div", {
8667
+ className: "sbk-rich-text-input__body",
8668
+ children: [/* @__PURE__ */ jsx("div", {
8669
+ ref,
8670
+ className: "sbk-rich-text-input__editor",
8671
+ contentEditable: true,
8672
+ suppressContentEditableWarning: true,
8673
+ role: "textbox",
8674
+ "aria-multiline": "true",
8675
+ ...invalid,
8676
+ "aria-label": placeholder ?? actionId,
8677
+ onInput,
8678
+ onKeyDown,
8679
+ style: {
8680
+ minHeight: element.min_lines ? `${element.min_lines * 22}px` : void 0,
8681
+ maxHeight: element.max_lines ? `${element.max_lines * 22}px` : void 0
8682
+ },
8683
+ children: initialText
8684
+ }), placeholder && empty && /* @__PURE__ */ jsx("div", {
8685
+ className: "sbk-rich-text-input__placeholder",
8686
+ "aria-hidden": "true",
8687
+ children: placeholder
8688
+ })]
8689
+ }),
8690
+ /* @__PURE__ */ jsx(RichTextComposerActions, {
8691
+ formatting,
8692
+ onToggleFormatting: () => setFormatting((shown) => !shown)
8693
+ })
8694
+ ]
8695
+ });
7617
8696
  }
7618
8697
  //#endregion
7619
8698
  //#region src/elements/accessoryContext.ts
@@ -7626,15 +8705,18 @@ const SectionAccessoryContext = createContext(false);
7626
8705
  //#region src/elements/SelectDialog.tsx
7627
8706
  const optionId = (option) => option.value ?? option.text.text;
7628
8707
  /**
7629
- * Slack's "Select options" dialog, which a `multi_static_select` in a section accessory opens
7630
- * instead of an inline menu. Picks are a draft: the option list stays open while choosing, and
7631
- * nothing is sent until Confirm. Cancel, the close button and Escape discard the draft.
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.
7632
8713
  */
7633
- function SelectDialog({ options, initial, placeholder, onConfirm, onCancel }) {
8714
+ function SelectDialog({ options, initial, placeholder, addTyped, onConfirm, onCancel }) {
7634
8715
  const { theme } = useBlockKit();
7635
8716
  const titleId = useId();
7636
8717
  const [draft, setDraft] = useState(initial);
7637
8718
  const [query, setQuery] = useState("");
8719
+ const dialogRef = useRef(null);
7638
8720
  const fieldRef = useRef(null);
7639
8721
  const listRef = useRef(null);
7640
8722
  const rows = options.filter((o) => o.text.text.toLowerCase().includes(query.toLowerCase()));
@@ -7649,17 +8731,24 @@ function SelectDialog({ options, initial, placeholder, onConfirm, onCancel }) {
7649
8731
  const option = rows[i];
7650
8732
  if (option) toggle(option);
7651
8733
  },
8734
+ onSubmitQuery: addTyped ? (typed) => {
8735
+ const option = addTyped(typed);
8736
+ if (!chosen(option)) setDraft((current) => [...current, option]);
8737
+ setQuery("");
8738
+ } : void 0,
7652
8739
  listRef
7653
8740
  });
7654
8741
  useEffect(() => {
7655
- combo.inputRef.current?.focus();
8742
+ dialogRef.current?.focus();
7656
8743
  }, []);
7657
8744
  if (typeof document === "undefined") return null;
7658
8745
  return createPortal(/* @__PURE__ */ jsx("div", {
7659
8746
  className: "sbk-root sbk-select-dialog__overlay",
7660
8747
  "data-theme": theme,
7661
8748
  children: /* @__PURE__ */ jsxs("div", {
8749
+ ref: dialogRef,
7662
8750
  className: "sbk-select-dialog",
8751
+ tabIndex: -1,
7663
8752
  role: "dialog",
7664
8753
  "aria-modal": "true",
7665
8754
  "aria-labelledby": titleId,
@@ -7675,7 +8764,7 @@ function SelectDialog({ options, initial, placeholder, onConfirm, onCancel }) {
7675
8764
  children: [/* @__PURE__ */ jsx("h2", {
7676
8765
  className: "sbk-select-dialog__title",
7677
8766
  id: titleId,
7678
- children: "Select options"
8767
+ children: placeholder
7679
8768
  }), /* @__PURE__ */ jsx("button", {
7680
8769
  type: "button",
7681
8770
  className: "sbk-select-dialog__close",
@@ -7689,26 +8778,34 @@ function SelectDialog({ options, initial, placeholder, onConfirm, onCancel }) {
7689
8778
  children: [/* @__PURE__ */ jsxs("div", {
7690
8779
  className: "sbk-select-dialog__field",
7691
8780
  ref: fieldRef,
7692
- children: [draft.map((option) => /* @__PURE__ */ jsxs("span", {
7693
- className: "sbk-select__chip",
7694
- children: [/* @__PURE__ */ jsx("span", {
7695
- className: "sbk-select__chip-label",
7696
- children: /* @__PURE__ */ jsx("span", {
7697
- className: "sbk-select__chip-text",
7698
- children: /* @__PURE__ */ jsx("span", { children: option.text.text })
7699
- })
7700
- }), /* @__PURE__ */ jsx("button", {
7701
- type: "button",
7702
- className: "sbk-select__chip-remove",
7703
- "aria-label": `Remove ${option.text.text}`,
7704
- onClick: () => toggle(option),
7705
- children: /* @__PURE__ */ jsx(CloseIcon, {})
7706
- })]
7707
- }, optionId(option))), /* @__PURE__ */ jsx("input", {
7708
- ...combo.inputProps(void 0, placeholder),
7709
- placeholder: draft.length > 0 ? "" : placeholder,
7710
- className: "sbk-select-dialog__input"
7711
- })]
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
+ ]
7712
8809
  }), combo.open && /* @__PURE__ */ jsx(Popover, {
7713
8810
  anchorRef: fieldRef,
7714
8811
  onDismiss: () => combo.setOpen(false),
@@ -7737,20 +8834,14 @@ function SelectDialog({ options, initial, placeholder, onConfirm, onCancel }) {
7737
8834
  className: "sbk-select-dialog__footer",
7738
8835
  children: [/* @__PURE__ */ jsx("button", {
7739
8836
  type: "button",
7740
- className: "sbk-button",
8837
+ className: "sbk-modal__button",
7741
8838
  onClick: onCancel,
7742
- children: /* @__PURE__ */ jsx("span", {
7743
- className: "sbk-button__label",
7744
- children: "Cancel"
7745
- })
8839
+ children: "Cancel"
7746
8840
  }), /* @__PURE__ */ jsx("button", {
7747
8841
  type: "button",
7748
- className: "sbk-button sbk-button--primary",
8842
+ className: "sbk-modal__button sbk-modal__button--primary",
7749
8843
  onClick: () => onConfirm(draft),
7750
- children: /* @__PURE__ */ jsx("span", {
7751
- className: "sbk-button__label",
7752
- children: "Confirm"
7753
- })
8844
+ children: "Confirm"
7754
8845
  })]
7755
8846
  })
7756
8847
  ]
@@ -7812,6 +8903,31 @@ function responseGroups(response) {
7812
8903
  }));
7813
8904
  return [{ options: response.options }];
7814
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
+ }
7815
8931
  /** Slack's default `min_query_length` for an `external_select`. */
7816
8932
  const DEFAULT_MIN_QUERY_LENGTH = 3;
7817
8933
  /** How long typing must pause before an `external_select` asks the app for options. */
@@ -7865,7 +8981,11 @@ function Select({ element, blockId }) {
7865
8981
  actionId,
7866
8982
  blockId
7867
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]));
7868
8986
  const resolveLabel = (id) => {
8987
+ const known = entryById.get(id);
8988
+ if (known) return known.name;
7869
8989
  return (source === "users" ? resolvers.user : source === "channels" || source === "conversations" ? resolvers.channel : void 0)?.(id) ?? id;
7870
8990
  };
7871
8991
  /** Slack's own Builder preview can't resolve a fabricated id against the real directory, and
@@ -7914,6 +9034,7 @@ function Select({ element, blockId }) {
7914
9034
  action_id: actionId,
7915
9035
  block_id: blockId,
7916
9036
  ...fields,
9037
+ ...!multi && element.initial_option ? { initial_option: element.initial_option } : {},
7917
9038
  ...element.placeholder ? { placeholder: element.placeholder } : {}
7918
9039
  });
7919
9040
  }
@@ -7939,27 +9060,64 @@ function Select({ element, blockId }) {
7939
9060
  async function removeItem(id) {
7940
9061
  await commit(items.filter((i) => i.id !== id));
7941
9062
  }
7942
- const filteredOptions = source === "static" ? allOptions(element).filter((o) => o.text.text.toLowerCase().includes(query.toLowerCase())) : [];
7943
- const matches = (o) => o.text.text.toLowerCase().includes(query.toLowerCase());
7944
- 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) => ({
7945
9082
  label: group.label.text,
7946
9083
  options: group.options.filter(matches)
7947
- })) : [{ options: filteredOptions }];
9084
+ })).filter((group) => group.options.length > 0) : [{ options: filteredOptions }];
7948
9085
  const flatOptions = visibleGroups.flatMap((g) => g.options);
7949
9086
  const isSelected = (option) => items.some((i) => i.id === (option.value ?? option.text.text));
7950
9087
  const showSearchOnly = source !== "static";
7951
- const typeable = source === "static" && !multi;
7952
- const dialogMode = useContext(SectionAccessoryContext) && multi && 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
+ };
7953
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;
7954
9107
  const combo = useCombobox({
7955
9108
  open,
7956
9109
  onOpenChange: setOpen,
7957
9110
  query,
7958
9111
  onQueryChange: setQuery,
7959
- count: flatOptions.length,
7960
- initialIndex: Math.max(0, flatOptions.findIndex(isSelected)),
9112
+ count: flatOptions.length + lead,
9113
+ initialIndex: typeable || placeholderRow ? 0 : Math.max(0, flatOptions.findIndex(isSelected)),
9114
+ holdFirstArrow: flatOptions.findIndex(isSelected) < 0 || query.trim() !== "",
7961
9115
  onChoose: (i) => {
7962
- const option = flatOptions[i];
9116
+ if (i < lead) {
9117
+ combo.setOpen(false);
9118
+ return;
9119
+ }
9120
+ const option = flatOptions[i - lead];
7963
9121
  if (option) selectItem(optionToItem(option));
7964
9122
  },
7965
9123
  onSubmitQuery: (typed) => {
@@ -7969,23 +9127,28 @@ function Select({ element, blockId }) {
7969
9127
  });
7970
9128
  },
7971
9129
  listRef,
7972
- returnFocusRef: typeable ? void 0 : triggerRef
9130
+ returnFocusRef: typeable ? void 0 : triggerRef,
9131
+ keepQuery: typeable,
9132
+ typedHighlight: !buttonSelect,
9133
+ chosenAsPlaceholder: !typeable
7973
9134
  });
7974
9135
  useFocusOnLoad(element, typeable ? combo.inputRef : triggerRef);
7975
9136
  const closedLabel = multi ? void 0 : items[0] ? items[0].label || resolveLabel(items[0].id) : void 0;
7976
- const placeholder = element.placeholder?.text ?? "Select an item";
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;
7977
9140
  const menu = open && /* @__PURE__ */ jsx(Popover, {
7978
9141
  anchorRef: rootRef,
7979
9142
  onDismiss: () => combo.setOpen(false),
7980
- offsetX: typeable ? -12 : 0,
9143
+ offsetX: typeable || buttonSelect || wideList ? -12 : 0,
7981
9144
  gap: -4,
7982
9145
  children: /* @__PURE__ */ jsxs("div", {
7983
- className: `sbk-select__menu${typeable ? " sbk-select__menu--typeable" : ""}`,
9146
+ className: `sbk-select__menu${typeable || buttonSelect ? " sbk-select__menu--typeable" : ""}${wideList ? " sbk-select__menu--wide" : ""}`,
7984
9147
  role: "listbox",
7985
9148
  id: combo.listId,
7986
9149
  ref: listRef,
7987
9150
  children: [
7988
- !typeable && (showSearchOnly || allOptions(element).length > 8) && /* @__PURE__ */ jsxs("div", {
9151
+ !typeable && !buttonSelect && (showSearchOnly && entries.length === 0 || (entries.length > 0 ? entries.length : allOptions(element).length) > 8) && /* @__PURE__ */ jsxs("div", {
7989
9152
  className: "sbk-select__search",
7990
9153
  children: [/* @__PURE__ */ jsx(SearchIcon$1, {}), /* @__PURE__ */ jsx("input", {
7991
9154
  autoFocus: true,
@@ -7995,34 +9158,61 @@ function Select({ element, blockId }) {
7995
9158
  onChange: (e) => setQuery(e.target.value)
7996
9159
  })]
7997
9160
  }),
7998
- (() => {
7999
- let index = 0;
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;
8000
9173
  return visibleGroups.map((group, gi) => {
8001
9174
  const rows = group.options.map((option) => {
8002
9175
  const i = index++;
8003
9176
  return /* @__PURE__ */ jsx(SelectOption, {
8004
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,
8005
9180
  selected: isSelected(option),
9181
+ check: (multi || typeable || buttonSelect) && isSelected(option),
9182
+ typed: combo.typed && combo.active === i,
8006
9183
  onSelect: () => selectItem(optionToItem(option)),
8007
9184
  navProps: combo.optionProps(i)
8008
9185
  }, option.value ?? i);
8009
9186
  });
8010
9187
  return group.label === void 0 ? rows : /* @__PURE__ */ jsxs("div", {
8011
9188
  className: "sbk-select__group",
8012
- children: [/* @__PURE__ */ jsx("div", {
8013
- className: "sbk-select__group-label",
8014
- children: group.label
8015
- }), rows]
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
+ ]
8016
9200
  }, gi);
8017
9201
  });
8018
9202
  })(),
8019
- remote && loading && /* @__PURE__ */ jsx("div", {
9203
+ remote && loading && !belowMinimum && /* @__PURE__ */ jsx("div", {
8020
9204
  className: "sbk-select__status",
8021
9205
  children: "Loading…"
8022
9206
  }),
8023
- remote && !loading && remoteGroups !== void 0 && flatOptions.length === 0 && /* @__PURE__ */ jsx("div", {
8024
- className: "sbk-select__status",
8025
- children: "No results"
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
+ })
8026
9216
  }),
8027
9217
  showSearchOnly && items.length > 0 && multi && /* @__PURE__ */ jsx("div", {
8028
9218
  className: "sbk-select__group-label",
@@ -8058,14 +9248,21 @@ function Select({ element, blockId }) {
8058
9248
  })
8059
9249
  }),
8060
9250
  dialogOpen && /* @__PURE__ */ jsx(SelectDialog, {
8061
- options: allOptions(element),
8062
- initial: items.flatMap((i) => i.option ? [i.option] : []),
9251
+ options: directory ? [] : allOptions(element),
9252
+ initial: items.map(itemToOption),
8063
9253
  placeholder,
9254
+ addTyped: directory ? (typed) => itemToOption({
9255
+ id: typed,
9256
+ label: resolveLabel(typed)
9257
+ }) : void 0,
8064
9258
  onCancel: closeDialog,
8065
9259
  onConfirm: async (selected) => {
8066
9260
  closeDialog();
8067
9261
  if (!await ask()) return;
8068
- await commit(selected.map(optionToItem));
9262
+ await commit(selected.map((o) => directory ? {
9263
+ id: o.value ?? o.text.text,
9264
+ label: ""
9265
+ } : optionToItem(o)));
8069
9266
  }
8070
9267
  }),
8071
9268
  dialog
@@ -8073,8 +9270,9 @@ function Select({ element, blockId }) {
8073
9270
  });
8074
9271
  }
8075
9272
  if (typeable) {
8076
- const display = items[0] !== void 0 && isUnresolved(items[0]) ? void 0 : closedLabel;
8077
- const overlay = display !== void 0 && !open;
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;
8078
9276
  return /* @__PURE__ */ jsxs("div", {
8079
9277
  className: `sbk-select sbk-select--typeable${sizeClass}`,
8080
9278
  ref: rootRef,
@@ -8091,7 +9289,7 @@ function Select({ element, blockId }) {
8091
9289
  overlay && /* @__PURE__ */ jsx("span", {
8092
9290
  className: "sbk-select__content",
8093
9291
  "aria-hidden": "true",
8094
- children: /* @__PURE__ */ jsx("span", {
9292
+ children: unresolved && items[0] ? unresolvedValue(items[0].id) : /* @__PURE__ */ jsx("span", {
8095
9293
  className: "sbk-select__content-text",
8096
9294
  children: display
8097
9295
  })
@@ -8104,27 +9302,27 @@ function Select({ element, blockId }) {
8104
9302
  });
8105
9303
  }
8106
9304
  return /* @__PURE__ */ jsxs("div", {
8107
- 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}`,
8108
9306
  ref: rootRef,
8109
9307
  onKeyDown: combo.onKeyDown,
8110
9308
  children: [
8111
- multi && element.max_selected_items && /* @__PURE__ */ jsxs("p", {
9309
+ multi && element.max_selected_items && /* @__PURE__ */ jsx("p", {
8112
9310
  className: "sbk-select__max-info",
8113
- children: [
8114
- "You can select up to ",
8115
- element.max_selected_items,
8116
- " ",
8117
- element.max_selected_items === 1 ? "item" : "items",
8118
- "."
8119
- ]
9311
+ children: `You can select up to ${element.max_selected_items} ${element.max_selected_items === 1 ? "item" : "items"}.`
8120
9312
  }),
8121
9313
  /* @__PURE__ */ jsxs("button", {
8122
9314
  ref: triggerRef,
8123
9315
  type: "button",
8124
9316
  className: "sbk-select__control",
8125
- onClick: () => setOpen((o) => !o),
9317
+ onClick: (e) => {
9318
+ if (buttonSelect) e.currentTarget.focus();
9319
+ setOpen((o) => !o);
9320
+ },
9321
+ role: buttonSelect ? "combobox" : void 0,
8126
9322
  "aria-haspopup": "listbox",
8127
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,
8128
9326
  ...invalid,
8129
9327
  children: [multi ? items.length > 0 ? /* @__PURE__ */ jsx("span", {
8130
9328
  className: "sbk-select__chips",
@@ -8160,34 +9358,165 @@ function Select({ element, blockId }) {
8160
9358
  }) : closedLabel ? items[0] && isUnresolved(items[0]) ? unresolvedValue(items[0].id) : /* @__PURE__ */ jsx("span", {
8161
9359
  className: "sbk-select__value",
8162
9360
  children: closedLabel
9361
+ }) : buttonSelect ? /* @__PURE__ */ jsx("span", {
9362
+ className: "sbk-select__value",
9363
+ children: `--${placeholder}--`
8163
9364
  }) : /* @__PURE__ */ jsx("span", {
8164
9365
  className: "sbk-select__placeholder",
8165
9366
  children: placeholder
8166
- }), /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-select__chevron" })]
9367
+ }), buttonSelect ? /* @__PURE__ */ jsx(CaretDownIcon$1, { className: "sbk-select__chevron" }) : /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-select__chevron" })]
8167
9368
  }),
8168
9369
  menu,
8169
9370
  dialog
8170
9371
  ]
8171
9372
  });
8172
9373
  }
8173
- 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 }) {
8174
9467
  return /* @__PURE__ */ jsxs("div", {
8175
9468
  className: `sbk-select__option${selected ? " sbk-select__option--selected" : ""}`,
8176
9469
  role: "option",
8177
9470
  "aria-selected": selected,
9471
+ "data-typed": typed || void 0,
8178
9472
  onClick: onSelect,
8179
9473
  ...navProps,
8180
- children: [/* @__PURE__ */ jsx("span", {
8181
- className: "sbk-select__option-text",
8182
- children: /* @__PURE__ */ jsx(Text, { text: option.text })
8183
- }), option.description && /* @__PURE__ */ jsx("span", {
8184
- className: "sbk-select__option-description",
8185
- children: /* @__PURE__ */ jsx(Text, { text: option.description })
8186
- })]
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
+ ]
8187
9509
  });
8188
9510
  }
8189
9511
  //#endregion
8190
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
+ };
8191
9520
  const HTML_TYPE = {
8192
9521
  plain_text_input: "text",
8193
9522
  email_text_input: "email",
@@ -8216,6 +9545,7 @@ function TextInput({ element, blockId }) {
8216
9545
  });
8217
9546
  }
8218
9547
  function fire(next) {
9548
+ clearTimeout(typing.current);
8219
9549
  dispatch({
8220
9550
  type: element.type,
8221
9551
  action_id: actionId,
@@ -8223,6 +9553,12 @@ function TextInput({ element, blockId }) {
8223
9553
  value: next
8224
9554
  });
8225
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
+ }
8226
9562
  function onChange(next) {
8227
9563
  if (element.type === "number_input") {
8228
9564
  const pattern = element.is_decimal_allowed ? /^-?\d*\.?\d*$/ : /^-?\d*$/;
@@ -8230,178 +9566,73 @@ function TextInput({ element, blockId }) {
8230
9566
  }
8231
9567
  setLocalValue(next);
8232
9568
  report(next);
8233
- if (dispatchEnabled && triggers.includes("on_character_entered")) fire(next);
9569
+ if (dispatchEnabled && triggers.includes("on_character_entered")) fireAfterPause(next);
8234
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();
8235
9582
  function onKeyDown(e) {
8236
- 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);
8237
9584
  }
8238
- const placeholder = element.placeholder?.text ?? "Write something";
9585
+ const placeholder = element.placeholder?.text ?? DEFAULT_PLACEHOLDER[element.type];
9586
+ const invalidDescribed = invalid["aria-describedby"];
8239
9587
  const commonProps = {
8240
- className: `sbk-text-input${sizeClass}`,
9588
+ className: `sbk-text-input${sizeClass}${remaining !== void 0 ? " sbk-text-input--counted" : ""}`,
8241
9589
  value,
8242
9590
  placeholder,
8243
9591
  onKeyDown,
8244
9592
  ref: focusRef,
8245
9593
  "aria-label": element.action_id,
8246
- ...invalid
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
+ } : {}
8247
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;
8248
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;
8249
- if (element.multiline) return /* @__PURE__ */ jsx("textarea", {
8250
- ...commonProps,
8251
- className: `sbk-text-input sbk-text-input--multiline${sizeClass}`,
8252
- onChange: (e) => onChange(e.target.value)
8253
- });
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
+ }
8254
9623
  const input = /* @__PURE__ */ jsx("input", {
8255
9624
  ...commonProps,
8256
9625
  type: HTML_TYPE[element.type],
8257
9626
  inputMode: element.type === "number_input" ? element.is_decimal_allowed ? "decimal" : "numeric" : void 0,
8258
- minLength: element.min_length,
8259
- maxLength: element.max_length,
8260
- onChange: (e) => onChange(e.target.value)
8261
- });
8262
- if (!leadingIcon) return input;
8263
- return /* @__PURE__ */ jsxs("div", {
8264
- className: "sbk-text-input__wrap",
8265
- children: [leadingIcon, input]
8266
- });
8267
- }
8268
- //#endregion
8269
- //#region src/elements/TimePicker.tsx
8270
- /** Formats `HH:mm` the way Slack's timepicker shows it, e.g. "1:37 PM". */
8271
- function formatTime(time) {
8272
- const [h = 0, m = 0] = time.split(":").map(Number);
8273
- const period = h >= 12 ? "PM" : "AM";
8274
- return `${h % 12 === 0 ? 12 : h % 12}:${String(m).padStart(2, "0")} ${period}`;
8275
- }
8276
- /** Every hour of the day, as Slack's timepicker list offers. */
8277
- const HOURS = Array.from({ length: 24 }, (_, h) => `${String(h).padStart(2, "0")}:00`);
8278
- /**
8279
- * Reads a typed time as `HH:mm`: "2:15 PM", "2:15pm", "2pm", "14:15" or "9". Without AM or PM the
8280
- * hour is read on a 24-hour clock. Slack accepts typed times too; whether it keeps one off the hour
8281
- * (2:15 PM) or rounds it hasn't been verified, so the minutes are kept as typed.
8282
- */
8283
- function parseTime(input) {
8284
- const match = /^(\d{1,2})(?::?(\d{2}))?\s*([ap])\.?m?\.?$|^(\d{1,2})(?::(\d{2}))?$/i.exec(input.trim());
8285
- if (!match) return void 0;
8286
- const meridiem = match[3]?.toLowerCase();
8287
- let hour = Number(match[1] ?? match[4]);
8288
- const minute = Number(match[2] ?? match[5] ?? 0);
8289
- if (minute > 59) return void 0;
8290
- if (meridiem) {
8291
- if (hour < 1 || hour > 12) return void 0;
8292
- hour = hour % 12 + (meridiem === "p" ? 12 : 0);
8293
- } else if (hour > 23) return;
8294
- return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
8295
- }
8296
- const squash = (text) => text.toLowerCase().replace(/\s+/g, "");
8297
- /** The rows for a query: the hours whose label starts with it, after a typed time the list lacks. */
8298
- function timesFor(query) {
8299
- if (!query.trim()) return HOURS;
8300
- const typed = parseTime(query);
8301
- const hours = HOURS.filter((t) => squash(formatTime(t)).startsWith(squash(query)));
8302
- return typed && !hours.includes(typed) && !HOURS.includes(typed) ? [typed, ...hours] : hours;
8303
- }
8304
- function TimePicker({ element, blockId }) {
8305
- const { setValue, dispatch } = useBlockKit();
8306
- const { ask, dialog } = useConfirm(element.confirm);
8307
- const [time, setTime] = useState(element.initial_time);
8308
- const rootRef = useRef(null);
8309
- const listRef = useRef(null);
8310
- const invalid = useInvalidProps();
8311
- const actionId = element.action_id ?? "";
8312
- useEffect(() => {
8313
- if (time) setValue(blockId, actionId, {
8314
- type: "timepicker",
8315
- selected_time: time
8316
- });
8317
- }, []);
8318
- async function pick(next) {
8319
- combo.setOpen(false);
8320
- if (!await ask()) return;
8321
- setTime(next);
8322
- setValue(blockId, actionId, {
8323
- type: "timepicker",
8324
- selected_time: next
8325
- });
8326
- dispatch({
8327
- type: "timepicker",
8328
- action_id: actionId,
8329
- block_id: blockId,
8330
- selected_time: next,
8331
- ...element.initial_time !== void 0 ? { initial_time: element.initial_time } : {}
8332
- });
8333
- }
8334
- const [query, setQuery] = useState("");
8335
- const times = timesFor(query);
8336
- const combo = useCombobox({
8337
- query,
8338
- onQueryChange: setQuery,
8339
- count: times.length,
8340
- initialIndex: time ? times.indexOf(time) : -1,
8341
- onChoose: (i) => {
8342
- const t = times[i];
8343
- if (t) pick(t);
8344
- },
8345
- onSubmitQuery: (typed) => {
8346
- const t = parseTime(typed);
8347
- if (t) pick(t);
8348
- },
8349
- listRef
8350
- });
8351
- useFocusOnLoad(element, combo.inputRef);
8352
- const display = time ? formatTime(time) : void 0;
8353
- const input = combo.inputProps(display, element.placeholder?.text ?? "Select time");
8354
- const overlay = display !== void 0 && !combo.open;
9627
+ onChange: (e) => onChange(e.target.value)
9628
+ });
9629
+ if (!leadingIcon && element.max_length === void 0) return input;
8355
9630
  return /* @__PURE__ */ jsxs("div", {
8356
- className: "sbk-timepicker",
8357
- ref: rootRef,
9631
+ className: `sbk-text-input__wrap${leadingIcon ? " sbk-text-input__wrap--icon" : ""}`,
8358
9632
  children: [
8359
- /* @__PURE__ */ jsxs("label", {
8360
- className: "sbk-timepicker__control",
8361
- children: [
8362
- /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-timepicker__icon" }),
8363
- /* @__PURE__ */ jsx("input", {
8364
- ...input,
8365
- ...invalid,
8366
- className: `sbk-timepicker__input${overlay ? " sbk-timepicker__input--behind" : ""}`
8367
- }),
8368
- /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-timepicker__chevron" }),
8369
- overlay && /* @__PURE__ */ jsx("span", {
8370
- className: "sbk-timepicker__content",
8371
- "aria-hidden": "true",
8372
- children: /* @__PURE__ */ jsx("span", {
8373
- className: "sbk-timepicker__content-text",
8374
- children: display
8375
- })
8376
- })
8377
- ]
8378
- }),
8379
- combo.open && /* @__PURE__ */ jsx(Popover, {
8380
- anchorRef: rootRef,
8381
- onDismiss: () => combo.setOpen(false),
8382
- offsetX: -12,
8383
- gap: -4,
8384
- children: /* @__PURE__ */ jsx("div", {
8385
- className: "sbk-timepicker__menu",
8386
- role: "listbox",
8387
- id: combo.listId,
8388
- ref: listRef,
8389
- children: times.map((t, i) => /* @__PURE__ */ jsx("div", {
8390
- role: "option",
8391
- "aria-selected": t === time,
8392
- className: `sbk-timepicker__option${t === time ? " sbk-timepicker__option--selected" : ""}`,
8393
- onMouseDown: (e) => e.preventDefault(),
8394
- onClick: () => pick(t),
8395
- ...combo.optionProps(i),
8396
- children: formatTime(t)
8397
- }, t))
8398
- })
8399
- }),
8400
- element.timezone && /* @__PURE__ */ jsxs("div", {
8401
- className: "sbk-timepicker__hint",
8402
- children: ["Timezone: ", element.timezone]
8403
- }),
8404
- dialog
9633
+ leadingIcon,
9634
+ input,
9635
+ count
8405
9636
  ]
8406
9637
  });
8407
9638
  }
@@ -9486,23 +10717,135 @@ function DataTable({ block }) {
9486
10717
  });
9487
10718
  }
9488
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
9489
10828
  //#region src/charts/palette.ts
9490
- /** Series/segment colours, in order, measured from Slack's Builder charts. */
9491
- const CHART_COLORS = [
9492
- "#e96825",
9493
- "#20a271",
9494
- "#c474d3",
9495
- "#0e9dd3"
9496
- ];
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
+ }
9497
10843
  /**
9498
10844
  * Fill of a series' area, as a token per theme (DataVisualization.css, and Message.css for dark):
9499
10845
  * a light tint of the series colour, or a dark one.
9500
10846
  */
9501
10847
  function areaFillForIndex(i) {
9502
- return `var(--sbk-chart-area-${i % CHART_COLORS.length + 1})`;
9503
- }
9504
- function colorForIndex(i) {
9505
- return CHART_COLORS[i % CHART_COLORS.length] ?? "#e96825";
10848
+ return `var(--sbk-chart-area-${i % COLORS + 1})`;
9506
10849
  }
9507
10850
  //#endregion
9508
10851
  //#region src/charts/paths.ts
@@ -9628,8 +10971,11 @@ function useContainerWidth(fallback) {
9628
10971
  //#endregion
9629
10972
  //#region src/charts/CartesianChart.tsx
9630
10973
  const HEIGHT$1 = 360;
10974
+ const PLOT_TOP = 9;
9631
10975
  const PLOT_BOTTOM = 334;
9632
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";
9633
10979
  function leftMarginFor(ticks) {
9634
10980
  const maxLen = Math.max(...ticks.map((t) => formatTick(t).length), 1);
9635
10981
  return Math.max(74, 34 + maxLen * 8);
@@ -9653,16 +10999,48 @@ function CartesianChart({ type, series, categories }) {
9653
10999
  const barGapRatio = .1;
9654
11000
  const barWidth = series.length > 0 ? Math.max(1, groupWidth / (series.length + (series.length - 1) * barGapRatio)) : 0;
9655
11001
  const barGap = barWidth * barGapRatio;
9656
- 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", {
9657
11033
  className: "sbk-chart",
9658
11034
  ref,
9659
- children: /* @__PURE__ */ jsx("svg", {
11035
+ children: [/* @__PURE__ */ jsx("svg", {
9660
11036
  className: "sbk-chart__svg",
9661
11037
  width,
9662
11038
  height: HEIGHT$1,
9663
11039
  viewBox: `0 0 ${width} ${HEIGHT$1}`,
9664
11040
  role: "img",
9665
11041
  "aria-label": "Chart",
11042
+ onPointerMove,
11043
+ onPointerLeave: () => setHover(null),
9666
11044
  children: /* @__PURE__ */ jsxs("g", { children: [
9667
11045
  scale.ticks.map((t) => /* @__PURE__ */ jsx("line", {
9668
11046
  x1: plotLeft,
@@ -9701,62 +11079,171 @@ function CartesianChart({ type, series, categories }) {
9701
11079
  const top = positive ? y(d.value) : zeroY;
9702
11080
  const bottom = positive ? zeroY : y(d.value);
9703
11081
  return /* @__PURE__ */ jsx("path", {
11082
+ className: "sbk-chart__bar",
9704
11083
  d: roundedBarPath(x, top, barWidth, bottom, 4, positive),
9705
- fill: colorForIndex(si)
11084
+ style: { fill: i === hovered ? liftedColorForIndex(si) : colorForIndex(si) }
9706
11085
  }, `${si}-${i}`);
9707
11086
  })) : series.map((s, si) => {
9708
11087
  const points = s.data.map((d, i) => ({
9709
11088
  x: pointX(i),
9710
11089
  y: y(d.value)
9711
11090
  }));
9712
- const color = colorForIndex(si);
11091
+ const color = type === "line" && hovered !== void 0 ? liftedColorForIndex(si) : colorForIndex(si);
9713
11092
  return /* @__PURE__ */ jsxs("g", { children: [type === "area" ? /* @__PURE__ */ jsx("path", {
9714
11093
  d: `${smoothLinePath(points)}L${points[points.length - 1]?.x ?? 0} ${zeroY}L${points[0]?.x ?? 0} ${zeroY}Z`,
9715
11094
  style: { fill: areaFillForIndex(si) },
9716
11095
  fillOpacity: .7
9717
11096
  }) : null, /* @__PURE__ */ jsx("path", {
11097
+ className: "sbk-chart__line",
9718
11098
  d: smoothLinePath(points),
9719
11099
  fill: "none",
9720
- stroke: color,
11100
+ style: { stroke: color },
9721
11101
  strokeWidth: 2,
9722
11102
  strokeLinejoin: "bevel"
9723
11103
  })] }, si);
9724
- })
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
9725
11114
  ] })
9726
- })
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]
9727
11126
  });
9728
11127
  }
9729
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
9730
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
+ */
9731
11219
  const WIDTH = 406;
9732
- /** Builder's pie plot area is 406×360, not square, with the pie centred in it. */
9733
11220
  const HEIGHT = 360;
9734
- const CENTER = {
9735
- x: 203,
9736
- y: 180
9737
- };
11221
+ const CENTER_Y = 180;
9738
11222
  const RADIUS = 144;
9739
11223
  /** Point on the pie's circle for a given angle in degrees, measured clockwise from 12 o'clock. */
9740
- function pointAt(angleDeg) {
11224
+ function pointAt(cx, angleDeg, radius = RADIUS) {
9741
11225
  const rad = (angleDeg - 90) * Math.PI / 180;
9742
11226
  return {
9743
- x: CENTER.x + RADIUS * Math.cos(rad),
9744
- y: CENTER.y + RADIUS * Math.sin(rad)
11227
+ x: cx + radius * Math.cos(rad),
11228
+ y: CENTER_Y + radius * Math.sin(rad)
9745
11229
  };
9746
11230
  }
9747
- function segmentPath(startAngle, endAngle) {
11231
+ function segmentPath(cx, startAngle, endAngle, radius = RADIUS) {
9748
11232
  const sweep = endAngle - startAngle;
9749
- const start = pointAt(startAngle);
9750
- const end = pointAt(endAngle);
11233
+ const start = pointAt(cx, startAngle, radius);
11234
+ const end = pointAt(cx, endAngle, radius);
9751
11235
  const largeArc = sweep > 180 ? 1 : 0;
9752
11236
  if (sweep >= 359.999) {
9753
- const stop = pointAt(startAngle + 360 - 1e-4 * 180 / Math.PI);
11237
+ const stop = pointAt(cx, startAngle + 360 - 1e-4 * 180 / Math.PI, radius);
9754
11238
  const r4 = (n) => Math.round(n * 1e4) / 1e4;
9755
- 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`;
9756
11240
  }
9757
- 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`;
9758
11242
  }
9759
11243
  function PieChart({ segments }) {
11244
+ const [hover, setHover] = useState(null);
11245
+ const { ref, width } = useContainerWidth(WIDTH);
11246
+ const cx = width / 2;
9760
11247
  const total = segments.reduce((sum, s) => sum + Math.max(0, s.value), 0);
9761
11248
  let angle = 0;
9762
11249
  const arcs = segments.map((segment, i) => {
@@ -9765,26 +11252,73 @@ function PieChart({ segments }) {
9765
11252
  const start = angle;
9766
11253
  const end = angle + sweep;
9767
11254
  angle = end;
11255
+ const hovered = hover?.index === i;
9768
11256
  return {
9769
11257
  key: i,
9770
- d: segmentPath(start, end),
9771
- color: colorForIndex(i)
11258
+ hovered,
11259
+ d: segmentPath(cx, start, end, hovered ? 149 : RADIUS),
11260
+ color: hovered ? liftedColorForIndex(i) : colorForIndex(i)
9772
11261
  };
9773
11262
  });
9774
- return /* @__PURE__ */ jsx("svg", {
9775
- className: "sbk-chart__svg sbk-chart__svg--pie",
9776
- width: WIDTH,
9777
- height: HEIGHT,
9778
- viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
9779
- role: "img",
9780
- "aria-label": "Pie chart",
9781
- children: arcs.map((arc) => /* @__PURE__ */ jsx("path", {
9782
- d: arc.d,
9783
- fill: arc.color,
9784
- stroke: "#fff",
9785
- strokeWidth: 2,
9786
- strokeLinejoin: "round"
9787
- }, 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]
9788
11322
  });
9789
11323
  }
9790
11324
  //#endregion
@@ -9794,13 +11328,14 @@ function PieChart({ segments }) {
9794
11328
  * 12px-radius pill of 32px icon buttons that fades in while its host is hovered or focused.
9795
11329
  * The host needs the `sbk-hover-actions-host` class.
9796
11330
  */
9797
- function HoverActions({ actions, className }) {
11331
+ function HoverActions({ actions, className, menuSide = "left" }) {
9798
11332
  return /* @__PURE__ */ jsx("div", {
9799
11333
  className: `sbk-hover-actions${className ? ` ${className}` : ""}`,
9800
11334
  role: "group",
9801
11335
  children: actions.map((action) => action.menu ? /* @__PURE__ */ jsx(MenuAction, {
9802
11336
  action,
9803
- items: action.menu
11337
+ items: action.menu,
11338
+ side: menuSide
9804
11339
  }, action.label) : /* @__PURE__ */ jsx(Tooltip, {
9805
11340
  label: action.label,
9806
11341
  children: action.href ? /* @__PURE__ */ jsx("a", {
@@ -9820,22 +11355,18 @@ function HoverActions({ actions, className }) {
9820
11355
  }, action.label))
9821
11356
  });
9822
11357
  }
9823
- function MenuAction({ action, items }) {
11358
+ const BUTTON_SIZE = 32;
11359
+ function MenuAction({ action, items, side }) {
9824
11360
  const [open, setOpen] = useState(false);
9825
- const rootRef = useRef(null);
9826
11361
  const triggerRef = useRef(null);
9827
11362
  const listRef = useRef(null);
9828
- useEffect(() => {
9829
- if (!open) return;
9830
- const onDocClick = (e) => {
9831
- if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
9832
- };
9833
- document.addEventListener("mousedown", onDocClick);
9834
- return () => document.removeEventListener("mousedown", onDocClick);
9835
- }, [open]);
11363
+ const idPrefix = useId();
9836
11364
  function choose(i) {
11365
+ const item = items[i];
11366
+ if (!item || item.disabled) return;
9837
11367
  setOpen(false);
9838
- items[i]?.onSelect();
11368
+ triggerRef.current?.focus();
11369
+ item.onSelect();
9839
11370
  }
9840
11371
  const nav = useMenuNavigation({
9841
11372
  open,
@@ -9850,7 +11381,6 @@ function MenuAction({ action, items }) {
9850
11381
  });
9851
11382
  return /* @__PURE__ */ jsxs("div", {
9852
11383
  className: "sbk-hover-actions__menu-root",
9853
- ref: rootRef,
9854
11384
  onKeyDown: nav.onKeyDown,
9855
11385
  children: [/* @__PURE__ */ jsx(Tooltip, {
9856
11386
  label: open ? "" : action.label,
@@ -9864,17 +11394,34 @@ function MenuAction({ action, items }) {
9864
11394
  onClick: () => setOpen((o) => !o),
9865
11395
  children: action.icon
9866
11396
  })
9867
- }), open && /* @__PURE__ */ jsx("div", {
9868
- className: "sbk-overflow__menu sbk-hover-actions__menu",
9869
- role: "menu",
9870
- ref: listRef,
9871
- children: items.map((item, i) => /* @__PURE__ */ jsx("div", {
9872
- role: "menuitem",
9873
- className: "sbk-overflow__option",
9874
- onClick: () => choose(i),
9875
- ...nav.itemProps(i),
9876
- children: item.label
9877
- }, 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
+ })
9878
11425
  })]
9879
11426
  });
9880
11427
  }
@@ -9917,8 +11464,11 @@ function TableActions({ tableRef }) {
9917
11464
  onClick: () => copyText(toTsv(tableRows(tableRef.current)))
9918
11465
  }] });
9919
11466
  }
9920
- /** Slack's `p-attachment_image_actions`: open the full image in a new window, plus a menu. */
9921
- 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 }) {
9922
11472
  return /* @__PURE__ */ jsx(HoverActions, {
9923
11473
  className: "sbk-hover-actions--image",
9924
11474
  actions: [{
@@ -9929,47 +11479,69 @@ function ImageActions({ url }) {
9929
11479
  label: "More actions",
9930
11480
  icon: /* @__PURE__ */ jsx(EllipsisVerticalIcon, {}),
9931
11481
  menu: [{
9932
- label: "Copy link to image",
11482
+ label: "Copy link",
9933
11483
  onSelect: () => copyText(url)
9934
11484
  }, {
9935
- label: "Download image",
9936
- onSelect: () => {
9937
- const link = document.createElement("a");
9938
- link.href = url;
9939
- link.download = "";
9940
- link.target = "_blank";
9941
- link.rel = "noopener noreferrer";
9942
- link.click();
9943
- }
11485
+ label: "Hide image",
11486
+ onSelect: onHide
9944
11487
  }]
9945
11488
  }]
9946
11489
  });
9947
11490
  }
9948
- /** A chart's single "More actions" menu, exporting its underlying data. */
9949
- function ChartActions({ rows, title }) {
9950
- 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 }) {
9951
11504
  return /* @__PURE__ */ jsx(HoverActions, {
9952
11505
  className: "sbk-hover-actions--chart",
11506
+ menuSide: "right",
9953
11507
  actions: [{
9954
11508
  label: "More actions",
9955
11509
  icon: /* @__PURE__ */ jsx(EllipsisVerticalIcon, {}),
9956
- menu: [{
9957
- label: "Copy data",
9958
- onSelect: () => copyText(toTsv(rows))
9959
- }, {
9960
- label: "Download as CSV",
9961
- onSelect: () => downloadText(name, toCsv(rows))
9962
- }]
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
+ ]
9963
11532
  }]
9964
11533
  });
9965
11534
  }
9966
11535
  //#endregion
9967
11536
  //#region src/blocks/DataVisualization.tsx
9968
- /** The chart's data as a table: a label column plus one column per series (or a value column). */
9969
- 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) {
9970
11543
  if (chart.type === "pie") return [["Label", "Value"], ...(chart.segments ?? []).map((s) => [s.label, String(s.value)])];
9971
- const series = chart.series ?? [];
9972
- 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 ?? ""))])];
9973
11545
  }
9974
11546
  function legendEntries(chart) {
9975
11547
  if (chart.type === "pie") return (chart.segments ?? []).map((s, i) => ({
@@ -9982,47 +11554,63 @@ function legendEntries(chart) {
9982
11554
  }));
9983
11555
  }
9984
11556
  function DataVisualization({ block }) {
11557
+ const [tableOpen, setTableOpen] = useState(false);
9985
11558
  const chart = block.chart;
9986
11559
  if (!chart) return null;
9987
11560
  const isPie = chart.type === "pie";
9988
11561
  const categories = chart.axis_config?.categories ?? chart.series?.[0]?.data.map((d) => d.label) ?? [];
9989
11562
  const legend = legendEntries(chart);
11563
+ const rows = chartTable(chart, categories);
9990
11564
  return /* @__PURE__ */ jsxs("div", {
9991
- className: "sbk-dataviz sbk-hover-actions-host",
11565
+ className: "sbk-dataviz-frame sbk-hover-actions-host",
9992
11566
  children: [
9993
- block.title ? /* @__PURE__ */ jsx("h3", {
9994
- className: "sbk-dataviz__title",
9995
- children: block.title
9996
- }) : null,
9997
- /* @__PURE__ */ jsxs("div", {
9998
- className: isPie ? "sbk-dataviz__body sbk-dataviz__body--pie" : "sbk-dataviz__body",
9999
- children: [!isPie && chart.axis_config?.y_label ? /* @__PURE__ */ jsx("div", {
10000
- className: "sbk-dataviz__y-label",
10001
- children: chart.axis_config.y_label
10002
- }) : null, chart.type === "pie" ? /* @__PURE__ */ jsx(PieChart, { segments: chart.segments ?? [] }) : /* @__PURE__ */ jsx(CartesianChart, {
10003
- type: chart.type,
10004
- series: chart.series ?? [],
10005
- categories
10006
- })]
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
+ })
10007
11603
  }),
10008
- !isPie && chart.axis_config?.x_label ? /* @__PURE__ */ jsx("div", {
10009
- className: "sbk-dataviz__x-label",
10010
- children: chart.axis_config.x_label
10011
- }) : null,
10012
- legend.length > 0 ? /* @__PURE__ */ jsx("div", {
10013
- className: isPie ? "sbk-dataviz__legend sbk-dataviz__legend--pie" : "sbk-dataviz__legend",
10014
- children: legend.map((entry) => /* @__PURE__ */ jsxs("span", {
10015
- className: "sbk-dataviz__legend-entry",
10016
- children: [/* @__PURE__ */ jsx("span", {
10017
- className: "sbk-dataviz__legend-dot",
10018
- style: { backgroundColor: entry.color }
10019
- }), entry.name]
10020
- }, entry.name))
10021
- }) : null,
10022
11604
  /* @__PURE__ */ jsx(ChartActions, {
10023
- rows: chartRows(chart, categories),
10024
- title: block.title
10025
- })
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
10026
11614
  ]
10027
11615
  });
10028
11616
  }
@@ -10140,25 +11728,39 @@ function ExpandCaret({ expanded }) {
10140
11728
  });
10141
11729
  }
10142
11730
  /**
10143
- * `image_url` renders directly. `slack_file` references a file in a workspace's file store —
10144
- * without an authenticated `files.info` call we can't resolve it to a URL, so we render the
10145
- * 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>`.
10146
11735
  *
10147
11736
  * Slack always shows a title row above the image with an expand caret and a "(N kB)" file-size
10148
- * caption — the caption comes from fetching the file's real bytes, which isn't available to us
10149
- * without a network round-trip, so we render the row (title + caret) but omit the byte count.
10150
- * 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.
10151
11743
  *
10152
11744
  * The caret is a toggle, as in Slack: pressing it hides the image (instantly, no animation) and
10153
11745
  * leaves only the title row, and pressing it again brings the image back.
10154
11746
  */
10155
11747
  function Image({ block }) {
10156
11748
  const json = block;
10157
- const imageUrl = "image_url" in block ? block.image_url : void 0;
11749
+ const directUrl = "image_url" in block ? block.image_url : void 0;
10158
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;
10159
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;
10160
11762
  return /* @__PURE__ */ jsxs("figure", {
10161
- className: "sbk-image",
11763
+ className: isFile ? "sbk-image sbk-image--file" : "sbk-image",
10162
11764
  children: [imageUrl && /* @__PURE__ */ jsxs("div", {
10163
11765
  className: "sbk-image__title",
10164
11766
  children: [block.title && /* @__PURE__ */ jsx("span", {
@@ -10166,15 +11768,20 @@ function Image({ block }) {
10166
11768
  children: /* @__PURE__ */ jsx(Text, { text: block.title })
10167
11769
  }), /* @__PURE__ */ jsxs("span", {
10168
11770
  className: "sbk-image__trigger",
10169
- children: [" ", /* @__PURE__ */ jsx("button", {
10170
- type: "button",
10171
- className: "sbk-image__toggle",
10172
- "aria-label": "image",
10173
- "aria-expanded": expanded,
10174
- title: expanded ? "Collapse" : "Expand",
10175
- onClick: () => setExpanded((value) => !value),
10176
- children: /* @__PURE__ */ jsx(ExpandCaret, { expanded })
10177
- })]
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
+ ]
10178
11785
  })]
10179
11786
  }), expanded && /* @__PURE__ */ jsxs("div", {
10180
11787
  className: "sbk-image__frame sbk-hover-actions-host",
@@ -10191,26 +11798,71 @@ function Image({ block }) {
10191
11798
  className: "sbk-image__fallback-text",
10192
11799
  children: slackFile ? "Slack file image unavailable" : block.alt_text
10193
11800
  })
10194
- }), imageUrl && /* @__PURE__ */ jsx(ImageActions, { url: imageUrl })]
11801
+ }), imageUrl && /* @__PURE__ */ jsx(ImageActions, {
11802
+ url: imageUrl,
11803
+ onHide: hide
11804
+ })]
10195
11805
  })]
10196
11806
  });
10197
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
+ }
10198
11855
  //#endregion
10199
11856
  //#region src/blocks/Input.tsx
10200
- const TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
10201
- "plain_text_input",
10202
- "email_text_input",
10203
- "url_text_input",
10204
- "number_input"
10205
- ]);
10206
- /** Slack's Builder preview synthesizes a "Press 'enter' to submit" hint below a single-line text
10207
- * input whose owning `input` block dispatches on the (default) `on_enter_pressed` trigger, when
10208
- * the block doesn't already carry its own explicit `hint`. It does NOT synthesize anything for
10209
- * `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). */
10210
11862
  function dispatchHint(block) {
10211
- if (block.dispatch_action !== true || block.hint) return false;
11863
+ if (block.dispatch_action !== true) return false;
10212
11864
  const element = block.element;
10213
- if (typeof element.type !== "string" || !TEXT_INPUT_TYPES.has(element.type)) return false;
11865
+ if (element.type !== "plain_text_input") return false;
10214
11866
  if (element.multiline === true) return false;
10215
11867
  return (element.dispatch_action_config?.trigger_actions_on ?? ["on_enter_pressed"]).includes("on_enter_pressed");
10216
11868
  }
@@ -10222,11 +11874,24 @@ function dispatchHint(block) {
10222
11874
  * `__dispatchAction` field, since text inputs need it to pick their triggers.
10223
11875
  */
10224
11876
  function Input({ block, blockId }) {
10225
- const { errors, surface } = useBlockKit();
11877
+ const { errors, surface, seedValue } = useBlockKit();
10226
11878
  const element = {
10227
11879
  ...block.element,
10228
11880
  __dispatchAction: block.dispatch_action === true
10229
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
+ ]);
10230
11895
  const error = errors[blockId];
10231
11896
  const errorId = useId();
10232
11897
  const showDispatchHint = dispatchHint(block);
@@ -10260,16 +11925,12 @@ function Input({ block, blockId }) {
10260
11925
  })
10261
11926
  })
10262
11927
  }),
10263
- block.hint && /* @__PURE__ */ jsx("div", {
11928
+ (block.hint || showDispatchHint) && /* @__PURE__ */ jsxs("div", {
10264
11929
  className: "sbk-input__hint",
10265
- children: /* @__PURE__ */ jsx("span", {
11930
+ children: [block.hint && /* @__PURE__ */ jsxs("span", {
10266
11931
  className: "sbk-input__hint-text",
10267
- children: /* @__PURE__ */ jsx(Text, { text: block.hint })
10268
- })
10269
- }),
10270
- showDispatchHint && /* @__PURE__ */ jsx("div", {
10271
- className: "sbk-input__hint",
10272
- 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", {
10273
11934
  className: "sbk-input__hint-text sbk-input__hint-text--dispatch",
10274
11935
  children: [
10275
11936
  /* @__PURE__ */ jsx("span", {
@@ -10279,7 +11940,7 @@ function Input({ block, blockId }) {
10279
11940
  " ",
10280
11941
  "Press 'enter' to submit"
10281
11942
  ]
10282
- })
11943
+ })] })]
10283
11944
  }),
10284
11945
  error && /* @__PURE__ */ jsx("div", {
10285
11946
  className: "sbk-input__error",
@@ -10889,7 +12550,24 @@ function renderSectionChildren(elements, ctx) {
10889
12550
  className: "sbk-rich-text__jumbo-emoji",
10890
12551
  children: elements.filter((el) => el.type === "emoji").map((el, i) => /* @__PURE__ */ jsx(Fragment$1, { children: renderLeaf(el, ctx, 32) }, i))
10891
12552
  });
10892
- 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;
10893
12571
  }
10894
12572
  function renderLeaf(el, ctx, emojiSize, isFirst = false) {
10895
12573
  let content;
@@ -10928,10 +12606,10 @@ function renderLeaf(el, ctx, emojiSize, isFirst = false) {
10928
12606
  }
10929
12607
  case "usergroup": {
10930
12608
  const name = ctx.resolvers.usergroup?.(el.usergroup_id);
10931
- content = name ? /* @__PURE__ */ jsxs(MentionLink, {
12609
+ content = name ? /* @__PURE__ */ jsx(MentionLink, {
10932
12610
  type: "usergroup",
10933
12611
  id: el.usergroup_id,
10934
- children: ["@", name]
12612
+ children: `@${name}`
10935
12613
  }) : /* @__PURE__ */ jsx("span", {
10936
12614
  className: "sbk-mention--loading",
10937
12615
  "aria-label": "Loading user group"
@@ -10940,10 +12618,10 @@ function renderLeaf(el, ctx, emojiSize, isFirst = false) {
10940
12618
  }
10941
12619
  case "channel": {
10942
12620
  const name = ctx.resolvers.channel?.(el.channel_id);
10943
- content = name ? /* @__PURE__ */ jsxs(MentionLink, {
12621
+ content = name ? /* @__PURE__ */ jsx(MentionLink, {
10944
12622
  type: "channel",
10945
12623
  id: el.channel_id,
10946
- children: ["#", name]
12624
+ children: `#${name}`
10947
12625
  }) : /* @__PURE__ */ jsxs("span", {
10948
12626
  className: "sbk-mention--private",
10949
12627
  children: [/* @__PURE__ */ jsx(LockIcon, {}), "Private channel"]
@@ -10951,9 +12629,9 @@ function renderLeaf(el, ctx, emojiSize, isFirst = false) {
10951
12629
  break;
10952
12630
  }
10953
12631
  case "broadcast":
10954
- content = /* @__PURE__ */ jsxs("span", {
12632
+ content = /* @__PURE__ */ jsx("span", {
10955
12633
  className: "sbk-mention--broadcast",
10956
- children: ["@", el.range]
12634
+ children: `@${el.range}`
10957
12635
  });
10958
12636
  break;
10959
12637
  case "date": {