@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--GXgMjw3.d.ts → index-xHrfGT5m.d.ts} +64 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2636 -958
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/dist/transport.d.ts +1 -1
- package/dist/web-api.d.ts +1 -1
- package/package.json +1 -1
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__ */
|
|
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:
|
|
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__ */
|
|
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__ */
|
|
6013
|
+
return name ? /* @__PURE__ */ jsx(MentionLink, {
|
|
5927
6014
|
type: "usergroup",
|
|
5928
6015
|
id: node.id,
|
|
5929
|
-
children:
|
|
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__ */
|
|
6024
|
+
return name ? /* @__PURE__ */ jsx(MentionLink, {
|
|
5938
6025
|
type: "channel",
|
|
5939
6026
|
id: node.id,
|
|
5940
|
-
children:
|
|
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__ */
|
|
6033
|
+
case "broadcast": return /* @__PURE__ */ jsx("span", {
|
|
5947
6034
|
className: "sbk-mention--broadcast",
|
|
5948
|
-
children:
|
|
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/
|
|
6460
|
-
/**
|
|
6461
|
-
function
|
|
6462
|
-
|
|
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
|
-
/**
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
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
|
-
|
|
6558
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
6757
|
-
|
|
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-
|
|
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")
|
|
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(
|
|
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(
|
|
7562
|
+
open && /* @__PURE__ */ jsx(CalendarPopover, {
|
|
6791
7563
|
anchorRef: rootRef,
|
|
6792
|
-
|
|
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
|
-
|
|
6795
|
-
|
|
7570
|
+
onEscape: close,
|
|
7571
|
+
focusOnOpen: () => {
|
|
7572
|
+
const focus = focusCalendar.current;
|
|
6796
7573
|
focusCalendar.current = false;
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
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
|
-
/**
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
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
|
-
|
|
6828
|
-
|
|
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
|
|
6850
|
-
const [
|
|
6851
|
-
const
|
|
6852
|
-
const
|
|
6853
|
-
const
|
|
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 (
|
|
7977
|
+
if (element.initial_date_time !== void 0) setValue(blockId, actionId, {
|
|
6857
7978
|
type: "datetimepicker",
|
|
6858
|
-
selected_date_time:
|
|
7979
|
+
selected_date_time: element.initial_date_time
|
|
6859
7980
|
});
|
|
6860
7981
|
}, []);
|
|
6861
|
-
async function
|
|
6862
|
-
if (!draftDate) return;
|
|
7982
|
+
async function change(next) {
|
|
6863
7983
|
if (!await ask()) return;
|
|
6864
|
-
|
|
6865
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
8051
|
+
ref: dateRef,
|
|
6888
8052
|
type: "button",
|
|
6889
8053
|
className: "sbk-datetimepicker__control sbk-datetimepicker__control--date",
|
|
6890
|
-
onClick: () =>
|
|
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
|
|
6895
|
-
children: value
|
|
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("
|
|
6903
|
-
|
|
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("
|
|
6909
|
-
|
|
6910
|
-
|
|
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__ */
|
|
8092
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
6915
8093
|
className: "sbk-datetimepicker__timezone",
|
|
6916
|
-
children:
|
|
8094
|
+
children: `Time zone: ${timeZoneLabel(zone)}`
|
|
6917
8095
|
})]
|
|
6918
8096
|
})]
|
|
6919
8097
|
}),
|
|
6920
|
-
|
|
6921
|
-
anchorRef:
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
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
|
-
/**
|
|
6952
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
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
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
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
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
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
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
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
|
|
7630
|
-
*
|
|
7631
|
-
* nothing is sent until Confirm. Cancel, the close button and Escape discard the
|
|
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
|
-
|
|
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:
|
|
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: [
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
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-
|
|
8837
|
+
className: "sbk-modal__button",
|
|
7741
8838
|
onClick: onCancel,
|
|
7742
|
-
children:
|
|
7743
|
-
className: "sbk-button__label",
|
|
7744
|
-
children: "Cancel"
|
|
7745
|
-
})
|
|
8839
|
+
children: "Cancel"
|
|
7746
8840
|
}), /* @__PURE__ */ jsx("button", {
|
|
7747
8841
|
type: "button",
|
|
7748
|
-
className: "sbk-
|
|
8842
|
+
className: "sbk-modal__button sbk-modal__button--primary",
|
|
7749
8843
|
onClick: () => onConfirm(draft),
|
|
7750
|
-
children:
|
|
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
|
|
7943
|
-
const
|
|
7944
|
-
const
|
|
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
|
|
7952
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: [
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
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
|
-
|
|
8024
|
-
className: "sbk-
|
|
8025
|
-
|
|
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.
|
|
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(
|
|
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
|
|
8077
|
-
const
|
|
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__ */
|
|
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: () =>
|
|
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
|
-
|
|
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: [
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
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"))
|
|
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 ??
|
|
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)
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
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
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
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-
|
|
8357
|
-
ref: rootRef,
|
|
9631
|
+
className: `sbk-text-input__wrap${leadingIcon ? " sbk-text-input__wrap--icon" : ""}`,
|
|
8358
9632
|
children: [
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
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
|
-
/**
|
|
9491
|
-
const
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
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 %
|
|
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
|
-
|
|
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
|
|
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:
|
|
9744
|
-
y:
|
|
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${
|
|
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${
|
|
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
|
-
|
|
9771
|
-
|
|
11258
|
+
hovered,
|
|
11259
|
+
d: segmentPath(cx, start, end, hovered ? 149 : RADIUS),
|
|
11260
|
+
color: hovered ? liftedColorForIndex(i) : colorForIndex(i)
|
|
9772
11261
|
};
|
|
9773
11262
|
});
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
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
|
-
/**
|
|
9921
|
-
|
|
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
|
|
11482
|
+
label: "Copy link",
|
|
9933
11483
|
onSelect: () => copyText(url)
|
|
9934
11484
|
}, {
|
|
9935
|
-
label: "
|
|
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
|
-
/**
|
|
9949
|
-
|
|
9950
|
-
|
|
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
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
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
|
-
/**
|
|
9969
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9994
|
-
className: "sbk-
|
|
9995
|
-
children:
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
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
|
|
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
|
-
*
|
|
10145
|
-
*
|
|
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
|
|
10149
|
-
*
|
|
10150
|
-
*
|
|
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
|
|
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: [
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
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, {
|
|
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
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
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
|
|
11863
|
+
if (block.dispatch_action !== true) return false;
|
|
10212
11864
|
const element = block.element;
|
|
10213
|
-
if (
|
|
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__ */
|
|
11928
|
+
(block.hint || showDispatchHint) && /* @__PURE__ */ jsxs("div", {
|
|
10264
11929
|
className: "sbk-input__hint",
|
|
10265
|
-
children: /* @__PURE__ */
|
|
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__ */
|
|
12609
|
+
content = name ? /* @__PURE__ */ jsx(MentionLink, {
|
|
10932
12610
|
type: "usergroup",
|
|
10933
12611
|
id: el.usergroup_id,
|
|
10934
|
-
children:
|
|
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__ */
|
|
12621
|
+
content = name ? /* @__PURE__ */ jsx(MentionLink, {
|
|
10944
12622
|
type: "channel",
|
|
10945
12623
|
id: el.channel_id,
|
|
10946
|
-
children:
|
|
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__ */
|
|
12632
|
+
content = /* @__PURE__ */ jsx("span", {
|
|
10955
12633
|
className: "sbk-mention--broadcast",
|
|
10956
|
-
children:
|
|
12634
|
+
children: `@${el.range}`
|
|
10957
12635
|
});
|
|
10958
12636
|
break;
|
|
10959
12637
|
case "date": {
|