@nkootstra/block-kit 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6285,6 +6285,39 @@ const TIME_ZONE_LABELS = {
6285
6285
  function timeZoneLabel(timeZone) {
6286
6286
  return TIME_ZONE_LABELS[timeZone] ?? timeZone;
6287
6287
  }
6288
+ /** A timestamp's wall-clock date ("2026-01-01") and time ("11:00") in `timeZone`. */
6289
+ function wallClock(ts, timeZone) {
6290
+ const parts = new Intl.DateTimeFormat("en-US", {
6291
+ timeZone,
6292
+ year: "numeric",
6293
+ month: "2-digit",
6294
+ day: "2-digit",
6295
+ hour: "2-digit",
6296
+ minute: "2-digit",
6297
+ hourCycle: "h23"
6298
+ }).formatToParts(/* @__PURE__ */ new Date(ts * 1e3));
6299
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
6300
+ return {
6301
+ date: `${get("year")}-${get("month")}-${get("day")}`,
6302
+ time: `${get("hour")}:${get("minute")}`
6303
+ };
6304
+ }
6305
+ /** The timestamp of a wall-clock date and time in `timeZone`. Around a daylight saving change it
6306
+ * resolves like JavaScript's `Date`: a time the clocks skip moves forward by the gap, and a time
6307
+ * they repeat takes its first occurrence. */
6308
+ function fromWallClock(date, time, timeZone) {
6309
+ const asUtc = Date.parse(`${date}T${time}:00Z`) / 1e3;
6310
+ const offsetAt = (ts) => {
6311
+ const wall = wallClock(ts, timeZone);
6312
+ return Date.parse(`${wall.date}T${wall.time}:00Z`) / 1e3 - ts;
6313
+ };
6314
+ const before = asUtc - offsetAt(asUtc - 86400);
6315
+ const matches = [before, asUtc - offsetAt(asUtc + 86400)].filter((ts) => {
6316
+ const wall = wallClock(ts, timeZone);
6317
+ return wall.date === date && wall.time === time;
6318
+ });
6319
+ return matches.length > 0 ? Math.min(...matches) : before;
6320
+ }
6288
6321
  //#endregion
6289
6322
  //#region src/elements/inputBlockContext.ts
6290
6323
  /** True while rendering the element of an `input` block, where Slack lays some elements out differently. */
@@ -6470,16 +6503,6 @@ function DatePicker({ element, blockId }) {
6470
6503
  }
6471
6504
  //#endregion
6472
6505
  //#region src/elements/DateTimePicker.tsx
6473
- function toISODate(ts) {
6474
- return (/* @__PURE__ */ new Date(ts * 1e3)).toISOString().slice(0, 10);
6475
- }
6476
- function toHM(ts) {
6477
- const d = /* @__PURE__ */ new Date(ts * 1e3);
6478
- return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
6479
- }
6480
- function combine(date, time) {
6481
- return Math.floor((/* @__PURE__ */ new Date(`${date}T${time}:00Z`)).getTime() / 1e3);
6482
- }
6483
6506
  /** Slack's Builder splits a datetimepicker's closed control into a date box ("January 1st,
6484
6507
  * 2026") and a separate time box ("11:00 AM"), rather than one combined string. */
6485
6508
  function formatDate(ts, timeZone) {
@@ -6488,7 +6511,7 @@ function formatDate(ts, timeZone) {
6488
6511
  month: "long",
6489
6512
  day: "numeric",
6490
6513
  year: "numeric",
6491
- timeZone: timeZone ?? "UTC"
6514
+ timeZone
6492
6515
  }).formatToParts(d);
6493
6516
  const month = parts.find((p) => p.type === "month")?.value ?? "";
6494
6517
  const day = Number(parts.find((p) => p.type === "day")?.value ?? "1");
@@ -6499,7 +6522,7 @@ function formatTime$1(ts, timeZone) {
6499
6522
  return (/* @__PURE__ */ new Date(ts * 1e3)).toLocaleString("en-US", {
6500
6523
  hour: "numeric",
6501
6524
  minute: "2-digit",
6502
- timeZone: timeZone ?? "UTC"
6525
+ timeZone
6503
6526
  });
6504
6527
  }
6505
6528
  function DateTimePicker({ element, blockId }) {
@@ -6507,8 +6530,10 @@ function DateTimePicker({ element, blockId }) {
6507
6530
  const { ask, dialog } = useConfirm(element.confirm);
6508
6531
  const [value, setValueState] = useState(element.initial_date_time);
6509
6532
  const [open, setOpen] = useState(false);
6510
- const [draftDate, setDraftDate] = useState(element.initial_date_time ? toISODate(element.initial_date_time) : void 0);
6511
- const [draftTime, setDraftTime] = useState(element.initial_date_time ? toHM(element.initial_date_time) : "12:00");
6533
+ const zone = timeZone ?? "UTC";
6534
+ const initial = element.initial_date_time !== void 0 ? wallClock(element.initial_date_time, zone) : void 0;
6535
+ const [draftDate, setDraftDate] = useState(initial?.date);
6536
+ const [draftTime, setDraftTime] = useState(initial?.time ?? "12:00");
6512
6537
  const rootRef = useRef(null);
6513
6538
  const focusRef = useFocusOnLoad(element);
6514
6539
  const actionId = element.action_id ?? "";
@@ -6521,7 +6546,7 @@ function DateTimePicker({ element, blockId }) {
6521
6546
  async function apply() {
6522
6547
  if (!draftDate) return;
6523
6548
  if (!await ask()) return;
6524
- const ts = combine(draftDate, draftTime);
6549
+ const ts = fromWallClock(draftDate, draftTime, zone);
6525
6550
  setValueState(ts);
6526
6551
  setOpen(false);
6527
6552
  setValue(blockId, actionId, {
@@ -6552,7 +6577,7 @@ function DateTimePicker({ element, blockId }) {
6552
6577
  /* @__PURE__ */ jsx(CalendarIcon, { className: "sbk-datetimepicker__icon" }),
6553
6578
  /* @__PURE__ */ jsx("span", {
6554
6579
  className: value !== void 0 ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
6555
- children: value !== void 0 ? formatDate(value, timeZone) : "Select a date"
6580
+ children: value !== void 0 ? formatDate(value, zone) : "Select a date"
6556
6581
  }),
6557
6582
  /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datetimepicker__chevron" })
6558
6583
  ]
@@ -6567,13 +6592,13 @@ function DateTimePicker({ element, blockId }) {
6567
6592
  /* @__PURE__ */ jsx(ClockIcon, { className: "sbk-datetimepicker__icon" }),
6568
6593
  /* @__PURE__ */ jsx("span", {
6569
6594
  className: value !== void 0 ? "sbk-datetimepicker__value" : "sbk-datetimepicker__placeholder",
6570
- children: value !== void 0 ? formatTime$1(value, timeZone) : "Select a time"
6595
+ children: value !== void 0 ? formatTime$1(value, zone) : "Select a time"
6571
6596
  }),
6572
6597
  /* @__PURE__ */ jsx(ChevronDownIcon, { className: "sbk-datetimepicker__chevron" })
6573
6598
  ]
6574
6599
  }), /* @__PURE__ */ jsxs("p", {
6575
6600
  className: "sbk-datetimepicker__timezone",
6576
- children: ["Time zone: ", timeZoneLabel(timeZone ?? "UTC")]
6601
+ children: ["Time zone: ", timeZoneLabel(zone)]
6577
6602
  })]
6578
6603
  })]
6579
6604
  }),
@@ -9872,6 +9897,24 @@ function DotIcon({ status }) {
9872
9897
  children: /* @__PURE__ */ jsx("path", { d: "M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0" })
9873
9898
  });
9874
9899
  }
9900
+ /** How a status reads aloud. The icons are hidden from assistive technology, so this stands in. */
9901
+ const STATUS_LABELS = {
9902
+ complete: "complete",
9903
+ in_progress: "in progress",
9904
+ pending: "pending",
9905
+ error: "failed"
9906
+ };
9907
+ /**
9908
+ * A status as text that only screen readers get, set after a title (", in progress"), so a toggle
9909
+ * or task header is announced with the state its icon shows. An unknown status says nothing.
9910
+ */
9911
+ function StatusText({ status }) {
9912
+ const label = STATUS_LABELS[status];
9913
+ return label ? /* @__PURE__ */ jsx("span", {
9914
+ className: "sbk-visually-hidden",
9915
+ children: `, ${label}`
9916
+ }) : null;
9917
+ }
9875
9918
  /** The glyph on a toggle bar (the plan's or a task_card's): check, warning or spinner. */
9876
9919
  function StatusIcon({ status }) {
9877
9920
  if (status === "complete") return /* @__PURE__ */ jsx(CheckCircleIcon, {});
@@ -9898,7 +9941,8 @@ function aggregateStatus(statuses) {
9898
9941
  /**
9899
9942
  * One step of the plan. As in Slack, a thin line joins each step's icon to the next, its details
9900
9943
  * start open, and the title is a button that hides and shows them instantly; a task with nothing
9901
- * to reveal gets a disabled header and no caret.
9944
+ * to reveal gets the same header as plain text, with no caret. The status, shown as an icon, is
9945
+ * also in the header as text for screen readers.
9902
9946
  */
9903
9947
  function TaskRow({ task }) {
9904
9948
  const [open, setOpen] = useState(true);
@@ -9919,21 +9963,28 @@ function TaskRow({ task }) {
9919
9963
  ]
9920
9964
  }), /* @__PURE__ */ jsxs("div", {
9921
9965
  className: "sbk-plan__task-content",
9922
- children: [/* @__PURE__ */ jsxs("button", {
9966
+ children: [expandable ? /* @__PURE__ */ jsxs("button", {
9923
9967
  type: "button",
9924
9968
  className: "sbk-plan__task-header",
9925
9969
  "aria-expanded": open,
9926
- "aria-disabled": expandable ? void 0 : true,
9927
- onClick: () => {
9928
- if (expandable) setOpen((value) => !value);
9929
- },
9970
+ onClick: () => setOpen((value) => !value),
9971
+ children: [
9972
+ /* @__PURE__ */ jsx("span", {
9973
+ className: "sbk-plan__task-title",
9974
+ children: task.title
9975
+ }),
9976
+ /* @__PURE__ */ jsx(StatusText, { status: task.status }),
9977
+ /* @__PURE__ */ jsx("span", {
9978
+ className: "sbk-plan__task-caret",
9979
+ children: /* @__PURE__ */ jsx(CaretIcon, { direction: open ? "down" : "right" })
9980
+ })
9981
+ ]
9982
+ }) : /* @__PURE__ */ jsxs("div", {
9983
+ className: "sbk-plan__task-header",
9930
9984
  children: [/* @__PURE__ */ jsx("span", {
9931
9985
  className: "sbk-plan__task-title",
9932
9986
  children: task.title
9933
- }), expandable ? /* @__PURE__ */ jsx("span", {
9934
- className: "sbk-plan__task-caret",
9935
- children: /* @__PURE__ */ jsx(CaretIcon, { direction: open ? "down" : "right" })
9936
- }) : null]
9987
+ }), /* @__PURE__ */ jsx(StatusText, { status: task.status })]
9937
9988
  }), open && expandable ? /* @__PURE__ */ jsxs("div", {
9938
9989
  className: "sbk-plan__task-body",
9939
9990
  children: [
@@ -9982,6 +10033,7 @@ function Plan({ block }) {
9982
10033
  className: "sbk-plan__pill-title",
9983
10034
  children: block.title
9984
10035
  }),
10036
+ /* @__PURE__ */ jsx(StatusText, { status }),
9985
10037
  /* @__PURE__ */ jsx("span", {
9986
10038
  className: "sbk-plan__pill-chevron",
9987
10039
  children: /* @__PURE__ */ jsx(CaretIcon, { direction: expanded ? "down" : "right" })
@@ -10395,33 +10447,35 @@ function TaskCard({ block }) {
10395
10447
  const [expanded, setExpanded] = useState(false);
10396
10448
  const sources = Array.isArray(block.sources) ? block.sources : [];
10397
10449
  const expandable = Boolean(block.details || block.output || sources.length > 0);
10450
+ const pillContent = /* @__PURE__ */ jsxs("span", {
10451
+ className: "sbk-task-card__pill-content",
10452
+ children: [
10453
+ /* @__PURE__ */ jsx("span", {
10454
+ className: "sbk-task-card__pill-icon",
10455
+ children: /* @__PURE__ */ jsx(StatusIcon, { status: block.status })
10456
+ }),
10457
+ /* @__PURE__ */ jsx("span", {
10458
+ className: "sbk-task-card__pill-title",
10459
+ children: block.title
10460
+ }),
10461
+ /* @__PURE__ */ jsx(StatusText, { status: block.status }),
10462
+ expandable ? /* @__PURE__ */ jsx("span", {
10463
+ className: "sbk-task-card__pill-chevron",
10464
+ children: /* @__PURE__ */ jsx(CaretIcon, { direction: expanded ? "down" : "right" })
10465
+ }) : null
10466
+ ]
10467
+ });
10398
10468
  return /* @__PURE__ */ jsxs("div", {
10399
10469
  className: "sbk-task-card",
10400
- children: [/* @__PURE__ */ jsx("button", {
10470
+ children: [expandable ? /* @__PURE__ */ jsx("button", {
10401
10471
  type: "button",
10402
10472
  className: "sbk-task-card__pill",
10403
10473
  "aria-expanded": expanded,
10404
- "aria-disabled": expandable ? void 0 : true,
10405
- onClick: () => {
10406
- if (expandable) setExpanded((value) => !value);
10407
- },
10408
- children: /* @__PURE__ */ jsxs("span", {
10409
- className: "sbk-task-card__pill-content",
10410
- children: [
10411
- /* @__PURE__ */ jsx("span", {
10412
- className: "sbk-task-card__pill-icon",
10413
- children: /* @__PURE__ */ jsx(StatusIcon, { status: block.status })
10414
- }),
10415
- /* @__PURE__ */ jsx("span", {
10416
- className: "sbk-task-card__pill-title",
10417
- children: block.title
10418
- }),
10419
- expandable ? /* @__PURE__ */ jsx("span", {
10420
- className: "sbk-task-card__pill-chevron",
10421
- children: /* @__PURE__ */ jsx(CaretIcon, { direction: expanded ? "down" : "right" })
10422
- }) : null
10423
- ]
10424
- })
10474
+ onClick: () => setExpanded((value) => !value),
10475
+ children: pillContent
10476
+ }) : /* @__PURE__ */ jsx("div", {
10477
+ className: "sbk-task-card__pill",
10478
+ children: pillContent
10425
10479
  }), /* @__PURE__ */ jsx(Collapse, {
10426
10480
  open: expanded,
10427
10481
  children: /* @__PURE__ */ jsxs("div", {