@agent-native/toolkit 0.9.1 → 0.10.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.
Files changed (162) 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.map +1 -1
  24. package/dist/composer/PromptComposer.js +22 -4
  25. package/dist/composer/PromptComposer.js.map +1 -1
  26. package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
  27. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  28. package/dist/composer/useRealtimeVoiceMode.js +91 -19
  29. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  30. package/dist/composer/useRealtimeVoiceMode.spec.js +58 -9
  31. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
  32. package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
  33. package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
  34. package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
  35. package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
  36. package/dist/conformance/conformance.spec.d.ts +2 -0
  37. package/dist/conformance/conformance.spec.d.ts.map +1 -0
  38. package/dist/conformance/conformance.spec.js +58 -0
  39. package/dist/conformance/conformance.spec.js.map +1 -0
  40. package/dist/conformance/index.d.ts +4 -0
  41. package/dist/conformance/index.d.ts.map +1 -0
  42. package/dist/conformance/index.js +4 -0
  43. package/dist/conformance/index.js.map +1 -0
  44. package/dist/conformance/runner.d.ts +12 -0
  45. package/dist/conformance/runner.d.ts.map +1 -0
  46. package/dist/conformance/runner.js +441 -0
  47. package/dist/conformance/runner.js.map +1 -0
  48. package/dist/conformance/types.d.ts +26 -0
  49. package/dist/conformance/types.d.ts.map +1 -0
  50. package/dist/conformance/types.js +13 -0
  51. package/dist/conformance/types.js.map +1 -0
  52. package/dist/conformance/version.d.ts +6 -0
  53. package/dist/conformance/version.d.ts.map +1 -0
  54. package/dist/conformance/version.js +14 -0
  55. package/dist/conformance/version.js.map +1 -0
  56. package/dist/design-system/components.d.ts +64 -0
  57. package/dist/design-system/components.d.ts.map +1 -0
  58. package/dist/design-system/components.js +50 -0
  59. package/dist/design-system/components.js.map +1 -0
  60. package/dist/design-system/context.d.ts +11 -0
  61. package/dist/design-system/context.d.ts.map +1 -0
  62. package/dist/design-system/context.js +17 -0
  63. package/dist/design-system/context.js.map +1 -0
  64. package/dist/design-system/default-adapter.d.ts +3 -0
  65. package/dist/design-system/default-adapter.d.ts.map +1 -0
  66. package/dist/design-system/default-adapter.js +166 -0
  67. package/dist/design-system/default-adapter.js.map +1 -0
  68. package/dist/design-system/default-adapter.spec.d.ts +2 -0
  69. package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
  70. package/dist/design-system/default-adapter.spec.js +96 -0
  71. package/dist/design-system/default-adapter.spec.js.map +1 -0
  72. package/dist/design-system/definition.d.ts +9 -0
  73. package/dist/design-system/definition.d.ts.map +1 -0
  74. package/dist/design-system/definition.js +4 -0
  75. package/dist/design-system/definition.js.map +1 -0
  76. package/dist/design-system/design-system.spec.d.ts +2 -0
  77. package/dist/design-system/design-system.spec.d.ts.map +1 -0
  78. package/dist/design-system/design-system.spec.js +121 -0
  79. package/dist/design-system/design-system.spec.js.map +1 -0
  80. package/dist/design-system/error-boundary.d.ts +18 -0
  81. package/dist/design-system/error-boundary.d.ts.map +1 -0
  82. package/dist/design-system/error-boundary.js +16 -0
  83. package/dist/design-system/error-boundary.js.map +1 -0
  84. package/dist/design-system/index.d.ts +8 -0
  85. package/dist/design-system/index.d.ts.map +1 -0
  86. package/dist/design-system/index.js +8 -0
  87. package/dist/design-system/index.js.map +1 -0
  88. package/dist/design-system/theme.d.ts +30 -0
  89. package/dist/design-system/theme.d.ts.map +1 -0
  90. package/dist/design-system/theme.js +95 -0
  91. package/dist/design-system/theme.js.map +1 -0
  92. package/dist/design-system/theme.spec.d.ts +2 -0
  93. package/dist/design-system/theme.spec.d.ts.map +1 -0
  94. package/dist/design-system/theme.spec.js +43 -0
  95. package/dist/design-system/theme.spec.js.map +1 -0
  96. package/dist/design-system/types.d.ts +288 -0
  97. package/dist/design-system/types.d.ts.map +1 -0
  98. package/dist/design-system/types.js +2 -0
  99. package/dist/design-system/types.js.map +1 -0
  100. package/dist/provider.d.ts +4 -3
  101. package/dist/provider.d.ts.map +1 -1
  102. package/dist/provider.js +46 -4
  103. package/dist/provider.js.map +1 -1
  104. package/dist/ui/button.d.ts +5 -0
  105. package/dist/ui/button.d.ts.map +1 -1
  106. package/dist/ui/button.js +25 -4
  107. package/dist/ui/button.js.map +1 -1
  108. package/dist/ui/dialog.d.ts +4 -0
  109. package/dist/ui/dialog.d.ts.map +1 -1
  110. package/dist/ui/dialog.js +2 -2
  111. package/dist/ui/dialog.js.map +1 -1
  112. package/dist/ui/popover.d.ts +1 -0
  113. package/dist/ui/popover.d.ts.map +1 -1
  114. package/dist/ui/popover.js +2 -2
  115. package/dist/ui/popover.js.map +1 -1
  116. package/dist/ui/select.d.ts +3 -1
  117. package/dist/ui/select.d.ts.map +1 -1
  118. package/dist/ui/select.js +1 -1
  119. package/dist/ui/select.js.map +1 -1
  120. package/dist/ui/sidebar.d.ts +6 -2
  121. package/dist/ui/sidebar.d.ts.map +1 -1
  122. package/dist/ui/sidebar.js +12 -15
  123. package/dist/ui/sidebar.js.map +1 -1
  124. package/dist/ui/tooltip.d.ts +3 -1
  125. package/dist/ui/tooltip.d.ts.map +1 -1
  126. package/dist/ui/tooltip.js +2 -2
  127. package/dist/ui/tooltip.js.map +1 -1
  128. package/package.json +31 -1
  129. package/src/chat-history/ChatHistoryRail.spec.tsx +174 -0
  130. package/src/chat-history/ChatHistoryRail.tsx +100 -39
  131. package/src/chat-history/index.ts +9 -0
  132. package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
  133. package/src/chat-history/useChatHistoryRailController.ts +76 -0
  134. package/src/chat-history.css +2 -0
  135. package/src/composer/ComposerPlusMenu.tsx +11 -8
  136. package/src/composer/PromptComposer.tsx +27 -6
  137. package/src/composer/useRealtimeVoiceMode.spec.ts +82 -8
  138. package/src/composer/useRealtimeVoiceMode.tsx +107 -22
  139. package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
  140. package/src/conformance/conformance.spec.tsx +83 -0
  141. package/src/conformance/index.ts +3 -0
  142. package/src/conformance/runner.tsx +710 -0
  143. package/src/conformance/types.ts +45 -0
  144. package/src/conformance/version.ts +22 -0
  145. package/src/design-system/components.tsx +146 -0
  146. package/src/design-system/context.tsx +31 -0
  147. package/src/design-system/default-adapter.spec.tsx +143 -0
  148. package/src/design-system/default-adapter.tsx +915 -0
  149. package/src/design-system/definition.ts +14 -0
  150. package/src/design-system/design-system.spec.tsx +183 -0
  151. package/src/design-system/error-boundary.tsx +37 -0
  152. package/src/design-system/index.ts +7 -0
  153. package/src/design-system/theme.spec.ts +61 -0
  154. package/src/design-system/theme.ts +145 -0
  155. package/src/design-system/types.ts +363 -0
  156. package/src/provider.tsx +98 -5
  157. package/src/ui/button.tsx +95 -6
  158. package/src/ui/dialog.tsx +48 -23
  159. package/src/ui/popover.tsx +16 -2
  160. package/src/ui/select.tsx +7 -3
  161. package/src/ui/sidebar.tsx +47 -48
  162. 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)
@@ -936,22 +1005,11 @@ export function createRealtimeVoiceToolManifestCoordinator(
936
1005
  export function extractRealtimeVoiceFunctionCalls(
937
1006
  event: RealtimeServerEvent,
938
1007
  ): Array<{ name: string; callId: string; argumentsText: string }> {
939
- if (event.type === "response.function_call_arguments.done") {
940
- const name = typeof event.name === "string" ? event.name : "";
941
- const callId = typeof event.call_id === "string" ? event.call_id : "";
942
- if (!name || !callId) return [];
943
- return [
944
- {
945
- name,
946
- callId,
947
- argumentsText:
948
- typeof event.arguments === "string" ? event.arguments : "{}",
949
- },
950
- ];
951
- }
952
1008
  if (event.type !== "response.done") return [];
953
1009
  const response = event.response;
954
1010
  if (!response || typeof response !== "object") return [];
1011
+ const status = (response as { status?: unknown }).status;
1012
+ if (status !== undefined && status !== "completed") return [];
955
1013
  const output = (response as { output?: unknown }).output;
956
1014
  if (!Array.isArray(output)) return [];
957
1015
  return output.flatMap((item) => {
@@ -1141,13 +1199,24 @@ function useRealtimeVoiceModeController(
1141
1199
  const preferencesHydratedRef = useRef(false);
1142
1200
  const preferencesEditedRef = useRef(false);
1143
1201
  const preferenceWriteChainRef = useRef<Promise<void>>(Promise.resolve());
1144
- const toolManifestCoordinator = useMemo(
1202
+ const responseCoordinator = useMemo(
1145
1203
  () =>
1146
- createRealtimeVoiceToolManifestCoordinator((event) => {
1204
+ createRealtimeVoiceResponseCoordinator((event) => {
1147
1205
  sendDataChannelEvent(channelRef.current, event);
1148
1206
  }),
1149
1207
  [],
1150
1208
  );
1209
+ const toolManifestCoordinator = useMemo(
1210
+ () =>
1211
+ createRealtimeVoiceToolManifestCoordinator((event) => {
1212
+ if (event.type === "response.create") {
1213
+ responseCoordinator.request(event);
1214
+ } else {
1215
+ sendDataChannelEvent(channelRef.current, event);
1216
+ }
1217
+ }),
1218
+ [responseCoordinator],
1219
+ );
1151
1220
  const switchMicrophoneRef = useRef<
1152
1221
  (deviceId: string, force?: boolean) => Promise<void>
1153
1222
  >(async () => undefined);
@@ -1461,8 +1530,9 @@ function useRealtimeVoiceModeController(
1461
1530
  if (audioContext) void audioContext.close().catch(() => undefined);
1462
1531
  audioLevels.reset();
1463
1532
  handledCallsRef.current.clear();
1533
+ responseCoordinator.reset();
1464
1534
  toolManifestCoordinator.reset();
1465
- }, [audioLevels, toolManifestCoordinator]);
1535
+ }, [audioLevels, responseCoordinator, toolManifestCoordinator]);
1466
1536
 
1467
1537
  const fail = useCallback(
1468
1538
  (message: string, options?: { openKeySettings?: boolean }) => {
@@ -1537,13 +1607,14 @@ function useRealtimeVoiceModeController(
1537
1607
  const greetingStarter = useMemo(
1538
1608
  () =>
1539
1609
  createRealtimeVoiceGreetingStarter((event) => {
1540
- sendDataChannelEvent(channelRef.current, event);
1610
+ responseCoordinator.request(event);
1541
1611
  }),
1542
- [],
1612
+ [responseCoordinator],
1543
1613
  );
1544
1614
 
1545
1615
  const handleServerEvent = useCallback(
1546
1616
  (event: RealtimeServerEvent) => {
1617
+ responseCoordinator.handleEvent(event);
1547
1618
  transcriptSequencer.handle(event);
1548
1619
  const sessionTools = extractRealtimeVoiceSessionTools(event);
1549
1620
  if (sessionTools) toolManifestCoordinator.setSessionTools(sessionTools);
@@ -1562,6 +1633,7 @@ function useRealtimeVoiceModeController(
1562
1633
  transition("listening");
1563
1634
  } else if (event.type === "input_audio_buffer.speech_stopped") {
1564
1635
  transition("working");
1636
+ responseCoordinator.request();
1565
1637
  } else if (event.type === "response.created") {
1566
1638
  // From this point onward the response is committed to audio output, so
1567
1639
  // changing voices risks violating Realtime's per-session voice lock.
@@ -1593,12 +1665,20 @@ function useRealtimeVoiceModeController(
1593
1665
  ? (response as { status?: unknown }).status
1594
1666
  : undefined;
1595
1667
  if (status === "failed") {
1596
- fail(
1597
- copy?.errors.responseFailed ??
1598
- "OpenAI could not complete the voice response.",
1599
- );
1668
+ transportGenerationRef.current += 1;
1669
+ handledCallsRef.current.clear();
1670
+ responseCoordinator.reset();
1671
+ toolManifestCoordinator.reset();
1672
+ transition("listening");
1600
1673
  return;
1601
1674
  }
1675
+ } else if (event.type === "response.failed") {
1676
+ transportGenerationRef.current += 1;
1677
+ handledCallsRef.current.clear();
1678
+ responseCoordinator.reset();
1679
+ toolManifestCoordinator.reset();
1680
+ transition("listening");
1681
+ return;
1602
1682
  } else if (event.type === "error") {
1603
1683
  const detail = event.error;
1604
1684
  const message =
@@ -1619,6 +1699,10 @@ function useRealtimeVoiceModeController(
1619
1699
  ) {
1620
1700
  return;
1621
1701
  }
1702
+ if (responseCoordinator.handleError(message)) {
1703
+ transition("working");
1704
+ return;
1705
+ }
1622
1706
  fail(
1623
1707
  message ||
1624
1708
  copy?.errors.sessionFailed ||
@@ -1641,6 +1725,7 @@ function useRealtimeVoiceModeController(
1641
1725
  syncAppState,
1642
1726
  transcriptSequencer,
1643
1727
  transition,
1728
+ responseCoordinator,
1644
1729
  toolManifestCoordinator,
1645
1730
  updateLivePreferences,
1646
1731
  ],
@@ -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
+ }
@@ -0,0 +1,83 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { defaultDesignSystemComponents } from "../design-system/default-adapter.js";
6
+ import { DESIGN_SYSTEM_CONTRACT_VERSION } from "../design-system/types.js";
7
+ import { cssInJsFixtureAdapter } from "./__fixtures__/css-in-js-adapter.js";
8
+ import {
9
+ assertDesignSystemConformance,
10
+ DESIGN_SYSTEM_CONFORMANCE_CHECKS,
11
+ runDesignSystemConformance,
12
+ } from "./runner.js";
13
+ import {
14
+ assertDesignSystemContractVersion,
15
+ DESIGN_SYSTEM_CONTRACT_EVOLUTION_POLICY,
16
+ } from "./version.js";
17
+
18
+ class ResizeObserverStub {
19
+ observe() {}
20
+ unobserve() {}
21
+ disconnect() {}
22
+ }
23
+
24
+ describe("design-system conformance kit", () => {
25
+ beforeEach(() => {
26
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", false);
27
+ vi.stubGlobal("ResizeObserver", ResizeObserverStub);
28
+ HTMLElement.prototype.scrollIntoView = vi.fn();
29
+ HTMLElement.prototype.hasPointerCapture = vi.fn(() => false);
30
+ HTMLElement.prototype.setPointerCapture = vi.fn();
31
+ HTMLElement.prototype.releasePointerCapture = vi.fn();
32
+ });
33
+
34
+ afterEach(() => {
35
+ document.body.replaceChildren();
36
+ vi.restoreAllMocks();
37
+ vi.unstubAllGlobals();
38
+ });
39
+
40
+ it("publishes named coverage for all seventeen components and overlay concerns", () => {
41
+ const covered = new Set(
42
+ DESIGN_SYSTEM_CONFORMANCE_CHECKS.flatMap((check) => check.components),
43
+ );
44
+ expect(covered.size).toBe(17);
45
+ expect(DESIGN_SYSTEM_CONFORMANCE_CHECKS.map((check) => check.id)).toEqual(
46
+ expect.arrayContaining([
47
+ "overlay.portal-and-z-index-stacking",
48
+ "overlay.focus-interoperability",
49
+ ]),
50
+ );
51
+ });
52
+
53
+ it("enforces the published contract evolution policy", () => {
54
+ expect(DESIGN_SYSTEM_CONTRACT_EVOLUTION_POLICY.minor).toContain(
55
+ "optional props",
56
+ );
57
+ expect(DESIGN_SYSTEM_CONTRACT_EVOLUTION_POLICY.major).toContain(
58
+ "behavioral",
59
+ );
60
+ expect(() =>
61
+ assertDesignSystemContractVersion(DESIGN_SYSTEM_CONTRACT_VERSION),
62
+ ).not.toThrow();
63
+ expect(() => assertDesignSystemContractVersion(2)).toThrow(/incompatible/);
64
+ });
65
+
66
+ it("passes the styling-independent CSS-in-JS fixture", async () => {
67
+ const report = await assertDesignSystemConformance({
68
+ adapterName: "CSS-in-JS fixture",
69
+ components: cssInJsFixtureAdapter,
70
+ contractVersion: DESIGN_SYSTEM_CONTRACT_VERSION,
71
+ });
72
+ expect(report.passed).toBe(true);
73
+ });
74
+
75
+ it("passes the default shadcn and Radix adapter", async () => {
76
+ const report = await runDesignSystemConformance({
77
+ adapterName: "Toolkit default",
78
+ components: defaultDesignSystemComponents,
79
+ contractVersion: DESIGN_SYSTEM_CONTRACT_VERSION,
80
+ });
81
+ expect(report.results.filter((result) => !result.passed)).toEqual([]);
82
+ });
83
+ });
@@ -0,0 +1,3 @@
1
+ export * from "./runner.js";
2
+ export * from "./types.js";
3
+ export * from "./version.js";