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