@assistant-ui/react-generative-ui 0.0.22 → 0.0.23

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.
Files changed (74) hide show
  1. package/dist/a2ui/convert.d.ts.map +1 -1
  2. package/dist/a2ui/convert.js +21 -4
  3. package/dist/a2ui/convert.js.map +1 -1
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +2 -1
  7. package/dist/constants.js.map +1 -1
  8. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  9. package/dist/slack/decodeBlockAction.js +64 -26
  10. package/dist/slack/decodeBlockAction.js.map +1 -1
  11. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  12. package/dist/slack/fromSlackBlocks.js +23 -0
  13. package/dist/slack/fromSlackBlocks.js.map +1 -1
  14. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  15. package/dist/slack/toSlackBlocks.js +74 -18
  16. package/dist/slack/toSlackBlocks.js.map +1 -1
  17. package/dist/slack/types.d.ts +20 -1
  18. package/dist/slack/types.d.ts.map +1 -1
  19. package/dist/slack.d.ts +1 -1
  20. package/dist/slack.d.ts.map +1 -1
  21. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  22. package/dist/teams/decodeSubmitData.js +24 -3
  23. package/dist/teams/decodeSubmitData.js.map +1 -1
  24. package/dist/teams/temporalId.d.ts +5 -0
  25. package/dist/teams/temporalId.d.ts.map +1 -0
  26. package/dist/teams/temporalId.js +28 -0
  27. package/dist/teams/temporalId.js.map +1 -0
  28. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  29. package/dist/teams/toAdaptiveCard.js +80 -1
  30. package/dist/teams/toAdaptiveCard.js.map +1 -1
  31. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  32. package/dist/teams/toTeamsAttachments.js.map +1 -1
  33. package/dist/teams/types.d.ts +19 -1
  34. package/dist/teams/types.d.ts.map +1 -1
  35. package/dist/teams.d.ts +1 -1
  36. package/dist/teams.d.ts.map +1 -1
  37. package/dist/temporal.d.ts +33 -0
  38. package/dist/temporal.d.ts.map +1 -0
  39. package/dist/temporal.js +152 -0
  40. package/dist/temporal.js.map +1 -0
  41. package/dist/vocabulary/collectFormValues.d.ts +1 -1
  42. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  43. package/dist/vocabulary/collectFormValues.js +7 -3
  44. package/dist/vocabulary/collectFormValues.js.map +1 -1
  45. package/dist/vocabulary/data.d.ts +1 -1
  46. package/dist/vocabulary/interactive.d.ts +7 -1
  47. package/dist/vocabulary/interactive.d.ts.map +1 -1
  48. package/dist/vocabulary/interactive.js +62 -12
  49. package/dist/vocabulary/interactive.js.map +1 -1
  50. package/package.json +5 -5
  51. package/src/a2ui/convert.test.ts +223 -14
  52. package/src/a2ui/convert.ts +41 -7
  53. package/src/constants.ts +1 -0
  54. package/src/slack/datePicker.test.ts +612 -0
  55. package/src/slack/decodeBlockAction.ts +114 -41
  56. package/src/slack/fromSlackBlocks.ts +29 -0
  57. package/src/slack/toSlackBlocks.ts +123 -36
  58. package/src/slack/types.ts +24 -0
  59. package/src/slack.ts +2 -0
  60. package/src/teams/decodeSubmitData.test.ts +216 -0
  61. package/src/teams/decodeSubmitData.ts +49 -4
  62. package/src/teams/temporal.test.ts +766 -0
  63. package/src/teams/temporalId.ts +56 -0
  64. package/src/teams/toAdaptiveCard.ts +166 -2
  65. package/src/teams/toTeamsAttachments.ts +4 -1
  66. package/src/teams/types.ts +17 -0
  67. package/src/teams.ts +1 -0
  68. package/src/temporal.test.ts +319 -0
  69. package/src/temporal.ts +259 -0
  70. package/src/vocabulary/collectFormValues.test.ts +198 -1
  71. package/src/vocabulary/collectFormValues.ts +31 -2
  72. package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
  73. package/src/vocabulary/interactive.dom.test.tsx +59 -11
  74. package/src/vocabulary/interactive.tsx +151 -15
@@ -12,6 +12,11 @@ import { createActionRegistry, type ActionHandler } from "../actionRegistry";
12
12
  import { AnsweredValuesProvider } from "../answeredValues";
13
13
  import type { GenerativeUIDispatch, GenerativeUILibrary } from "../types";
14
14
  import { renderGenerativeUI } from "../renderGenerativeUI";
15
+ import {
16
+ fromLocalDateTime,
17
+ normalizeTemporalInputValue,
18
+ toLocalDateTime,
19
+ } from "../temporal";
15
20
  import { defaultGenerativeUILibrary } from "./index";
16
21
 
17
22
  (
@@ -1533,11 +1538,19 @@ describe.each([
1533
1538
  type: "number",
1534
1539
  },
1535
1540
  {
1536
- title: "date and time",
1541
+ title: "floating date and time",
1537
1542
  field: { component: "DateTimeInput", enableDate: true, enableTime: true },
1538
- initial: "2025-12-15T17:00:00Z",
1539
- edited: "2025-12-16T08:30:45+02:00",
1540
- type: "text",
1543
+ initial: "2025-12-15T17:00",
1544
+ edited: "2025-12-16T08:30",
1545
+ type: "datetime-local",
1546
+ },
1547
+ {
1548
+ title: "floating date and time with seconds",
1549
+ field: { component: "DateTimeInput", enableDate: true, enableTime: true },
1550
+ initial: "2025-12-15T17:00:00",
1551
+ displayed: "2025-12-15T17:00",
1552
+ edited: "2025-12-16T08:30",
1553
+ type: "datetime-local",
1541
1554
  },
1542
1555
  {
1543
1556
  title: "timestamp in date mode",
@@ -1550,8 +1563,17 @@ describe.each([
1550
1563
  title: "time only",
1551
1564
  field: { component: "DateTimeInput", enableTime: true },
1552
1565
  initial: "17:00:00",
1566
+ displayed: "17:00",
1553
1567
  edited: "08:30:45",
1554
- type: "text",
1568
+ type: "time",
1569
+ },
1570
+ {
1571
+ title: "fractional time",
1572
+ field: { component: "DateTimeInput", enableTime: true },
1573
+ initial: "17:00:00.000",
1574
+ displayed: "17:00",
1575
+ edited: "08:30",
1576
+ type: "time",
1555
1577
  },
1556
1578
  {
1557
1579
  title: "omitted temporal flags",
@@ -1593,15 +1615,15 @@ describe.each([
1593
1615
  type: "number",
1594
1616
  },
1595
1617
  {
1596
- title: "unresolved date and time",
1597
- field: { component: "DateTimeInput", enableDate: true, enableTime: true },
1618
+ title: "unresolved time",
1619
+ field: { component: "DateTimeInput", enableTime: true },
1598
1620
  initial: undefined,
1599
- edited: "2025-12-15T17:00:00Z",
1600
- type: "text",
1621
+ edited: "08:30",
1622
+ type: "time",
1601
1623
  },
1602
1624
  ])(
1603
1625
  "preserves untouched, edited, and cleared $title values",
1604
- async ({ field, initial, edited, type }) => {
1626
+ async ({ field, initial, displayed, edited, type }) => {
1605
1627
  const { container, render, submit } = await mountSurface(field, initial);
1606
1628
  const input = () =>
1607
1629
  container.querySelector<HTMLInputElement>('input[name="/form/value"]')!;
@@ -1620,7 +1642,7 @@ describe.each([
1620
1642
 
1621
1643
  expect(input().type).toBe(type);
1622
1644
  expect(input().getAttribute("aria-label")).toBe("Value");
1623
- expect(input().value).toBe(initial ?? "");
1645
+ expect(input().value).toBe(displayed ?? initial ?? "");
1624
1646
  if (type === "number") expect(input().step).toBe("any");
1625
1647
  await submit(initial ?? "");
1626
1648
 
@@ -1646,6 +1668,32 @@ describe.each([
1646
1668
  },
1647
1669
  );
1648
1670
 
1671
+ it("preserves an instant offset in the A2UI button context", async () => {
1672
+ const initial = "2025-12-15T17:00:00+02:00";
1673
+ const { container, submit } = await mountSurface(
1674
+ { component: "DateTimeInput", enableDate: true, enableTime: true },
1675
+ initial,
1676
+ );
1677
+ const input = container.querySelector<HTMLInputElement>(
1678
+ 'input[name="/form/value"]',
1679
+ )!;
1680
+ expect(input.type).toBe("datetime-local");
1681
+ expect(input.value).toBe(
1682
+ normalizeTemporalInputValue(toLocalDateTime(initial)),
1683
+ );
1684
+ await submit(initial);
1685
+
1686
+ await act(async () => {
1687
+ Object.getOwnPropertyDescriptor(
1688
+ HTMLInputElement.prototype,
1689
+ "value",
1690
+ )!.set!.call(input, "2025-12-16T09:30");
1691
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1692
+ });
1693
+ expect(input.value).toBe("2025-12-16T09:30");
1694
+ await submit(fromLocalDateTime("2025-12-16T09:30", initial));
1695
+ });
1696
+
1649
1697
  it.each([undefined, "Pick one"])(
1650
1698
  "keeps chips empty until selected with placeholder %s",
1651
1699
  async (placeholder) => {
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import {
4
4
  CHECKBOX_GROUP_ATTR,
5
5
  FIELD_NAME_ATTR,
6
+ FIELD_VALUE_ATTR,
6
7
  GENERATED_NAME_ATTR,
7
8
  } from "../constants";
8
9
  import type { Action } from "../ir";
@@ -17,6 +18,14 @@ import { useAnsweredValue } from "../answeredValues";
17
18
  import { actionAttr, fire } from "./dispatch";
18
19
  import { toTextContent } from "./toTextContent";
19
20
  import { useRadioGroupName } from "../RadioGroupScope";
21
+ import {
22
+ classifyTemporal,
23
+ fromLocalDateTime,
24
+ getTemporalInputStep,
25
+ normalizeTemporalInputValue,
26
+ toLocalDateTime,
27
+ toPickerLocalDateTime,
28
+ } from "../temporal";
20
29
 
21
30
  const optionSchema = z.object({
22
31
  label: z.string(),
@@ -368,6 +377,7 @@ function InputRender({
368
377
  }
369
378
 
370
379
  type DatePickerRenderProps = {
380
+ inputType?: "date" | "datetime" | "time";
371
381
  value?: string;
372
382
  min?: string;
373
383
  max?: string;
@@ -378,7 +388,12 @@ type DatePickerRenderProps = {
378
388
  $dispatch?: GenerativeUIDispatch;
379
389
  };
380
390
 
391
+ const subscribeToHydration = () => () => {};
392
+ const clientHydrationSnapshot = () => true;
393
+ const serverHydrationSnapshot = () => false;
394
+
381
395
  function DatePickerRender({
396
+ inputType = "date",
382
397
  value,
383
398
  min,
384
399
  max,
@@ -392,25 +407,125 @@ function DatePickerRender({
392
407
  const isBound = updateBinding !== undefined;
393
408
  const initialValue =
394
409
  typeof answeredValue === "string" ? answeredValue : value;
410
+ const [selection, setSelection] = React.useState({
411
+ initialValue,
412
+ value: initialValue,
413
+ anchor: initialValue,
414
+ });
415
+ if (selection.initialValue !== initialValue) {
416
+ setSelection({
417
+ initialValue,
418
+ value: initialValue,
419
+ anchor:
420
+ isBound && initialValue === selection.value
421
+ ? selection.anchor
422
+ : initialValue,
423
+ });
424
+ }
425
+ const currentValue = isBound ? initialValue : selection.value;
426
+ const temporal = classifyTemporal(currentValue ?? "");
427
+ const anchor = classifyTemporal(selection.anchor ?? "");
428
+ const hasInstantAnchor =
429
+ inputType === "datetime" && anchor.kind === "instant";
430
+ const canonicalFieldValue = (value: string | undefined) => {
431
+ const current = value ?? "";
432
+ const kind = classifyTemporal(current).kind;
433
+ if (inputType === "time")
434
+ return kind === "time" && normalizeTemporalInputValue(current) !== current
435
+ ? value
436
+ : undefined;
437
+ if (inputType !== "datetime") return undefined;
438
+ if (kind === "instant") return value;
439
+ if (hasInstantAnchor) return selection.anchor;
440
+ return kind === "floating" &&
441
+ normalizeTemporalInputValue(current) !== current
442
+ ? value
443
+ : undefined;
444
+ };
445
+ const minimum = classifyTemporal(min ?? "");
446
+ const maximum = classifyTemporal(max ?? "");
447
+ const hydrated = React.useSyncExternalStore(
448
+ subscribeToHydration,
449
+ clientHydrationSnapshot,
450
+ serverHydrationSnapshot,
451
+ );
452
+ const showRawValue =
453
+ inputType === "datetime" && temporal.kind === "instant" && !hydrated;
454
+ const displayValue =
455
+ currentValue === undefined
456
+ ? undefined
457
+ : inputType === "datetime" && hydrated
458
+ ? toPickerLocalDateTime(currentValue)
459
+ : normalizeTemporalInputValue(currentValue);
395
460
  return (
396
461
  <input
397
462
  key={isBound ? undefined : initialValue}
398
- type="date"
463
+ type={
464
+ showRawValue
465
+ ? "text"
466
+ : inputType === "datetime"
467
+ ? "datetime-local"
468
+ : inputType
469
+ }
470
+ readOnly={showRawValue || undefined}
399
471
  data-aui="datepicker"
400
472
  data-aui-action={actionAttr($action)}
473
+ {...{
474
+ [FIELD_VALUE_ATTR]: canonicalFieldValue(currentValue),
475
+ }}
401
476
  name={name}
402
477
  aria-label={label}
403
- {...(isBound
404
- ? {
405
- value:
406
- typeof answeredValue === "string" ? answeredValue : (value ?? ""),
407
- }
408
- : { defaultValue: initialValue })}
409
- min={min}
410
- max={max}
478
+ {...(isBound || inputType === "datetime"
479
+ ? { value: displayValue ?? "" }
480
+ : { defaultValue: displayValue })}
481
+ min={
482
+ inputType === "datetime" && hydrated && min !== undefined
483
+ ? normalizeTemporalInputValue(
484
+ toLocalDateTime(
485
+ min,
486
+ minimum.kind === "instant" ? minimum.precision : undefined,
487
+ ),
488
+ )
489
+ : min === undefined
490
+ ? undefined
491
+ : normalizeTemporalInputValue(min)
492
+ }
493
+ max={
494
+ inputType === "datetime" && hydrated && max !== undefined
495
+ ? normalizeTemporalInputValue(
496
+ toLocalDateTime(
497
+ max,
498
+ maximum.kind === "instant" ? maximum.precision : undefined,
499
+ ),
500
+ )
501
+ : max === undefined
502
+ ? undefined
503
+ : normalizeTemporalInputValue(max)
504
+ }
505
+ step={
506
+ inputType !== "date"
507
+ ? getTemporalInputStep(selection.anchor, currentValue, min, max)
508
+ : undefined
509
+ }
411
510
  onChange={(e) => {
412
- updateBinding?.(e.currentTarget.value);
413
- fire($action, $dispatch, e.currentTarget.value, e.currentTarget);
511
+ const inputValue = normalizeTemporalInputValue(e.currentTarget.value);
512
+ const nextValue =
513
+ inputType === "datetime"
514
+ ? fromLocalDateTime(inputValue, selection.anchor)
515
+ : inputValue;
516
+ const canonical = canonicalFieldValue(nextValue);
517
+ if (canonical !== undefined) {
518
+ e.currentTarget.setAttribute(FIELD_VALUE_ATTR, canonical);
519
+ } else {
520
+ e.currentTarget.removeAttribute(FIELD_VALUE_ATTR);
521
+ }
522
+ setSelection({
523
+ initialValue,
524
+ value: nextValue,
525
+ anchor: selection.anchor,
526
+ });
527
+ updateBinding?.(nextValue);
528
+ fire($action, $dispatch, nextValue, e.currentTarget);
414
529
  }}
415
530
  />
416
531
  );
@@ -837,11 +952,32 @@ export const interactiveVocabulary = {
837
952
  },
838
953
  DatePicker: {
839
954
  description:
840
- "A date input. Carries `$action` describing the on-select behavior.",
955
+ "A date, datetime, or time input. Carries `$action` describing the on-select behavior.",
841
956
  properties: z.object({
842
- value: z.string().optional().describe("Initial date (YYYY-MM-DD)."),
843
- min: z.string().optional().describe("Minimum date (YYYY-MM-DD)."),
844
- max: z.string().optional().describe("Maximum date (YYYY-MM-DD)."),
957
+ inputType: z
958
+ .enum(["date", "datetime", "time"])
959
+ .optional()
960
+ .describe(
961
+ 'Temporal input type. Defaults to "date". Values remain strings.',
962
+ ),
963
+ value: z
964
+ .string()
965
+ .optional()
966
+ .describe(
967
+ "Initial value: YYYY-MM-DD for date, HH:mm or HH:mm:ss with optional fraction of at most 9 digits for time, YYYY-MM-DDTHH:mm with optional :ss and fraction of at most 9 digits for datetime. A datetime with Z or ±HH:mm is an instant, displayed in the viewer's time zone and submitted with the same offset and precision. A datetime without an offset is local and submitted unchanged. An empty datetime submits with the viewer's offset and seconds.",
968
+ ),
969
+ min: z
970
+ .string()
971
+ .optional()
972
+ .describe(
973
+ "Minimum value: YYYY-MM-DD for date, HH:mm or HH:mm:ss with optional fraction of at most 9 digits for time, YYYY-MM-DDTHH:mm with optional :ss and fraction of at most 9 digits for datetime. Datetimes with Z or ±HH:mm are converted to the viewer's time zone.",
974
+ ),
975
+ max: z
976
+ .string()
977
+ .optional()
978
+ .describe(
979
+ "Maximum value: YYYY-MM-DD for date, HH:mm or HH:mm:ss with optional fraction of at most 9 digits for time, YYYY-MM-DDTHH:mm with optional :ss and fraction of at most 9 digits for datetime. Datetimes with Z or ±HH:mm are converted to the viewer's time zone.",
980
+ ),
845
981
  label: z
846
982
  .string()
847
983
  .optional()