@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
@@ -0,0 +1,862 @@
1
+ import {
2
+ createElement,
3
+ useRef,
4
+ useState,
5
+ type ReactElement,
6
+ type ReactNode,
7
+ } from "react";
8
+ import { flushSync } from "react-dom";
9
+ import { createRoot, type Root } from "react-dom/client";
10
+
11
+ import { DESIGN_SYSTEM_CONTRACT_VERSION } from "../design-system/types.js";
12
+ import type { DesignSystemComponents } from "../design-system/types.js";
13
+ import type {
14
+ DesignSystemConformanceCategory,
15
+ DesignSystemConformanceCheckResult,
16
+ DesignSystemConformanceReport,
17
+ RunDesignSystemConformanceOptions,
18
+ } from "./types.js";
19
+ import { assertDesignSystemContractVersion } from "./version.js";
20
+
21
+ interface MountedProbe {
22
+ container: HTMLDivElement;
23
+ root: Root;
24
+ }
25
+
26
+ interface CheckContext {
27
+ components: DesignSystemComponents;
28
+ document: Document;
29
+ mount(element: ReactElement): MountedProbe;
30
+ unmount(probe: MountedProbe): void;
31
+ settle(): Promise<void>;
32
+ }
33
+
34
+ interface ConformanceCheck {
35
+ id: string;
36
+ category: DesignSystemConformanceCategory;
37
+ components: readonly (keyof DesignSystemComponents)[];
38
+ run(context: CheckContext): void | Promise<void>;
39
+ }
40
+
41
+ const contractComponentNames = [
42
+ "ActionButton",
43
+ "IconButton",
44
+ "TextField",
45
+ "TextArea",
46
+ "Spinner",
47
+ "Skeleton",
48
+ "Status",
49
+ "Surface",
50
+ "Avatar",
51
+ "Tooltip",
52
+ "Menu",
53
+ "Popover",
54
+ "Dialog",
55
+ "Picker",
56
+ "Checkbox",
57
+ "Switch",
58
+ "Tabs",
59
+ ] as const satisfies readonly (keyof DesignSystemComponents)[];
60
+
61
+ function invariant(value: unknown, message: string): asserts value {
62
+ if (!value) throw new Error(message);
63
+ }
64
+
65
+ function dispatch(element: Element, event: Event): void {
66
+ flushSync(() => element.dispatchEvent(event));
67
+ }
68
+
69
+ function click(element: Element, document: Document): void {
70
+ for (const type of ["pointerdown", "mousedown"]) {
71
+ dispatch(
72
+ element,
73
+ new document.defaultView!.MouseEvent(type, {
74
+ bubbles: true,
75
+ cancelable: true,
76
+ button: 0,
77
+ }),
78
+ );
79
+ }
80
+ dispatch(
81
+ element,
82
+ new document.defaultView!.MouseEvent("click", {
83
+ bubbles: true,
84
+ cancelable: true,
85
+ button: 0,
86
+ }),
87
+ );
88
+ }
89
+
90
+ function keydown(element: Element, key: string, document: Document): void {
91
+ dispatch(
92
+ element,
93
+ new document.defaultView!.KeyboardEvent("keydown", {
94
+ key,
95
+ bubbles: true,
96
+ cancelable: true,
97
+ }),
98
+ );
99
+ }
100
+
101
+ function input(element: HTMLInputElement | HTMLTextAreaElement, value: string) {
102
+ const prototype =
103
+ element instanceof element.ownerDocument.defaultView!.HTMLTextAreaElement
104
+ ? element.ownerDocument.defaultView!.HTMLTextAreaElement.prototype
105
+ : element.ownerDocument.defaultView!.HTMLInputElement.prototype;
106
+ Object.getOwnPropertyDescriptor(prototype, "value")?.set?.call(
107
+ element,
108
+ value,
109
+ );
110
+ dispatch(
111
+ element,
112
+ new element.ownerDocument.defaultView!.Event("input", {
113
+ bubbles: true,
114
+ cancelable: true,
115
+ }),
116
+ );
117
+ }
118
+
119
+ function elementWithText(document: Document, selector: string, text: string) {
120
+ return [...document.querySelectorAll(selector)].find((element) =>
121
+ element.textContent?.includes(text),
122
+ );
123
+ }
124
+
125
+ function assertInlineLayout(
126
+ container: Element,
127
+ iconSelector: string,
128
+ labelSelector: string,
129
+ message: string,
130
+ ) {
131
+ const icon = container.querySelector<HTMLElement>(iconSelector);
132
+ const label = container.querySelector<HTMLElement>(labelSelector);
133
+ invariant(icon && label, message);
134
+
135
+ const iconRect = icon.getBoundingClientRect();
136
+ const labelRect = label.getBoundingClientRect();
137
+ const hasGeometry =
138
+ iconRect.width > 0 ||
139
+ iconRect.height > 0 ||
140
+ labelRect.width > 0 ||
141
+ labelRect.height > 0;
142
+ if (hasGeometry) {
143
+ const verticalOverlap =
144
+ Math.min(iconRect.bottom, labelRect.bottom) -
145
+ Math.max(iconRect.top, labelRect.top);
146
+ invariant(
147
+ verticalOverlap > 0 || Math.abs(iconRect.top - labelRect.top) <= 2,
148
+ message,
149
+ );
150
+ return;
151
+ }
152
+
153
+ // happy-dom does not calculate layout. Still reject adapters that explicitly
154
+ // stack the tab contents, while browser-backed runs use the geometry check.
155
+ const commonAncestor = (() => {
156
+ let candidate: Element | null = icon;
157
+ while (candidate && !candidate.contains(label)) {
158
+ candidate = candidate.parentElement;
159
+ }
160
+ return candidate;
161
+ })();
162
+ const styles = [
163
+ container,
164
+ commonAncestor,
165
+ icon.parentElement,
166
+ label.parentElement,
167
+ ]
168
+ .filter((element): element is Element => Boolean(element))
169
+ .map((element) =>
170
+ element.ownerDocument.defaultView?.getComputedStyle(element),
171
+ );
172
+ invariant(
173
+ !styles.some(
174
+ (style) => style?.display === "flex" && style.flexDirection === "column",
175
+ ),
176
+ message,
177
+ );
178
+ }
179
+
180
+ function assertContainedFooter(dialog: Element, footerSelector: string) {
181
+ const footer = dialog.querySelector<HTMLElement>(footerSelector);
182
+ invariant(
183
+ footer,
184
+ "Dialog footer controls must be rendered inside the dialog surface.",
185
+ );
186
+
187
+ const dialogRect = (dialog as HTMLElement).getBoundingClientRect();
188
+ const footerRect = footer.getBoundingClientRect();
189
+ const hasGeometry =
190
+ dialogRect.width > 0 ||
191
+ dialogRect.height > 0 ||
192
+ footerRect.width > 0 ||
193
+ footerRect.height > 0;
194
+ if (!hasGeometry) return;
195
+
196
+ const epsilon = 2;
197
+ invariant(
198
+ footerRect.top >= dialogRect.top - epsilon &&
199
+ footerRect.bottom <= dialogRect.bottom + epsilon &&
200
+ footerRect.left >= dialogRect.left - epsilon &&
201
+ footerRect.right <= dialogRect.right + epsilon,
202
+ "Dialog footer controls must remain within the dialog surface bounds.",
203
+ );
204
+ }
205
+
206
+ const checks: readonly ConformanceCheck[] = [
207
+ {
208
+ id: "contract.components",
209
+ category: "contract",
210
+ components: contractComponentNames,
211
+ run: ({ components }) => {
212
+ for (const name of contractComponentNames) {
213
+ invariant(
214
+ typeof components[name] === "function",
215
+ `${name} must be supplied as a React component.`,
216
+ );
217
+ }
218
+ },
219
+ },
220
+ {
221
+ id: "leaf.action-button",
222
+ category: "leaf",
223
+ components: ["ActionButton"],
224
+ run: ({ components, document, mount, unmount }) => {
225
+ let presses = 0;
226
+ const probe = mount(
227
+ <components.ActionButton
228
+ intent="danger"
229
+ emphasis="outline"
230
+ size="compact"
231
+ onPress={() => presses++}
232
+ >
233
+ Remove
234
+ </components.ActionButton>,
235
+ );
236
+ const button = probe.container.querySelector("button");
237
+ invariant(button, "ActionButton must render an operable button.");
238
+ click(button, document);
239
+ invariant(presses === 1, "ActionButton must call onPress once.");
240
+ unmount(probe);
241
+ },
242
+ },
243
+ {
244
+ id: "leaf.icon-button",
245
+ category: "leaf",
246
+ components: ["IconButton"],
247
+ run: ({ components, document, mount, unmount }) => {
248
+ let presses = 0;
249
+ const probe = mount(
250
+ <components.IconButton
251
+ label="Close panel"
252
+ icon={<span aria-hidden="true">x</span>}
253
+ onPress={() => presses++}
254
+ />,
255
+ );
256
+ const button = probe.container.querySelector(
257
+ 'button[aria-label="Close panel"]',
258
+ );
259
+ invariant(
260
+ button,
261
+ "IconButton must expose its label as an accessible name.",
262
+ );
263
+ click(button, document);
264
+ invariant(presses === 1, "IconButton must call onPress once.");
265
+ unmount(probe);
266
+ },
267
+ },
268
+ {
269
+ id: "leaf.text-field",
270
+ category: "leaf",
271
+ components: ["TextField"],
272
+ run: ({ components, mount, unmount }) => {
273
+ let value = "";
274
+ const probe = mount(
275
+ <components.TextField
276
+ aria-label="Project name"
277
+ value={value}
278
+ onChange={(next) => (value = next)}
279
+ />,
280
+ );
281
+ const field = probe.container.querySelector("input");
282
+ invariant(field, "TextField must render an input.");
283
+ input(field, "Atlas");
284
+ invariant(value === "Atlas", "TextField must report string values.");
285
+ unmount(probe);
286
+ },
287
+ },
288
+ {
289
+ id: "leaf.text-area",
290
+ category: "leaf",
291
+ components: ["TextArea"],
292
+ run: ({ components, mount, unmount }) => {
293
+ let value = "";
294
+ const probe = mount(
295
+ <components.TextArea
296
+ aria-label="Notes"
297
+ value={value}
298
+ onChange={(next) => (value = next)}
299
+ />,
300
+ );
301
+ const field = probe.container.querySelector("textarea");
302
+ invariant(field, "TextArea must render a textarea.");
303
+ input(field, "Conformant");
304
+ invariant(value === "Conformant", "TextArea must report string values.");
305
+ unmount(probe);
306
+ },
307
+ },
308
+ {
309
+ id: "leaf.progress-and-placeholder",
310
+ category: "leaf",
311
+ components: ["Spinner", "Skeleton"],
312
+ run: ({ components, mount, unmount }) => {
313
+ const probe = mount(
314
+ <>
315
+ <components.Spinner label="Loading records" size="compact" />
316
+ <components.Skeleton width={48} height={12} shape="line" />
317
+ </>,
318
+ );
319
+ invariant(
320
+ probe.container.querySelector('[role="status"]'),
321
+ "A labeled Spinner must expose status semantics.",
322
+ );
323
+ invariant(
324
+ probe.container.querySelector('[aria-hidden="true"]'),
325
+ "Skeleton must be hidden from assistive technology.",
326
+ );
327
+ unmount(probe);
328
+ },
329
+ },
330
+ {
331
+ id: "leaf.status-surface-avatar",
332
+ category: "leaf",
333
+ components: ["Status", "Surface", "Avatar"],
334
+ run: ({ components, document, mount, unmount }) => {
335
+ let presses = 0;
336
+ const probe = mount(
337
+ <>
338
+ <components.Status tone="success">Connected</components.Status>
339
+ <components.Surface interactive onPress={() => presses++}>
340
+ Open workspace
341
+ </components.Surface>
342
+ <components.Avatar name="Ada Lovelace" fallback="AL" />
343
+ </>,
344
+ );
345
+ invariant(
346
+ probe.container.textContent?.includes("Connected"),
347
+ "Status must render its content.",
348
+ );
349
+ const surface = elementWithText(
350
+ document,
351
+ '[role="button"]',
352
+ "Open workspace",
353
+ );
354
+ invariant(
355
+ surface,
356
+ "An interactive Surface must expose button semantics.",
357
+ );
358
+ keydown(surface, "Enter", document);
359
+ invariant(
360
+ presses === 1,
361
+ "An interactive Surface must support keyboard press.",
362
+ );
363
+ invariant(
364
+ probe.container.textContent?.includes("AL"),
365
+ "Avatar must render a fallback when no image is supplied.",
366
+ );
367
+ unmount(probe);
368
+ },
369
+ },
370
+ {
371
+ id: "behavior.tooltip",
372
+ category: "behavior",
373
+ components: ["Tooltip"],
374
+ run: async ({ components, document, mount, settle, unmount }) => {
375
+ const portal = document.createElement("div");
376
+ portal.dataset.conformancePortal = "true";
377
+ document.body.appendChild(portal);
378
+ const probe = mount(
379
+ <components.Tooltip
380
+ trigger={<button>Help</button>}
381
+ content="Helpful context"
382
+ open
383
+ portalContainer={portal}
384
+ />,
385
+ );
386
+ await settle();
387
+ invariant(
388
+ portal.textContent?.includes("Helpful context"),
389
+ "Tooltip must honor controlled open and portalContainer.",
390
+ );
391
+ unmount(probe);
392
+
393
+ const defaultOpenProbe = mount(
394
+ <components.Tooltip
395
+ trigger={<button>Help by default</button>}
396
+ content="Default tooltip"
397
+ defaultOpen
398
+ portalContainer={portal}
399
+ />,
400
+ );
401
+ await settle();
402
+ invariant(
403
+ portal.textContent?.includes("Default tooltip"),
404
+ "Tooltip must honor defaultOpen and portalContainer.",
405
+ );
406
+ unmount(defaultOpenProbe);
407
+ portal.remove();
408
+ },
409
+ },
410
+ {
411
+ id: "behavior.menu",
412
+ category: "behavior",
413
+ components: ["Menu"],
414
+ run: async ({ components, document, mount, settle, unmount }) => {
415
+ let selected: string | number | undefined;
416
+ const probe = mount(
417
+ <components.Menu
418
+ trigger={<button>Actions</button>}
419
+ items={[{ id: "archive", label: "Archive" }]}
420
+ open
421
+ onAction={(id) => (selected = id)}
422
+ />,
423
+ );
424
+ await settle();
425
+ const item = elementWithText(document, '[role="menuitem"]', "Archive");
426
+ invariant(item, "Menu must render open items with menuitem semantics.");
427
+ click(item, document);
428
+ invariant(selected === "archive", "Menu must report selected item ids.");
429
+ unmount(probe);
430
+
431
+ const defaultOpenProbe = mount(
432
+ <components.Menu
433
+ trigger={<button>Default actions</button>}
434
+ items={[{ id: "restore", label: "Restore" }]}
435
+ defaultOpen
436
+ onAction={() => {}}
437
+ />,
438
+ );
439
+ await settle();
440
+ invariant(
441
+ elementWithText(document, '[role="menuitem"]', "Restore"),
442
+ "Menu must honor defaultOpen.",
443
+ );
444
+ unmount(defaultOpenProbe);
445
+ },
446
+ },
447
+ {
448
+ id: "behavior.popover",
449
+ category: "behavior",
450
+ components: ["Popover"],
451
+ run: async ({ components, document, mount, settle, unmount }) => {
452
+ const portal = document.createElement("div");
453
+ portal.dataset.conformancePortal = "true";
454
+ document.body.appendChild(portal);
455
+ const probe = mount(
456
+ <components.Popover
457
+ trigger={<button>Details</button>}
458
+ open
459
+ portalContainer={portal}
460
+ >
461
+ Popover details
462
+ </components.Popover>,
463
+ );
464
+ await settle();
465
+ invariant(
466
+ portal.textContent?.includes("Popover details"),
467
+ "Popover must honor controlled open and portalContainer.",
468
+ );
469
+ unmount(probe);
470
+ portal.remove();
471
+ },
472
+ },
473
+ {
474
+ id: "behavior.dialog",
475
+ category: "behavior",
476
+ components: ["Dialog"],
477
+ run: async ({ components, document, mount, settle, unmount }) => {
478
+ const portal = document.createElement("div");
479
+ portal.dataset.conformancePortal = "true";
480
+ document.body.appendChild(portal);
481
+ const probe = mount(
482
+ <components.Dialog
483
+ open
484
+ onOpenChange={() => {}}
485
+ title="Edit project"
486
+ dismissible={false}
487
+ portalContainer={portal}
488
+ >
489
+ <input aria-label="Project title" />
490
+ </components.Dialog>,
491
+ );
492
+ await settle();
493
+ invariant(
494
+ portal.querySelector('[role="dialog"]'),
495
+ "Dialog must expose dialog semantics.",
496
+ );
497
+ invariant(
498
+ portal.textContent?.includes("Edit project"),
499
+ "Dialog must expose its title and honor portalContainer.",
500
+ );
501
+ invariant(
502
+ portal.querySelector('[role="dialog"]')?.querySelectorAll("button")
503
+ .length === 0,
504
+ "Dialog must not invent actions when footer is omitted.",
505
+ );
506
+ unmount(probe);
507
+
508
+ const footerProbe = mount(
509
+ <components.Dialog
510
+ open
511
+ onOpenChange={() => {}}
512
+ title="Footer probe"
513
+ portalContainer={portal}
514
+ footer={
515
+ <button data-conformance-dialog-footer-action="true" type="button">
516
+ Save
517
+ </button>
518
+ }
519
+ >
520
+ Footer content
521
+ </components.Dialog>,
522
+ );
523
+ await settle();
524
+ const footerDialog = portal.querySelector('[role="dialog"]');
525
+ invariant(footerDialog, "Dialog footer probe did not render a dialog.");
526
+ assertContainedFooter(
527
+ footerDialog,
528
+ '[data-conformance-dialog-footer-action="true"]',
529
+ );
530
+ unmount(footerProbe);
531
+ portal.remove();
532
+ },
533
+ },
534
+ {
535
+ id: "behavior.picker",
536
+ category: "behavior",
537
+ components: ["Picker"],
538
+ run: async ({ components, document, mount, settle, unmount }) => {
539
+ let value: string | null = null;
540
+ const probe = mount(
541
+ <components.Picker
542
+ mode="select"
543
+ aria-label="Owner"
544
+ options={[{ value: "ada", label: "Ada" }]}
545
+ value={value}
546
+ onChange={(next) => (value = next)}
547
+ open
548
+ />,
549
+ );
550
+ await settle();
551
+ const option = elementWithText(document, '[role="option"]', "Ada");
552
+ invariant(option, "An open Picker must expose options.");
553
+ click(option, document);
554
+ invariant(value === "ada", "Picker must report option values.");
555
+ unmount(probe);
556
+ },
557
+ },
558
+ {
559
+ id: "behavior.checkbox-switch",
560
+ category: "behavior",
561
+ components: ["Checkbox", "Switch"],
562
+ run: ({ components, document, mount, unmount }) => {
563
+ let checked = false;
564
+ let switched = false;
565
+ const probe = mount(
566
+ <>
567
+ <components.Checkbox
568
+ aria-label="Include archived"
569
+ checked={checked}
570
+ onChange={(next) => (checked = next)}
571
+ />
572
+ <components.Switch
573
+ aria-label="Notifications"
574
+ checked={switched}
575
+ onChange={(next) => (switched = next)}
576
+ />
577
+ </>,
578
+ );
579
+ const checkbox = probe.container.querySelector('[role="checkbox"]');
580
+ const toggle = probe.container.querySelector('[role="switch"]');
581
+ invariant(checkbox, "Checkbox must expose checkbox semantics.");
582
+ invariant(toggle, "Switch must expose switch semantics.");
583
+ click(checkbox, document);
584
+ click(toggle, document);
585
+ invariant(
586
+ checked && switched,
587
+ "Checkbox and Switch must report state changes.",
588
+ );
589
+ unmount(probe);
590
+ },
591
+ },
592
+ {
593
+ id: "behavior.tabs",
594
+ category: "behavior",
595
+ components: ["Tabs"],
596
+ run: ({ components, document, mount, unmount }) => {
597
+ let value = "first";
598
+ const probe = mount(
599
+ <components.Tabs
600
+ value={value}
601
+ orientation="vertical"
602
+ onChange={(next) => (value = next)}
603
+ items={[
604
+ {
605
+ value: "first",
606
+ label: <span data-conformance-tab-label="true">First</span>,
607
+ icon: (
608
+ <span data-conformance-tab-icon="true" aria-hidden="true">
609
+
610
+ </span>
611
+ ),
612
+ content: "One",
613
+ },
614
+ { value: "second", label: "Second", content: "Two" },
615
+ ]}
616
+ />,
617
+ );
618
+ const tabList = probe.container.querySelector('[role="tablist"]');
619
+ invariant(tabList, "Tabs must expose a tablist.");
620
+ invariant(
621
+ tabList.getAttribute("aria-orientation") === "vertical",
622
+ "Tabs must honor vertical orientation in their tablist semantics.",
623
+ );
624
+ const first = elementWithText(document, '[role="tab"]', "First");
625
+ invariant(first, "Tabs must expose the icon-bearing tab.");
626
+ assertInlineLayout(
627
+ first,
628
+ '[data-conformance-tab-icon="true"]',
629
+ '[data-conformance-tab-label="true"]',
630
+ "Tabs must lay out an icon and its label on the same row.",
631
+ );
632
+ const second = elementWithText(document, '[role="tab"]', "Second");
633
+ invariant(second, "Tabs must expose tab semantics.");
634
+ click(second, document);
635
+ invariant(value === "second", "Tabs must report selected values.");
636
+ unmount(probe);
637
+ },
638
+ },
639
+ {
640
+ id: "overlay.portal-and-z-index-stacking",
641
+ category: "overlay-interoperability",
642
+ components: ["Dialog"],
643
+ run: async ({ components, document, mount, settle, unmount }) => {
644
+ const radixHost = document.createElement("div");
645
+ radixHost.dataset.hostedOverlay = "radix";
646
+ radixHost.dataset.conformancePortal = "true";
647
+ radixHost.style.zIndex = "40";
648
+ document.body.appendChild(radixHost);
649
+ const customerOverlayLayer = document.createElement("div");
650
+ customerOverlayLayer.dataset.designSystemOverlayLayer = "true";
651
+ customerOverlayLayer.dataset.conformancePortal = "true";
652
+ customerOverlayLayer.style.position = "relative";
653
+ customerOverlayLayer.style.zIndex = "50";
654
+ document.body.appendChild(customerOverlayLayer);
655
+ const probe = mount(
656
+ <components.Dialog
657
+ open
658
+ onOpenChange={() => {}}
659
+ title="Stacking probe"
660
+ portalContainer={customerOverlayLayer}
661
+ >
662
+ Overlay content
663
+ </components.Dialog>,
664
+ );
665
+ await settle();
666
+ invariant(
667
+ customerOverlayLayer.querySelector('[role="dialog"]'),
668
+ "Behavior adapters must portal into the supplied stacking layer so they can interoperate with Radix-hosted surfaces.",
669
+ );
670
+ invariant(
671
+ Number(customerOverlayLayer.style.zIndex) >
672
+ Number(radixHost.style.zIndex),
673
+ "The supplied design-system portal layer must be able to stack above an existing hosted overlay.",
674
+ );
675
+ unmount(probe);
676
+ customerOverlayLayer.remove();
677
+ radixHost.remove();
678
+ },
679
+ },
680
+ {
681
+ id: "overlay.focus-interoperability",
682
+ category: "overlay-interoperability",
683
+ components: ["Dialog"],
684
+ run: async ({ components, document, mount, settle, unmount }) => {
685
+ function FocusProbe() {
686
+ const [open, setOpen] = useState(true);
687
+ const initialFocusRef = useRef<HTMLInputElement>(null);
688
+ const restoreFocusRef = useRef<HTMLButtonElement>(null);
689
+ return (
690
+ <>
691
+ <button ref={restoreFocusRef}>Hosted trigger</button>
692
+ <components.Dialog
693
+ open={open}
694
+ onOpenChange={setOpen}
695
+ title="Focus probe"
696
+ initialFocusRef={initialFocusRef}
697
+ restoreFocusRef={restoreFocusRef}
698
+ >
699
+ <input ref={initialFocusRef} aria-label="Initial focus" />
700
+ <button onClick={() => setOpen(false)}>Close probe</button>
701
+ </components.Dialog>
702
+ </>
703
+ );
704
+ }
705
+ const probe = mount(<FocusProbe />);
706
+ await settle();
707
+ const initial = document.querySelector<HTMLInputElement>(
708
+ 'input[aria-label="Initial focus"]',
709
+ );
710
+ invariant(initial, "Dialog must render its initial focus target.");
711
+ invariant(
712
+ document.activeElement === initial,
713
+ "Dialog must honor initialFocusRef across adapter overlay implementations.",
714
+ );
715
+ const close = elementWithText(document, "button", "Close probe");
716
+ invariant(close, "Focus probe close control did not render.");
717
+ click(close, document);
718
+ await settle();
719
+ invariant(
720
+ document.activeElement?.textContent === "Hosted trigger",
721
+ "Dialog must restore focus to a hosted trigger after its native overlay closes.",
722
+ );
723
+ unmount(probe);
724
+ },
725
+ },
726
+ ];
727
+
728
+ export const DESIGN_SYSTEM_CONFORMANCE_CHECKS = checks.map(
729
+ ({ id, category, components }) => ({ id, category, components }),
730
+ );
731
+
732
+ export async function runDesignSystemConformance({
733
+ adapterName = "custom adapter",
734
+ components,
735
+ contractVersion,
736
+ document: suppliedDocument,
737
+ }: RunDesignSystemConformanceOptions): Promise<DesignSystemConformanceReport> {
738
+ const document = suppliedDocument ?? globalThis.document;
739
+ if (!document?.body || !document.defaultView) {
740
+ throw new Error(
741
+ "runDesignSystemConformance requires a browser-like document, such as Playwright, jsdom, or happy-dom.",
742
+ );
743
+ }
744
+
745
+ const results: DesignSystemConformanceCheckResult[] = [];
746
+ try {
747
+ assertDesignSystemContractVersion(contractVersion);
748
+ results.push({
749
+ id: "contract.version-policy",
750
+ category: "contract",
751
+ components: [],
752
+ passed: true,
753
+ });
754
+ } catch (error) {
755
+ results.push({
756
+ id: "contract.version-policy",
757
+ category: "contract",
758
+ components: [],
759
+ passed: false,
760
+ message: error instanceof Error ? error.message : String(error),
761
+ });
762
+ }
763
+
764
+ const mounted = new Set<MountedProbe>();
765
+ const exercisedComponents = new Set<keyof DesignSystemComponents>();
766
+ const trackedComponents = new Proxy(components, {
767
+ get(target, property, receiver) {
768
+ if (typeof property === "string" && property in target) {
769
+ exercisedComponents.add(property as keyof DesignSystemComponents);
770
+ }
771
+ return Reflect.get(target, property, receiver);
772
+ },
773
+ });
774
+ const context: CheckContext = {
775
+ components: trackedComponents,
776
+ document,
777
+ mount(element) {
778
+ const container = document.createElement("div");
779
+ container.dataset.conformanceRoot = "true";
780
+ document.body.appendChild(container);
781
+ const root = createRoot(container);
782
+ const probe = { container, root };
783
+ mounted.add(probe);
784
+ flushSync(() => root.render(element));
785
+ return probe;
786
+ },
787
+ unmount(probe) {
788
+ if (!mounted.delete(probe)) return;
789
+ flushSync(() => probe.root.unmount());
790
+ probe.container.remove();
791
+ },
792
+ async settle() {
793
+ await new Promise<void>((resolve) =>
794
+ document.defaultView!.setTimeout(resolve, 0),
795
+ );
796
+ },
797
+ };
798
+
799
+ for (const check of checks) {
800
+ try {
801
+ exercisedComponents.clear();
802
+ await check.run(context);
803
+ const missingComponents = check.components.filter(
804
+ (component) => !exercisedComponents.has(component),
805
+ );
806
+ invariant(
807
+ missingComponents.length === 0,
808
+ `Conformance check ${check.id} declared ${missingComponents.join(", ")} but did not exercise them.`,
809
+ );
810
+ results.push({
811
+ id: check.id,
812
+ category: check.category,
813
+ components: check.components,
814
+ passed: true,
815
+ });
816
+ } catch (error) {
817
+ results.push({
818
+ id: check.id,
819
+ category: check.category,
820
+ components: check.components,
821
+ passed: false,
822
+ message: error instanceof Error ? error.message : String(error),
823
+ });
824
+ } finally {
825
+ for (const probe of [...mounted]) context.unmount(probe);
826
+ document
827
+ .querySelectorAll("[data-radix-popper-content-wrapper]")
828
+ .forEach((element) => element.remove());
829
+ document
830
+ .querySelectorAll('[data-conformance-portal="true"]')
831
+ .forEach((element) => element.remove());
832
+ }
833
+ }
834
+
835
+ const report: DesignSystemConformanceReport = {
836
+ adapterName,
837
+ contractVersion: DESIGN_SYSTEM_CONTRACT_VERSION,
838
+ passed: results.every((result) => result.passed),
839
+ results,
840
+ };
841
+ return report;
842
+ }
843
+
844
+ export async function assertDesignSystemConformance(
845
+ options: RunDesignSystemConformanceOptions,
846
+ ): Promise<DesignSystemConformanceReport> {
847
+ const report = await runDesignSystemConformance(options);
848
+ if (!report.passed) {
849
+ const { DesignSystemConformanceError } = await import("./types.js");
850
+ throw new DesignSystemConformanceError(report);
851
+ }
852
+ return report;
853
+ }
854
+
855
+ export function renderDesignSystemConformanceFixture(
856
+ components: DesignSystemComponents,
857
+ ): ReactNode {
858
+ return createElement(components.Status, {
859
+ tone: "info",
860
+ children: "Adapter loaded",
861
+ });
862
+ }