@agent-native/toolkit 0.9.1 → 0.10.1

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 (194) hide show
  1. package/agent-native.eject.json +71 -0
  2. package/dist/chat-history/ChatHistoryRail.d.ts +15 -1
  3. package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
  4. package/dist/chat-history/ChatHistoryRail.js +26 -13
  5. package/dist/chat-history/ChatHistoryRail.js.map +1 -1
  6. package/dist/chat-history/ChatHistoryRail.spec.js +77 -1
  7. package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -1
  8. package/dist/chat-history/index.d.ts +2 -1
  9. package/dist/chat-history/index.d.ts.map +1 -1
  10. package/dist/chat-history/index.js +2 -1
  11. package/dist/chat-history/index.js.map +1 -1
  12. package/dist/chat-history/useChatHistoryRailController.d.ts +26 -0
  13. package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -0
  14. package/dist/chat-history/useChatHistoryRailController.js +36 -0
  15. package/dist/chat-history/useChatHistoryRailController.js.map +1 -0
  16. package/dist/chat-history/useChatHistoryRailController.spec.d.ts +2 -0
  17. package/dist/chat-history/useChatHistoryRailController.spec.d.ts.map +1 -0
  18. package/dist/chat-history/useChatHistoryRailController.spec.js +101 -0
  19. package/dist/chat-history/useChatHistoryRailController.spec.js.map +1 -0
  20. package/dist/chat-history.css +2 -0
  21. package/dist/composer/ComposerPlusMenu.js +1 -1
  22. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  23. package/dist/composer/PromptComposer.d.ts +1 -0
  24. package/dist/composer/PromptComposer.d.ts.map +1 -1
  25. package/dist/composer/PromptComposer.js +26 -5
  26. package/dist/composer/PromptComposer.js.map +1 -1
  27. package/dist/composer/PromptComposer.spec.js +8 -1
  28. package/dist/composer/PromptComposer.spec.js.map +1 -1
  29. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
  30. package/dist/composer/RealtimeVoiceMode.js +56 -11
  31. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  32. package/dist/composer/RealtimeVoiceMode.spec.js +59 -7
  33. package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
  34. package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
  35. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  36. package/dist/composer/useRealtimeVoiceMode.js +110 -6
  37. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  38. package/dist/composer/useRealtimeVoiceMode.spec.js +74 -2
  39. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
  40. package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
  41. package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
  42. package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
  43. package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
  44. package/dist/conformance/conformance.spec.d.ts +2 -0
  45. package/dist/conformance/conformance.spec.d.ts.map +1 -0
  46. package/dist/conformance/conformance.spec.js +87 -0
  47. package/dist/conformance/conformance.spec.js.map +1 -0
  48. package/dist/conformance/index.d.ts +4 -0
  49. package/dist/conformance/index.d.ts.map +1 -0
  50. package/dist/conformance/index.js +4 -0
  51. package/dist/conformance/index.js.map +1 -0
  52. package/dist/conformance/runner.d.ts +12 -0
  53. package/dist/conformance/runner.d.ts.map +1 -0
  54. package/dist/conformance/runner.js +515 -0
  55. package/dist/conformance/runner.js.map +1 -0
  56. package/dist/conformance/types.d.ts +26 -0
  57. package/dist/conformance/types.d.ts.map +1 -0
  58. package/dist/conformance/types.js +13 -0
  59. package/dist/conformance/types.js.map +1 -0
  60. package/dist/conformance/version.d.ts +6 -0
  61. package/dist/conformance/version.d.ts.map +1 -0
  62. package/dist/conformance/version.js +14 -0
  63. package/dist/conformance/version.js.map +1 -0
  64. package/dist/design-system/components.d.ts +64 -0
  65. package/dist/design-system/components.d.ts.map +1 -0
  66. package/dist/design-system/components.js +50 -0
  67. package/dist/design-system/components.js.map +1 -0
  68. package/dist/design-system/context.d.ts +11 -0
  69. package/dist/design-system/context.d.ts.map +1 -0
  70. package/dist/design-system/context.js +17 -0
  71. package/dist/design-system/context.js.map +1 -0
  72. package/dist/design-system/default-adapter.d.ts +3 -0
  73. package/dist/design-system/default-adapter.d.ts.map +1 -0
  74. package/dist/design-system/default-adapter.js +166 -0
  75. package/dist/design-system/default-adapter.js.map +1 -0
  76. package/dist/design-system/default-adapter.spec.d.ts +2 -0
  77. package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
  78. package/dist/design-system/default-adapter.spec.js +96 -0
  79. package/dist/design-system/default-adapter.spec.js.map +1 -0
  80. package/dist/design-system/definition.d.ts +9 -0
  81. package/dist/design-system/definition.d.ts.map +1 -0
  82. package/dist/design-system/definition.js +4 -0
  83. package/dist/design-system/definition.js.map +1 -0
  84. package/dist/design-system/design-system.spec.d.ts +2 -0
  85. package/dist/design-system/design-system.spec.d.ts.map +1 -0
  86. package/dist/design-system/design-system.spec.js +121 -0
  87. package/dist/design-system/design-system.spec.js.map +1 -0
  88. package/dist/design-system/error-boundary.d.ts +18 -0
  89. package/dist/design-system/error-boundary.d.ts.map +1 -0
  90. package/dist/design-system/error-boundary.js +16 -0
  91. package/dist/design-system/error-boundary.js.map +1 -0
  92. package/dist/design-system/index.d.ts +8 -0
  93. package/dist/design-system/index.d.ts.map +1 -0
  94. package/dist/design-system/index.js +8 -0
  95. package/dist/design-system/index.js.map +1 -0
  96. package/dist/design-system/theme.d.ts +30 -0
  97. package/dist/design-system/theme.d.ts.map +1 -0
  98. package/dist/design-system/theme.js +95 -0
  99. package/dist/design-system/theme.js.map +1 -0
  100. package/dist/design-system/theme.spec.d.ts +2 -0
  101. package/dist/design-system/theme.spec.d.ts.map +1 -0
  102. package/dist/design-system/theme.spec.js +43 -0
  103. package/dist/design-system/theme.spec.js.map +1 -0
  104. package/dist/design-system/types.d.ts +288 -0
  105. package/dist/design-system/types.d.ts.map +1 -0
  106. package/dist/design-system/types.js +2 -0
  107. package/dist/design-system/types.js.map +1 -0
  108. package/dist/editor/SharedRichEditor.d.ts.map +1 -1
  109. package/dist/editor/SharedRichEditor.js +3 -2
  110. package/dist/editor/SharedRichEditor.js.map +1 -1
  111. package/dist/editor/SlashCommandMenu.d.ts +8 -0
  112. package/dist/editor/SlashCommandMenu.d.ts.map +1 -1
  113. package/dist/editor/SlashCommandMenu.js +6 -0
  114. package/dist/editor/SlashCommandMenu.js.map +1 -1
  115. package/dist/editor/SlashCommandMenu.spec.d.ts +2 -0
  116. package/dist/editor/SlashCommandMenu.spec.d.ts.map +1 -0
  117. package/dist/editor/SlashCommandMenu.spec.js +20 -0
  118. package/dist/editor/SlashCommandMenu.spec.js.map +1 -0
  119. package/dist/editor/index.d.ts +1 -1
  120. package/dist/editor/index.d.ts.map +1 -1
  121. package/dist/editor/index.js +1 -1
  122. package/dist/editor/index.js.map +1 -1
  123. package/dist/provider.d.ts +4 -3
  124. package/dist/provider.d.ts.map +1 -1
  125. package/dist/provider.js +46 -4
  126. package/dist/provider.js.map +1 -1
  127. package/dist/styles.css +52 -23
  128. package/dist/ui/button.d.ts +5 -0
  129. package/dist/ui/button.d.ts.map +1 -1
  130. package/dist/ui/button.js +25 -4
  131. package/dist/ui/button.js.map +1 -1
  132. package/dist/ui/dialog.d.ts +4 -0
  133. package/dist/ui/dialog.d.ts.map +1 -1
  134. package/dist/ui/dialog.js +2 -2
  135. package/dist/ui/dialog.js.map +1 -1
  136. package/dist/ui/popover.d.ts +1 -0
  137. package/dist/ui/popover.d.ts.map +1 -1
  138. package/dist/ui/popover.js +2 -2
  139. package/dist/ui/popover.js.map +1 -1
  140. package/dist/ui/select.d.ts +3 -1
  141. package/dist/ui/select.d.ts.map +1 -1
  142. package/dist/ui/select.js +1 -1
  143. package/dist/ui/select.js.map +1 -1
  144. package/dist/ui/sidebar.d.ts +6 -2
  145. package/dist/ui/sidebar.d.ts.map +1 -1
  146. package/dist/ui/sidebar.js +12 -15
  147. package/dist/ui/sidebar.js.map +1 -1
  148. package/dist/ui/tooltip.d.ts +3 -1
  149. package/dist/ui/tooltip.d.ts.map +1 -1
  150. package/dist/ui/tooltip.js +2 -2
  151. package/dist/ui/tooltip.js.map +1 -1
  152. package/package.json +31 -1
  153. package/src/chat-history/ChatHistoryRail.spec.tsx +174 -0
  154. package/src/chat-history/ChatHistoryRail.tsx +100 -39
  155. package/src/chat-history/index.ts +9 -0
  156. package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
  157. package/src/chat-history/useChatHistoryRailController.ts +76 -0
  158. package/src/chat-history.css +2 -0
  159. package/src/composer/ComposerPlusMenu.tsx +11 -8
  160. package/src/composer/PromptComposer.spec.ts +12 -1
  161. package/src/composer/PromptComposer.tsx +32 -7
  162. package/src/composer/RealtimeVoiceMode.spec.tsx +117 -9
  163. package/src/composer/RealtimeVoiceMode.tsx +119 -31
  164. package/src/composer/useRealtimeVoiceMode.spec.ts +101 -1
  165. package/src/composer/useRealtimeVoiceMode.tsx +124 -9
  166. package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
  167. package/src/conformance/conformance.spec.tsx +143 -0
  168. package/src/conformance/index.ts +3 -0
  169. package/src/conformance/runner.tsx +862 -0
  170. package/src/conformance/types.ts +45 -0
  171. package/src/conformance/version.ts +22 -0
  172. package/src/design-system/components.tsx +146 -0
  173. package/src/design-system/context.tsx +31 -0
  174. package/src/design-system/default-adapter.spec.tsx +143 -0
  175. package/src/design-system/default-adapter.tsx +915 -0
  176. package/src/design-system/definition.ts +14 -0
  177. package/src/design-system/design-system.spec.tsx +183 -0
  178. package/src/design-system/error-boundary.tsx +37 -0
  179. package/src/design-system/index.ts +7 -0
  180. package/src/design-system/theme.spec.ts +61 -0
  181. package/src/design-system/theme.ts +145 -0
  182. package/src/design-system/types.ts +363 -0
  183. package/src/editor/SharedRichEditor.tsx +13 -2
  184. package/src/editor/SlashCommandMenu.spec.ts +33 -0
  185. package/src/editor/SlashCommandMenu.tsx +20 -0
  186. package/src/editor/index.ts +2 -0
  187. package/src/provider.tsx +98 -5
  188. package/src/styles.css +52 -23
  189. package/src/ui/button.tsx +95 -6
  190. package/src/ui/dialog.tsx +48 -23
  191. package/src/ui/popover.tsx +16 -2
  192. package/src/ui/select.tsx +7 -3
  193. package/src/ui/sidebar.tsx +47 -48
  194. package/src/ui/tooltip.tsx +7 -3
@@ -484,6 +484,75 @@ export function createRealtimeVoiceGreetingStarter(
484
484
  };
485
485
  }
486
486
 
487
+ export interface RealtimeVoiceResponseCoordinator {
488
+ request: (event?: Record<string, unknown>) => void;
489
+ handleEvent: (event: RealtimeServerEvent) => void;
490
+ handleError: (message?: string) => boolean;
491
+ reset: () => void;
492
+ }
493
+
494
+ export function createRealtimeVoiceResponseCoordinator(
495
+ send: (event: Record<string, unknown>) => void,
496
+ ): RealtimeVoiceResponseCoordinator {
497
+ let active = false;
498
+ let requestInFlight = false;
499
+ let pending: Record<string, unknown> | undefined;
500
+
501
+ const flush = () => {
502
+ if (active || requestInFlight || !pending) return;
503
+ const event = pending;
504
+ pending = undefined;
505
+ requestInFlight = true;
506
+ send(event);
507
+ };
508
+
509
+ return {
510
+ request(event = { type: "response.create" }) {
511
+ pending = event;
512
+ flush();
513
+ },
514
+ handleEvent(event) {
515
+ if (event.type === "response.created") {
516
+ active = true;
517
+ requestInFlight = false;
518
+ return;
519
+ }
520
+ if (
521
+ event.type === "response.done" ||
522
+ event.type === "response.cancelled" ||
523
+ event.type === "response.failed"
524
+ ) {
525
+ active = false;
526
+ requestInFlight = false;
527
+ const response = event.response;
528
+ const status =
529
+ response && typeof response === "object"
530
+ ? (response as { status?: unknown }).status
531
+ : undefined;
532
+ if (event.type === "response.failed" || status === "failed") {
533
+ pending = undefined;
534
+ return;
535
+ }
536
+ flush();
537
+ }
538
+ },
539
+ handleError(message) {
540
+ if (!message || !/active response.*in progress/i.test(message)) {
541
+ return false;
542
+ }
543
+ pending ??= { type: "response.create" };
544
+ requestInFlight = false;
545
+ active = true;
546
+ return true;
547
+ },
548
+ reset() {
549
+ active = false;
550
+ requestInFlight = false;
551
+ pending = undefined;
552
+ },
553
+ };
554
+ }
555
+
487
556
  /**
488
557
  * Voice mode owns the chat only temporarily. Restore the captured transcript
489
558
  * when it is still the user's active thread (or the chat has no active thread)
@@ -949,9 +1018,28 @@ export function extractRealtimeVoiceFunctionCalls(
949
1018
  },
950
1019
  ];
951
1020
  }
1021
+ if (event.type === "response.output_item.done") {
1022
+ const item = event.item;
1023
+ if (!item || typeof item !== "object") return [];
1024
+ const record = item as Record<string, unknown>;
1025
+ if (record.type !== "function_call") return [];
1026
+ const name = typeof record.name === "string" ? record.name : "";
1027
+ const callId = typeof record.call_id === "string" ? record.call_id : "";
1028
+ if (!name || !callId) return [];
1029
+ return [
1030
+ {
1031
+ name,
1032
+ callId,
1033
+ argumentsText:
1034
+ typeof record.arguments === "string" ? record.arguments : "{}",
1035
+ },
1036
+ ];
1037
+ }
952
1038
  if (event.type !== "response.done") return [];
953
1039
  const response = event.response;
954
1040
  if (!response || typeof response !== "object") return [];
1041
+ const status = (response as { status?: unknown }).status;
1042
+ if (status !== undefined && status !== "completed") return [];
955
1043
  const output = (response as { output?: unknown }).output;
956
1044
  if (!Array.isArray(output)) return [];
957
1045
  return output.flatMap((item) => {
@@ -1141,13 +1229,24 @@ function useRealtimeVoiceModeController(
1141
1229
  const preferencesHydratedRef = useRef(false);
1142
1230
  const preferencesEditedRef = useRef(false);
1143
1231
  const preferenceWriteChainRef = useRef<Promise<void>>(Promise.resolve());
1144
- const toolManifestCoordinator = useMemo(
1232
+ const responseCoordinator = useMemo(
1145
1233
  () =>
1146
- createRealtimeVoiceToolManifestCoordinator((event) => {
1234
+ createRealtimeVoiceResponseCoordinator((event) => {
1147
1235
  sendDataChannelEvent(channelRef.current, event);
1148
1236
  }),
1149
1237
  [],
1150
1238
  );
1239
+ const toolManifestCoordinator = useMemo(
1240
+ () =>
1241
+ createRealtimeVoiceToolManifestCoordinator((event) => {
1242
+ if (event.type === "response.create") {
1243
+ responseCoordinator.request(event);
1244
+ } else {
1245
+ sendDataChannelEvent(channelRef.current, event);
1246
+ }
1247
+ }),
1248
+ [responseCoordinator],
1249
+ );
1151
1250
  const switchMicrophoneRef = useRef<
1152
1251
  (deviceId: string, force?: boolean) => Promise<void>
1153
1252
  >(async () => undefined);
@@ -1461,8 +1560,9 @@ function useRealtimeVoiceModeController(
1461
1560
  if (audioContext) void audioContext.close().catch(() => undefined);
1462
1561
  audioLevels.reset();
1463
1562
  handledCallsRef.current.clear();
1563
+ responseCoordinator.reset();
1464
1564
  toolManifestCoordinator.reset();
1465
- }, [audioLevels, toolManifestCoordinator]);
1565
+ }, [audioLevels, responseCoordinator, toolManifestCoordinator]);
1466
1566
 
1467
1567
  const fail = useCallback(
1468
1568
  (message: string, options?: { openKeySettings?: boolean }) => {
@@ -1537,13 +1637,14 @@ function useRealtimeVoiceModeController(
1537
1637
  const greetingStarter = useMemo(
1538
1638
  () =>
1539
1639
  createRealtimeVoiceGreetingStarter((event) => {
1540
- sendDataChannelEvent(channelRef.current, event);
1640
+ responseCoordinator.request(event);
1541
1641
  }),
1542
- [],
1642
+ [responseCoordinator],
1543
1643
  );
1544
1644
 
1545
1645
  const handleServerEvent = useCallback(
1546
1646
  (event: RealtimeServerEvent) => {
1647
+ responseCoordinator.handleEvent(event);
1547
1648
  transcriptSequencer.handle(event);
1548
1649
  const sessionTools = extractRealtimeVoiceSessionTools(event);
1549
1650
  if (sessionTools) toolManifestCoordinator.setSessionTools(sessionTools);
@@ -1562,6 +1663,7 @@ function useRealtimeVoiceModeController(
1562
1663
  transition("listening");
1563
1664
  } else if (event.type === "input_audio_buffer.speech_stopped") {
1564
1665
  transition("working");
1666
+ responseCoordinator.request();
1565
1667
  } else if (event.type === "response.created") {
1566
1668
  // From this point onward the response is committed to audio output, so
1567
1669
  // changing voices risks violating Realtime's per-session voice lock.
@@ -1593,12 +1695,20 @@ function useRealtimeVoiceModeController(
1593
1695
  ? (response as { status?: unknown }).status
1594
1696
  : undefined;
1595
1697
  if (status === "failed") {
1596
- fail(
1597
- copy?.errors.responseFailed ??
1598
- "OpenAI could not complete the voice response.",
1599
- );
1698
+ transportGenerationRef.current += 1;
1699
+ handledCallsRef.current.clear();
1700
+ responseCoordinator.reset();
1701
+ toolManifestCoordinator.reset();
1702
+ transition("listening");
1600
1703
  return;
1601
1704
  }
1705
+ } else if (event.type === "response.failed") {
1706
+ transportGenerationRef.current += 1;
1707
+ handledCallsRef.current.clear();
1708
+ responseCoordinator.reset();
1709
+ toolManifestCoordinator.reset();
1710
+ transition("listening");
1711
+ return;
1602
1712
  } else if (event.type === "error") {
1603
1713
  const detail = event.error;
1604
1714
  const message =
@@ -1619,6 +1729,10 @@ function useRealtimeVoiceModeController(
1619
1729
  ) {
1620
1730
  return;
1621
1731
  }
1732
+ if (responseCoordinator.handleError(message)) {
1733
+ transition("working");
1734
+ return;
1735
+ }
1622
1736
  fail(
1623
1737
  message ||
1624
1738
  copy?.errors.sessionFailed ||
@@ -1641,6 +1755,7 @@ function useRealtimeVoiceModeController(
1641
1755
  syncAppState,
1642
1756
  transcriptSequencer,
1643
1757
  transition,
1758
+ responseCoordinator,
1644
1759
  toolManifestCoordinator,
1645
1760
  updateLivePreferences,
1646
1761
  ],
@@ -0,0 +1,401 @@
1
+ import {
2
+ cloneElement,
3
+ createElement,
4
+ useEffect,
5
+ type CSSProperties,
6
+ type ElementType,
7
+ type KeyboardEvent,
8
+ type ReactElement,
9
+ } from "react";
10
+ import { createPortal } from "react-dom";
11
+
12
+ import type { DesignSystemComponents } from "../../design-system/types.js";
13
+
14
+ const css = {
15
+ control: {
16
+ border: "1px solid rgb(80 80 90)",
17
+ borderRadius: 6,
18
+ background: "rgb(250 250 252)",
19
+ color: "rgb(20 20 24)",
20
+ padding: "6px 10px",
21
+ font: "inherit",
22
+ } satisfies CSSProperties,
23
+ overlay: {
24
+ position: "fixed",
25
+ inset: "20% auto auto 20%",
26
+ zIndex: 120,
27
+ border: "1px solid rgb(80 80 90)",
28
+ borderRadius: 8,
29
+ background: "white",
30
+ color: "black",
31
+ padding: 12,
32
+ } satisfies CSSProperties,
33
+ stack: {
34
+ display: "grid",
35
+ gap: 6,
36
+ } satisfies CSSProperties,
37
+ };
38
+
39
+ function portal(content: ReactElement, container?: Element | null) {
40
+ return container
41
+ ? createPortal(content, container)
42
+ : createPortal(content, document.body);
43
+ }
44
+
45
+ export const cssInJsFixtureAdapter: DesignSystemComponents = {
46
+ ActionButton: ({
47
+ children,
48
+ leadingIcon,
49
+ trailingIcon,
50
+ pending,
51
+ disabled,
52
+ onPress,
53
+ elementRef,
54
+ style,
55
+ ...props
56
+ }) => (
57
+ <button
58
+ {...props}
59
+ ref={elementRef}
60
+ disabled={disabled || pending}
61
+ onClick={(event) => onPress?.(event)}
62
+ style={{ ...css.control, ...style }}
63
+ >
64
+ {pending ? "Waiting" : leadingIcon}
65
+ {children}
66
+ {trailingIcon}
67
+ </button>
68
+ ),
69
+ IconButton: ({
70
+ label,
71
+ icon,
72
+ pending,
73
+ disabled,
74
+ onPress,
75
+ elementRef,
76
+ style,
77
+ ...props
78
+ }) => (
79
+ <button
80
+ {...props}
81
+ ref={elementRef}
82
+ aria-label={label}
83
+ disabled={disabled || pending}
84
+ onClick={(event) => onPress?.(event)}
85
+ style={{ ...css.control, ...style }}
86
+ >
87
+ {pending ? "Waiting" : icon}
88
+ </button>
89
+ ),
90
+ TextField: ({
91
+ value,
92
+ onChange,
93
+ label,
94
+ description,
95
+ errorMessage,
96
+ inputRef,
97
+ leadingContent,
98
+ trailingContent,
99
+ style,
100
+ onKeyDown,
101
+ ...props
102
+ }) => (
103
+ <label style={{ ...css.stack, ...style }}>
104
+ {label}
105
+ <span>
106
+ {leadingContent}
107
+ <input
108
+ {...props}
109
+ ref={inputRef}
110
+ value={value}
111
+ onChange={(event) => onChange(event.currentTarget.value)}
112
+ onKeyDown={onKeyDown}
113
+ style={css.control}
114
+ />
115
+ {trailingContent}
116
+ </span>
117
+ {errorMessage ?? description}
118
+ </label>
119
+ ),
120
+ TextArea: ({
121
+ value,
122
+ onChange,
123
+ label,
124
+ description,
125
+ errorMessage,
126
+ textAreaRef,
127
+ style,
128
+ onKeyDown,
129
+ ...props
130
+ }) => (
131
+ <label style={{ ...css.stack, ...style }}>
132
+ {label}
133
+ <textarea
134
+ {...props}
135
+ ref={textAreaRef}
136
+ value={value}
137
+ onChange={(event) => onChange(event.currentTarget.value)}
138
+ onKeyDown={onKeyDown}
139
+ style={css.control}
140
+ />
141
+ {errorMessage ?? description}
142
+ </label>
143
+ ),
144
+ Spinner: ({ label, size: _size, ...props }) => (
145
+ <span {...props} role={label ? "status" : undefined} aria-label={label}>
146
+ Loading
147
+ </span>
148
+ ),
149
+ Skeleton: ({ width, height, shape: _shape, style, ...props }) => (
150
+ <span
151
+ {...props}
152
+ aria-hidden="true"
153
+ style={{
154
+ display: "block",
155
+ width,
156
+ height,
157
+ background: "silver",
158
+ ...style,
159
+ }}
160
+ />
161
+ ),
162
+ Status: ({ children, icon, tone: _tone, size: _size, ...props }) => (
163
+ <span {...props}>
164
+ {icon}
165
+ {children}
166
+ </span>
167
+ ),
168
+ Surface: ({
169
+ children,
170
+ as = "div",
171
+ interactive,
172
+ onPress,
173
+ elevation: _elevation,
174
+ padding: _padding,
175
+ style,
176
+ ...props
177
+ }) => {
178
+ const Element = as as ElementType;
179
+ return (
180
+ <Element
181
+ {...props}
182
+ role={interactive ? "button" : undefined}
183
+ tabIndex={interactive ? 0 : undefined}
184
+ onClick={onPress}
185
+ onKeyDown={(event: KeyboardEvent<HTMLElement>) => {
186
+ if (interactive && (event.key === "Enter" || event.key === " "))
187
+ onPress?.(event);
188
+ }}
189
+ style={{ border: "1px solid gray", padding: 8, ...style }}
190
+ >
191
+ {children}
192
+ </Element>
193
+ );
194
+ },
195
+ Avatar: ({
196
+ name,
197
+ src,
198
+ fallback,
199
+ imageRef,
200
+ size: _size,
201
+ status: _status,
202
+ ...props
203
+ }) => (
204
+ <span {...props}>
205
+ {src ? (
206
+ <img ref={imageRef} src={src} alt={name} />
207
+ ) : (
208
+ (fallback ?? name.slice(0, 2))
209
+ )}
210
+ </span>
211
+ ),
212
+ Tooltip: ({
213
+ trigger,
214
+ content,
215
+ open,
216
+ defaultOpen,
217
+ disabled,
218
+ portalContainer,
219
+ }) => (
220
+ <>
221
+ {trigger}
222
+ {!disabled && (open ?? defaultOpen)
223
+ ? portal(
224
+ <div role="tooltip" style={css.overlay}>
225
+ {content}
226
+ </div>,
227
+ portalContainer,
228
+ )
229
+ : null}
230
+ </>
231
+ ),
232
+ Menu: ({
233
+ trigger,
234
+ items = [],
235
+ sections = [],
236
+ open,
237
+ defaultOpen,
238
+ onAction,
239
+ portalContainer,
240
+ }) => (
241
+ <>
242
+ {trigger}
243
+ {(open ?? defaultOpen)
244
+ ? portal(
245
+ <div role="menu" style={css.overlay}>
246
+ {[...items, ...sections.flatMap((section) => section.items)].map(
247
+ (item) => (
248
+ <button
249
+ key={item.id}
250
+ role="menuitem"
251
+ disabled={item.disabled}
252
+ onClick={() => onAction(item.id)}
253
+ >
254
+ {item.label}
255
+ </button>
256
+ ),
257
+ )}
258
+ </div>,
259
+ portalContainer,
260
+ )
261
+ : null}
262
+ </>
263
+ ),
264
+ Popover: ({ trigger, children, open, defaultOpen, portalContainer }) => (
265
+ <>
266
+ {trigger}
267
+ {(open ?? defaultOpen)
268
+ ? portal(
269
+ <div role="dialog" style={css.overlay}>
270
+ {children}
271
+ </div>,
272
+ portalContainer,
273
+ )
274
+ : null}
275
+ </>
276
+ ),
277
+ Dialog: ({
278
+ open,
279
+ title,
280
+ description,
281
+ children,
282
+ footer,
283
+ trigger,
284
+ portalContainer,
285
+ initialFocusRef,
286
+ restoreFocusRef,
287
+ }) => {
288
+ useEffect(() => {
289
+ if (open) initialFocusRef?.current?.focus();
290
+ return () => restoreFocusRef?.current?.focus();
291
+ }, [initialFocusRef, open, restoreFocusRef]);
292
+ return (
293
+ <>
294
+ {trigger}
295
+ {open
296
+ ? portal(
297
+ <div role="dialog" aria-modal="true" style={css.overlay}>
298
+ <h2>{title}</h2>
299
+ {description ? <p>{description}</p> : null}
300
+ {children}
301
+ {footer}
302
+ </div>,
303
+ portalContainer,
304
+ )
305
+ : null}
306
+ </>
307
+ );
308
+ },
309
+ Picker: ({
310
+ options,
311
+ value,
312
+ onChange,
313
+ label,
314
+ open,
315
+ portalContainer,
316
+ ...props
317
+ }) => (
318
+ <label>
319
+ {label}
320
+ <button
321
+ {...props}
322
+ role="combobox"
323
+ aria-expanded={open}
324
+ style={css.control}
325
+ >
326
+ {options.find((option) => option.value === value)?.label ?? "Choose"}
327
+ </button>
328
+ {open
329
+ ? portal(
330
+ <div role="listbox" style={css.overlay}>
331
+ {options.map((option) => (
332
+ <button
333
+ key={option.value}
334
+ role="option"
335
+ aria-selected={option.value === value}
336
+ disabled={option.disabled}
337
+ onClick={() => onChange(option.value)}
338
+ >
339
+ {option.label}
340
+ </button>
341
+ ))}
342
+ </div>,
343
+ portalContainer,
344
+ )
345
+ : null}
346
+ </label>
347
+ ),
348
+ Checkbox: ({ checked, onChange, label, inputRef, ...props }) => (
349
+ <button
350
+ {...props}
351
+ ref={inputRef}
352
+ role="checkbox"
353
+ aria-checked={checked}
354
+ onClick={() => onChange(!checked)}
355
+ style={css.control}
356
+ >
357
+ {label}
358
+ </button>
359
+ ),
360
+ Switch: ({ checked, onChange, label, inputRef, ...props }) => (
361
+ <button
362
+ {...props}
363
+ ref={inputRef}
364
+ role="switch"
365
+ aria-checked={checked}
366
+ onClick={() => onChange(!checked)}
367
+ style={css.control}
368
+ >
369
+ {label}
370
+ </button>
371
+ ),
372
+ Tabs: ({ items, value, onChange, orientation }) => (
373
+ <div>
374
+ <div role="tablist" aria-orientation={orientation}>
375
+ {items.map((item) => (
376
+ <button
377
+ key={item.value}
378
+ role="tab"
379
+ aria-selected={item.value === value}
380
+ disabled={item.disabled}
381
+ onClick={() => onChange(item.value)}
382
+ >
383
+ {item.icon}
384
+ {item.label}
385
+ </button>
386
+ ))}
387
+ </div>
388
+ {items.map((item) =>
389
+ item.value === value ? (
390
+ <div key={item.value} role="tabpanel">
391
+ {item.content}
392
+ </div>
393
+ ) : null,
394
+ )}
395
+ </div>
396
+ ),
397
+ };
398
+
399
+ export function CloneTrigger({ trigger }: { trigger: ReactElement }) {
400
+ return cloneElement(trigger);
401
+ }