@assistant-ui/react-generative-ui 0.0.21 → 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 (212) hide show
  1. package/LICENSE +1 -1
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +30 -13
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/RadioGroupScope.d.ts +6 -0
  6. package/dist/RadioGroupScope.d.ts.map +1 -0
  7. package/dist/RadioGroupScope.js +25 -0
  8. package/dist/RadioGroupScope.js.map +1 -0
  9. package/dist/a2ui/BindingContext.d.ts +11 -0
  10. package/dist/a2ui/BindingContext.d.ts.map +1 -0
  11. package/dist/a2ui/BindingContext.js +12 -0
  12. package/dist/a2ui/BindingContext.js.map +1 -0
  13. package/dist/a2ui/PresentRenderer.d.ts +11 -0
  14. package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
  15. package/dist/a2ui/PresentRenderer.js +39 -0
  16. package/dist/a2ui/PresentRenderer.js.map +1 -0
  17. package/dist/a2ui/convert.d.ts +6 -0
  18. package/dist/a2ui/convert.d.ts.map +1 -1
  19. package/dist/a2ui/convert.js +363 -85
  20. package/dist/a2ui/convert.js.map +1 -1
  21. package/dist/a2ui/dataModel.d.ts +9 -0
  22. package/dist/a2ui/dataModel.d.ts.map +1 -0
  23. package/dist/a2ui/dataModel.js +44 -0
  24. package/dist/a2ui/dataModel.js.map +1 -0
  25. package/dist/a2ui/reducer.d.ts +5 -0
  26. package/dist/a2ui/reducer.d.ts.map +1 -1
  27. package/dist/a2ui/reducer.js +3 -3
  28. package/dist/a2ui/reducer.js.map +1 -1
  29. package/dist/a2ui/types.d.ts +8 -2
  30. package/dist/a2ui/types.d.ts.map +1 -1
  31. package/dist/a2ui/types.js.map +1 -1
  32. package/dist/a2ui/valueFunctions.d.ts +16 -0
  33. package/dist/a2ui/valueFunctions.d.ts.map +1 -0
  34. package/dist/a2ui/valueFunctions.js +332 -0
  35. package/dist/a2ui/valueFunctions.js.map +1 -0
  36. package/dist/actionRegistry.d.ts +1 -1
  37. package/dist/actionRegistry.js.map +1 -1
  38. package/dist/answeredValues.d.ts +16 -0
  39. package/dist/answeredValues.d.ts.map +1 -0
  40. package/dist/answeredValues.js +37 -0
  41. package/dist/answeredValues.js.map +1 -0
  42. package/dist/constants.d.ts +3 -0
  43. package/dist/constants.d.ts.map +1 -1
  44. package/dist/constants.js +4 -1
  45. package/dist/constants.js.map +1 -1
  46. package/dist/fieldReferences.d.ts +9 -0
  47. package/dist/fieldReferences.d.ts.map +1 -0
  48. package/dist/fieldReferences.js +53 -0
  49. package/dist/fieldReferences.js.map +1 -0
  50. package/dist/generativeUIToJSX.js +3 -3
  51. package/dist/generativeUIToJSX.js.map +1 -1
  52. package/dist/index.js +2 -2
  53. package/dist/index.server.js +2 -2
  54. package/dist/index.shared.js +2 -2
  55. package/dist/ir.d.ts +1 -1
  56. package/dist/ir.js.map +1 -1
  57. package/dist/renderGenerativeUI.d.ts.map +1 -1
  58. package/dist/renderGenerativeUI.js +12 -1
  59. package/dist/renderGenerativeUI.js.map +1 -1
  60. package/dist/slack/constants.d.ts +2 -0
  61. package/dist/slack/constants.d.ts.map +1 -1
  62. package/dist/slack/constants.js +3 -1
  63. package/dist/slack/constants.js.map +1 -1
  64. package/dist/slack/decodeBlockAction.d.ts +3 -1
  65. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  66. package/dist/slack/decodeBlockAction.js +96 -7
  67. package/dist/slack/decodeBlockAction.js.map +1 -1
  68. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  69. package/dist/slack/fromSlackBlocks.js +43 -3
  70. package/dist/slack/fromSlackBlocks.js.map +1 -1
  71. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  72. package/dist/slack/toSlackBlocks.js +244 -20
  73. package/dist/slack/toSlackBlocks.js.map +1 -1
  74. package/dist/slack/types.d.ts +24 -1
  75. package/dist/slack/types.d.ts.map +1 -1
  76. package/dist/slack.d.ts +1 -1
  77. package/dist/slack.d.ts.map +1 -1
  78. package/dist/teams/constants.d.ts +1 -1
  79. package/dist/teams/constants.d.ts.map +1 -1
  80. package/dist/teams/constants.js +1 -1
  81. package/dist/teams/constants.js.map +1 -1
  82. package/dist/teams/decodeSubmitData.d.ts +4 -0
  83. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  84. package/dist/teams/decodeSubmitData.js +29 -3
  85. package/dist/teams/decodeSubmitData.js.map +1 -1
  86. package/dist/teams/temporalId.d.ts +5 -0
  87. package/dist/teams/temporalId.d.ts.map +1 -0
  88. package/dist/teams/temporalId.js +28 -0
  89. package/dist/teams/temporalId.js.map +1 -0
  90. package/dist/teams/toAdaptiveCard.d.ts +5 -4
  91. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  92. package/dist/teams/toAdaptiveCard.js +136 -18
  93. package/dist/teams/toAdaptiveCard.js.map +1 -1
  94. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  95. package/dist/teams/toTeamsAttachments.js.map +1 -1
  96. package/dist/teams/types.d.ts +23 -2
  97. package/dist/teams/types.d.ts.map +1 -1
  98. package/dist/teams.d.ts +1 -1
  99. package/dist/teams.d.ts.map +1 -1
  100. package/dist/temporal.d.ts +33 -0
  101. package/dist/temporal.d.ts.map +1 -0
  102. package/dist/temporal.js +152 -0
  103. package/dist/temporal.js.map +1 -0
  104. package/dist/vocabulary/alert.d.ts +9 -2
  105. package/dist/vocabulary/alert.d.ts.map +1 -1
  106. package/dist/vocabulary/alert.js +116 -19
  107. package/dist/vocabulary/alert.js.map +1 -1
  108. package/dist/vocabulary/collectFormValues.d.ts +2 -1
  109. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  110. package/dist/vocabulary/collectFormValues.js +15 -4
  111. package/dist/vocabulary/collectFormValues.js.map +1 -1
  112. package/dist/vocabulary/data.d.ts +22 -5
  113. package/dist/vocabulary/data.d.ts.map +1 -1
  114. package/dist/vocabulary/data.js +278 -83
  115. package/dist/vocabulary/data.js.map +1 -1
  116. package/dist/vocabulary/dispatch.d.ts +3 -2
  117. package/dist/vocabulary/dispatch.d.ts.map +1 -1
  118. package/dist/vocabulary/dispatch.js +33 -4
  119. package/dist/vocabulary/dispatch.js.map +1 -1
  120. package/dist/vocabulary/fact.d.ts +8 -1
  121. package/dist/vocabulary/fact.d.ts.map +1 -1
  122. package/dist/vocabulary/fact.js +37 -11
  123. package/dist/vocabulary/fact.js.map +1 -1
  124. package/dist/vocabulary/form.d.ts.map +1 -1
  125. package/dist/vocabulary/form.js +3 -3
  126. package/dist/vocabulary/form.js.map +1 -1
  127. package/dist/vocabulary/formatValue.d.ts +7 -0
  128. package/dist/vocabulary/formatValue.d.ts.map +1 -0
  129. package/dist/vocabulary/formatValue.js +76 -0
  130. package/dist/vocabulary/formatValue.js.map +1 -0
  131. package/dist/vocabulary/icon.d.ts +1 -1
  132. package/dist/vocabulary/interactive.d.ts +143 -8
  133. package/dist/vocabulary/interactive.d.ts.map +1 -1
  134. package/dist/vocabulary/interactive.js +458 -107
  135. package/dist/vocabulary/interactive.js.map +1 -1
  136. package/dist/vocabulary/layout.d.ts.map +1 -1
  137. package/dist/vocabulary/layout.js +4 -4
  138. package/dist/vocabulary/layout.js.map +1 -1
  139. package/dist/vocabulary/list.js +2 -2
  140. package/dist/vocabulary/list.js.map +1 -1
  141. package/dist/vocabulary/text.d.ts +1 -1
  142. package/package.json +8 -5
  143. package/src/JSONGenerativeUI.client.tsx +84 -40
  144. package/src/JSONGenerativeUI.test.tsx +964 -68
  145. package/src/RadioGroupScope.server.test.tsx +24 -0
  146. package/src/RadioGroupScope.tsx +34 -0
  147. package/src/a2ui/BindingContext.ts +26 -0
  148. package/src/a2ui/PresentRenderer.tsx +83 -0
  149. package/src/a2ui/convert.test.ts +2054 -229
  150. package/src/a2ui/convert.ts +665 -134
  151. package/src/a2ui/dataModel.ts +69 -0
  152. package/src/a2ui/reducer.test.ts +35 -0
  153. package/src/a2ui/reducer.ts +5 -4
  154. package/src/a2ui/types.ts +9 -2
  155. package/src/a2ui/valueFunctions.test.ts +271 -0
  156. package/src/a2ui/valueFunctions.ts +523 -0
  157. package/src/actionRegistry.ts +1 -1
  158. package/src/answeredValues.test.ts +33 -0
  159. package/src/answeredValues.tsx +70 -0
  160. package/src/constants.ts +5 -0
  161. package/src/fieldReferences.test.ts +66 -0
  162. package/src/fieldReferences.ts +83 -0
  163. package/src/generativeUIToJSX.test.ts +18 -0
  164. package/src/generativeUIToJSX.ts +4 -6
  165. package/src/index.server.test.ts +106 -0
  166. package/src/ir.ts +1 -1
  167. package/src/renderGenerativeUI.test.tsx +14 -5
  168. package/src/renderGenerativeUI.tsx +30 -1
  169. package/src/slack/a2uiSlack.test.ts +198 -0
  170. package/src/slack/constants.ts +3 -0
  171. package/src/slack/datePicker.test.ts +612 -0
  172. package/src/slack/decodeBlockAction.test.ts +98 -0
  173. package/src/slack/decodeBlockAction.ts +182 -16
  174. package/src/slack/fromSlackBlocks.test.ts +222 -3
  175. package/src/slack/fromSlackBlocks.ts +82 -2
  176. package/src/slack/toSlackBlocks.test.ts +682 -3
  177. package/src/slack/toSlackBlocks.ts +463 -25
  178. package/src/slack/types.ts +28 -0
  179. package/src/slack.ts +2 -0
  180. package/src/teams/constants.ts +1 -1
  181. package/src/teams/decodeSubmitData.test.ts +249 -0
  182. package/src/teams/decodeSubmitData.ts +57 -4
  183. package/src/teams/temporal.test.ts +766 -0
  184. package/src/teams/temporalId.ts +56 -0
  185. package/src/teams/toAdaptiveCard.test.ts +219 -0
  186. package/src/teams/toAdaptiveCard.ts +259 -21
  187. package/src/teams/toTeamsAttachments.ts +4 -1
  188. package/src/teams/types.ts +21 -1
  189. package/src/teams.ts +1 -0
  190. package/src/temporal.test.ts +319 -0
  191. package/src/temporal.ts +259 -0
  192. package/src/vocabulary/alert.test.tsx +288 -7
  193. package/src/vocabulary/alert.tsx +168 -26
  194. package/src/vocabulary/collectFormValues.test.ts +225 -1
  195. package/src/vocabulary/collectFormValues.ts +54 -5
  196. package/src/vocabulary/data.test.tsx +248 -6
  197. package/src/vocabulary/data.tsx +398 -100
  198. package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
  199. package/src/vocabulary/dispatch.ts +63 -2
  200. package/src/vocabulary/fact.test.tsx +67 -0
  201. package/src/vocabulary/fact.tsx +40 -9
  202. package/src/vocabulary/form.tsx +7 -2
  203. package/src/vocabulary/formatValue.test.ts +72 -0
  204. package/src/vocabulary/formatValue.ts +127 -0
  205. package/src/vocabulary/index.test.ts +2 -0
  206. package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
  207. package/src/vocabulary/interactive.dom.test.tsx +1747 -0
  208. package/src/vocabulary/interactive.test.tsx +60 -4
  209. package/src/vocabulary/interactive.tsx +910 -155
  210. package/src/vocabulary/layout.test.tsx +6 -4
  211. package/src/vocabulary/layout.tsx +13 -2
  212. package/src/vocabulary/list.tsx +2 -2
@@ -11,6 +11,16 @@ import {
11
11
  type NormalizedUIElement,
12
12
  type NormalizedUINode,
13
13
  } from "../ir";
14
+ import {
15
+ classifyTemporal,
16
+ splitTemporalMinutes,
17
+ temporalOffsetLabel,
18
+ } from "../temporal";
19
+ import {
20
+ factTrend,
21
+ formatFactDelta,
22
+ formatValue,
23
+ } from "../vocabulary/formatValue";
14
24
  import {
15
25
  CHOICE_OPTION_CAP,
16
26
  PAYLOAD_SOFT_CAP,
@@ -21,6 +31,7 @@ import {
21
31
  buildSubmitAction,
22
32
  utf8ByteLength,
23
33
  } from "./constants";
34
+ import { encodeTemporalInputId } from "./temporalId";
24
35
  import type {
25
36
  AdaptiveCardResult,
26
37
  TeamsActionSet,
@@ -52,6 +63,9 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
52
63
  const asString = (value: unknown): string =>
53
64
  typeof value === "string" ? value : "";
54
65
 
66
+ const asFiniteNumber = (value: unknown): number | undefined =>
67
+ typeof value === "number" && Number.isFinite(value) ? value : undefined;
68
+
55
69
  const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
56
70
 
57
71
  const SUBTLE_COLORS = new Set([
@@ -100,7 +114,12 @@ function reservedSafeId(
100
114
  context: ConversionContext,
101
115
  ): string {
102
116
  const reserved = id === RESERVED_INPUT_ID;
103
- const base = reserved ? `${RESERVED_INPUT_ID}_` : id;
117
+ const temporalReserved = /^_*aui:datetime:/.test(id);
118
+ const base = reserved
119
+ ? `${RESERVED_INPUT_ID}_`
120
+ : temporalReserved
121
+ ? `_${id}`
122
+ : id;
104
123
  let candidate = base;
105
124
  let n = 2;
106
125
  while (context.usedInputIds.has(candidate)) {
@@ -114,6 +133,13 @@ function reservedSafeId(
114
133
  component,
115
134
  `the input id "${RESERVED_INPUT_ID}" collides with the submit envelope's reserved key and was renamed to "${candidate}".`,
116
135
  );
136
+ } else if (temporalReserved) {
137
+ warn(
138
+ context,
139
+ "fallback",
140
+ component,
141
+ `the input id "${id}" collides with the reserved datetime namespace and was renamed to "${candidate}".`,
142
+ );
117
143
  } else if (candidate !== base) {
118
144
  warn(
119
145
  context,
@@ -171,7 +197,14 @@ const toChoice = (option: unknown): TeamsInputChoice | undefined => {
171
197
  if (!isRecord(option) || typeof option["value"] !== "string") {
172
198
  return undefined;
173
199
  }
174
- return { title: asString(option["label"]), value: option["value"] };
200
+ const label = asString(option["label"]);
201
+ const description = asString(option["description"]);
202
+ return {
203
+ title: description
204
+ ? [label, description].filter(Boolean).join(": ")
205
+ : label,
206
+ value: option["value"],
207
+ };
175
208
  };
176
209
 
177
210
  const choicesFrom = (
@@ -206,10 +239,18 @@ const choicesFrom = (
206
239
  };
207
240
 
208
241
  function convertFacts(facts: readonly NormalizedUIElement[]): TeamsCardElement {
209
- const set: TeamsFact[] = facts.map((fact) => ({
210
- title: asString(fact.props["label"]),
211
- value: asString(fact.props["value"]),
212
- }));
242
+ const set: TeamsFact[] = facts.map((fact) => {
243
+ const value = asString(fact.props["value"]);
244
+ const delta = fact.props["delta"];
245
+ const deltaText =
246
+ typeof delta === "string"
247
+ ? formatFactDelta(delta, factTrend(delta, fact.props["trend"]))
248
+ : undefined;
249
+ return {
250
+ title: asString(fact.props["label"]),
251
+ value: deltaText === undefined ? value : `${value} (${deltaText})`,
252
+ };
253
+ });
213
254
  return { type: "FactSet", facts: set };
214
255
  }
215
256
 
@@ -277,13 +318,6 @@ const ALERT_STYLE_MAP: Record<string, TeamsContainerStyle> = {
277
318
  danger: "attention",
278
319
  };
279
320
 
280
- const stringifyCell = (value: unknown): string => {
281
- if (typeof value === "string") return value;
282
- if (typeof value === "number" || typeof value === "boolean")
283
- return String(value);
284
- return "";
285
- };
286
-
287
321
  function convertTable(
288
322
  props: Readonly<Record<string, unknown>>,
289
323
  context: ConversionContext,
@@ -342,9 +376,18 @@ function convertTable(
342
376
  cells: (Array.isArray(row)
343
377
  ? copyBounded(row, TABLE_COLUMN_CAP).items
344
378
  : []
345
- ).map((cell) => ({
379
+ ).map((cell, index) => ({
346
380
  type: "TableCell" as const,
347
- items: [textBlock(stringifyCell(cell))],
381
+ items: [
382
+ textBlock(
383
+ formatValue(
384
+ cell,
385
+ isRecord(rawColumns[index])
386
+ ? rawColumns[index]["format"]
387
+ : undefined,
388
+ ),
389
+ ),
390
+ ],
348
391
  })),
349
392
  }));
350
393
 
@@ -465,11 +508,15 @@ export function convertElement(
465
508
  const name = asString(props["name"]);
466
509
  const placeholder = asString(props["placeholder"]);
467
510
  const label = asString(props["label"]);
511
+ const defaultValue = props["defaultValue"];
468
512
  const input: TeamsCardElement = {
469
513
  type: "Input.ChoiceSet",
470
514
  id: reservedSafeId(name || "select", "Select", context),
471
515
  style: "compact",
472
516
  choices: choicesFrom(props["options"], "Select", context),
517
+ ...(typeof defaultValue === "string" && defaultValue
518
+ ? { value: defaultValue }
519
+ : {}),
473
520
  ...(placeholder ? { placeholder } : {}),
474
521
  ...(label ? { label } : {}),
475
522
  };
@@ -491,6 +538,26 @@ export function convertElement(
491
538
  };
492
539
  return withCompanionSubmit(element, input, context);
493
540
  }
541
+ case "CheckboxGroup": {
542
+ const name = asString(props["name"]);
543
+ const label = asString(props["label"]);
544
+ const checked = Array.isArray(props["defaultValue"])
545
+ ? props["defaultValue"].filter(
546
+ (value): value is string =>
547
+ typeof value === "string" && value !== "",
548
+ )
549
+ : [];
550
+ const input: TeamsCardElement = {
551
+ type: "Input.ChoiceSet",
552
+ id: reservedSafeId(name || "checkboxgroup", "CheckboxGroup", context),
553
+ style: "expanded",
554
+ isMultiSelect: true,
555
+ choices: choicesFrom(props["options"], "CheckboxGroup", context),
556
+ ...(checked.length > 0 ? { value: checked.join(",") } : {}),
557
+ ...(label ? { label } : {}),
558
+ };
559
+ return withCompanionSubmit(element, input, context);
560
+ }
494
561
  case "Checkbox": {
495
562
  const name = asString(props["name"]);
496
563
  const label = asString(props["label"]);
@@ -504,22 +571,189 @@ export function convertElement(
504
571
  };
505
572
  return withCompanionSubmit(element, input, context);
506
573
  }
574
+ case "Slider": {
575
+ const name = asString(props["name"]);
576
+ const label = asString(props["label"]);
577
+ const min = asFiniteNumber(props["min"]);
578
+ const max = asFiniteNumber(props["max"]);
579
+ const defaultValue = asFiniteNumber(props["defaultValue"]);
580
+ const input = {
581
+ type: "Input.Number",
582
+ id: reservedSafeId(name || "slider", "Slider", context),
583
+ ...(label ? { label } : {}),
584
+ ...(min !== undefined ? { min } : {}),
585
+ ...(max !== undefined ? { max } : {}),
586
+ ...(defaultValue !== undefined ? { value: defaultValue } : {}),
587
+ } as unknown as TeamsCardElement;
588
+ return withCompanionSubmit(element, input, context);
589
+ }
507
590
  case "Input": {
508
591
  const name = asString(props["name"]);
509
592
  const label = asString(props["label"]);
510
593
  const placeholder = asString(props["placeholder"]);
594
+ const defaultValue = props["defaultValue"];
511
595
  const input: TeamsCardElement = {
512
596
  type: "Input.Text",
513
597
  id: reservedSafeId(name || "input", "Input", context),
598
+ ...(props["inputType"] === "password" ? { style: "password" } : {}),
514
599
  ...(label ? { label } : {}),
515
600
  ...(placeholder ? { placeholder } : {}),
516
- ...(props["multiline"] === true ? { isMultiline: true } : {}),
601
+ ...(typeof defaultValue === "string" && defaultValue
602
+ ? { value: defaultValue }
603
+ : {}),
604
+ ...(props["multiline"] === true &&
605
+ (props["inputType"] === undefined || props["inputType"] === "text")
606
+ ? { isMultiline: true }
607
+ : {}),
517
608
  };
518
609
  return withCompanionSubmit(element, input, context);
519
610
  }
520
611
  case "DatePicker": {
521
612
  const name = asString(props["name"]);
522
613
  const label = asString(props["label"]);
614
+ const mode = props["inputType"];
615
+ if (mode === "time") {
616
+ const id = reservedSafeId(name || "datepicker", "DatePicker", context);
617
+ const rawValue = asString(props["value"]);
618
+ const parts = splitTemporalMinutes(rawValue);
619
+ if (
620
+ classifyTemporal(rawValue).kind === "time" &&
621
+ parts?.droppedPrecision
622
+ ) {
623
+ warn(
624
+ context,
625
+ "dropped",
626
+ "DatePicker",
627
+ "Nonzero seconds were dropped from the time value.",
628
+ );
629
+ }
630
+ const value = parts?.time;
631
+ const rawMin = asString(props["min"]);
632
+ const minParts = splitTemporalMinutes(rawMin);
633
+ let min = minParts?.time;
634
+ if (
635
+ classifyTemporal(rawMin).kind === "time" &&
636
+ minParts?.droppedPrecision
637
+ ) {
638
+ const minuteOfDay =
639
+ Number(minParts.time.slice(0, 2)) * 60 +
640
+ Number(minParts.time.slice(3, 5)) +
641
+ 1;
642
+ if (minuteOfDay === 24 * 60) {
643
+ min = undefined;
644
+ warn(
645
+ context,
646
+ "dropped",
647
+ "DatePicker",
648
+ "The time minimum rounds past 23:59 and was dropped.",
649
+ );
650
+ } else {
651
+ min = `${String(Math.floor(minuteOfDay / 60)).padStart(2, "0")}:${String(minuteOfDay % 60).padStart(2, "0")}`;
652
+ }
653
+ }
654
+ const rawMax = asString(props["max"]);
655
+ const maxParts = splitTemporalMinutes(rawMax);
656
+ const max = maxParts?.time;
657
+ if (
658
+ classifyTemporal(rawMax).kind === "time" &&
659
+ maxParts?.droppedPrecision
660
+ ) {
661
+ warn(
662
+ context,
663
+ "dropped",
664
+ "DatePicker",
665
+ rawMax.slice(6, 8) === "00"
666
+ ? "Nonzero fractional seconds were dropped from the time maximum."
667
+ : /\.\d*[1-9]/.test(rawMax)
668
+ ? "Nonzero seconds and fractional seconds were dropped from the time maximum."
669
+ : "Nonzero seconds were dropped from the time maximum.",
670
+ );
671
+ }
672
+ const input: TeamsCardElement = {
673
+ type: "Input.Time",
674
+ id,
675
+ ...(label ? { label } : {}),
676
+ ...(classifyTemporal(rawValue).kind === "time" && value !== undefined
677
+ ? { value }
678
+ : {}),
679
+ ...(classifyTemporal(rawMin).kind === "time" && min !== undefined
680
+ ? { min }
681
+ : {}),
682
+ ...(classifyTemporal(rawMax).kind === "time" && max !== undefined
683
+ ? { max }
684
+ : {}),
685
+ };
686
+ return withCompanionSubmit(element, input, context);
687
+ }
688
+ if (mode === "datetime") {
689
+ const rawValue = asString(props["value"]);
690
+ const temporal = classifyTemporal(rawValue);
691
+ const fieldId = reservedSafeId(
692
+ name || "datepicker",
693
+ "DatePicker",
694
+ context,
695
+ );
696
+ const dateId = encodeTemporalInputId({ fieldId, role: "date" });
697
+ const timeId = encodeTemporalInputId({
698
+ fieldId,
699
+ role: "time",
700
+ ...(temporal.kind === "instant" ? { previousValue: rawValue } : {}),
701
+ });
702
+ const parts = splitTemporalMinutes(rawValue);
703
+ if (temporal.kind === "floating" && parts?.droppedPrecision) {
704
+ warn(
705
+ context,
706
+ "dropped",
707
+ "DatePicker",
708
+ "Nonzero seconds were dropped from the datetime value.",
709
+ );
710
+ }
711
+ const offsetLabel = temporalOffsetLabel(rawValue);
712
+ const boundDate = (raw: unknown) => {
713
+ const value = asString(raw);
714
+ return classifyTemporal(value).kind === "date"
715
+ ? value
716
+ : splitTemporalMinutes(
717
+ value,
718
+ temporal.kind === "instant" ? temporal.offset : undefined,
719
+ )?.date;
720
+ };
721
+ const min = boundDate(props["min"]);
722
+ const max = boundDate(props["max"]);
723
+ const dateInput: TeamsCardElement = {
724
+ type: "Input.Date",
725
+ id: dateId,
726
+ ...(label ? { label } : {}),
727
+ ...(parts?.date !== undefined ? { value: parts.date } : {}),
728
+ ...(min !== undefined ? { min } : {}),
729
+ ...(max !== undefined ? { max } : {}),
730
+ };
731
+ const timeInput: TeamsCardElement = {
732
+ type: "Input.Time",
733
+ id: timeId,
734
+ ...(label || offsetLabel
735
+ ? {
736
+ label: `${label ? `${label} time` : "Time"}${offsetLabel ? ` (${offsetLabel})` : ""}`,
737
+ }
738
+ : {}),
739
+ ...(parts?.time !== undefined ? { value: parts.time } : {}),
740
+ };
741
+ if (element.action === undefined) return [dateInput, timeInput];
742
+ const [input, submit] = withCompanionSubmit(
743
+ element,
744
+ timeInput,
745
+ context,
746
+ );
747
+ return [dateInput, input!, submit!];
748
+ }
749
+ if (mode !== undefined && mode !== "date") {
750
+ warn(
751
+ context,
752
+ "dropped",
753
+ "DatePicker",
754
+ "Unsupported inputType was dropped; the picker was rendered as a date input.",
755
+ );
756
+ }
523
757
  const rawValue = props["value"];
524
758
  const value =
525
759
  typeof rawValue === "string" && DATE_PATTERN.test(rawValue)
@@ -785,16 +1019,20 @@ export function convertRootToCard(
785
1019
  * Converts a generative-ui tree into a Microsoft Teams Adaptive Card and
786
1020
  * non-fatal conversion warnings. Sizes, weights, and colors map to Adaptive
787
1021
  * Card's semantic enums rather than raw values. An Input/Select/RadioGroup/
788
- * Checkbox/DatePicker whose id would be the reserved {@link RESERVED_INPUT_ID}
789
- * is renamed with a warning (see `decodeSubmitData`). Never throws: an
790
- * unknown `$type` is skipped with a "dropped" warning, and a malformed input
791
- * resolves to an empty card plus a "dropped" warning instead of throwing.
1022
+ * CheckboxGroup/Checkbox/Slider/DatePicker whose id would be the reserved
1023
+ * {@link RESERVED_INPUT_ID} is renamed with a warning (see `decodeSubmitData`).
1024
+ * Never throws: an unknown `$type` is skipped with a "dropped" warning, and a
1025
+ * malformed input resolves to an empty card plus a "dropped" warning instead
1026
+ * of throwing.
792
1027
  */
793
1028
  export function toAdaptiveCard(
794
1029
  node: unknown,
795
1030
  _options?: ToAdaptiveCardOptions,
796
1031
  ): AdaptiveCardResult {
797
- const context: ConversionContext = { warnings: [], usedInputIds: new Set() };
1032
+ const context: ConversionContext = {
1033
+ warnings: [],
1034
+ usedInputIds: new Set(),
1035
+ };
798
1036
  try {
799
1037
  const bounded = boundSpec(node, (reason) =>
800
1038
  warn(context, "clamped", "Root", clampReasonDetail(reason)),
@@ -77,7 +77,10 @@ export function toTeamsAttachments(
77
77
  _options?: ToAdaptiveCardOptions,
78
78
  ): TeamsAttachmentsResult {
79
79
  const warnings: TeamsConversionWarning[] = [];
80
- const context: ConversionContext = { warnings, usedInputIds: new Set() };
80
+ const context: ConversionContext = {
81
+ warnings,
82
+ usedInputIds: new Set(),
83
+ };
81
84
  try {
82
85
  const bounded = boundSpec(node, (reason) =>
83
86
  warnings.push({
@@ -55,6 +55,11 @@ export interface TeamsSubmitData {
55
55
  };
56
56
  }
57
57
 
58
+ export type TeamsTemporalField = { readonly fieldId: string } & (
59
+ | { readonly role: "date" }
60
+ | { readonly role: "time"; readonly previousValue?: string }
61
+ );
62
+
58
63
  /**
59
64
  * A submit action. `data` carries the resume payload under an `aui` key so
60
65
  * {@link decodeSubmitData} can invert it out of the merged `activity.value`
@@ -109,11 +114,12 @@ export interface TeamsInputChoice {
109
114
  readonly value: string;
110
115
  }
111
116
 
112
- /** A dropdown (`compact`) or radio group (`expanded`) input. */
117
+ /** A dropdown (`compact`), radio group (`expanded`), or checkbox group (`expanded` with `isMultiSelect`) input. */
113
118
  export interface TeamsInputChoiceSet {
114
119
  readonly type: "Input.ChoiceSet";
115
120
  readonly id: string;
116
121
  readonly style: "compact" | "expanded";
122
+ readonly isMultiSelect?: true;
117
123
  readonly choices: readonly TeamsInputChoice[];
118
124
  readonly placeholder?: string;
119
125
  readonly label?: string;
@@ -138,8 +144,10 @@ export interface TeamsInputToggle {
138
144
  export interface TeamsInputText {
139
145
  readonly type: "Input.Text";
140
146
  readonly id: string;
147
+ readonly style?: "password";
141
148
  readonly label?: string;
142
149
  readonly placeholder?: string;
150
+ readonly value?: string;
143
151
  readonly isMultiline?: true;
144
152
  readonly separator?: true;
145
153
  readonly spacing?: "large";
@@ -157,6 +165,17 @@ export interface TeamsInputDate {
157
165
  readonly spacing?: "large";
158
166
  }
159
167
 
168
+ export interface TeamsInputTime {
169
+ readonly type: "Input.Time";
170
+ readonly id: string;
171
+ readonly label?: string;
172
+ readonly value?: string;
173
+ readonly min?: string;
174
+ readonly max?: string;
175
+ readonly separator?: true;
176
+ readonly spacing?: "large";
177
+ }
178
+
160
179
  /** One cell inside a {@link TeamsTableRow}. */
161
180
  export interface TeamsTableCell {
162
181
  readonly type: "TableCell";
@@ -196,6 +215,7 @@ export type TeamsCardElement =
196
215
  | TeamsInputToggle
197
216
  | TeamsInputText
198
217
  | TeamsInputDate
218
+ | TeamsInputTime
199
219
  | TeamsTable;
200
220
 
201
221
  /** A Microsoft Teams Adaptive Card, pinned to schema version 1.5 (the Teams desktop/web cap; mobile clients cap at 1.2). */
package/src/teams.ts CHANGED
@@ -21,6 +21,7 @@ export type {
21
21
  TeamsInputChoiceSet,
22
22
  TeamsInputDate,
23
23
  TeamsInputText,
24
+ TeamsInputTime,
24
25
  TeamsInputToggle,
25
26
  TeamsSubmitAction,
26
27
  TeamsSubmitData,