@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.
- package/agent-native.eject.json +71 -0
- package/dist/chat-history/ChatHistoryRail.d.ts +15 -1
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryRail.js +26 -13
- package/dist/chat-history/ChatHistoryRail.js.map +1 -1
- package/dist/chat-history/ChatHistoryRail.spec.js +77 -1
- package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -1
- package/dist/chat-history/index.d.ts +2 -1
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +2 -1
- package/dist/chat-history/index.js.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.d.ts +26 -0
- package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.js +36 -0
- package/dist/chat-history/useChatHistoryRailController.js.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts +2 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js +101 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js.map +1 -0
- package/dist/chat-history.css +2 -0
- package/dist/composer/ComposerPlusMenu.js +1 -1
- package/dist/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +22 -4
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +91 -19
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.spec.js +58 -9
- package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
- package/dist/conformance/conformance.spec.d.ts +2 -0
- package/dist/conformance/conformance.spec.d.ts.map +1 -0
- package/dist/conformance/conformance.spec.js +58 -0
- package/dist/conformance/conformance.spec.js.map +1 -0
- package/dist/conformance/index.d.ts +4 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +4 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/conformance/runner.d.ts +12 -0
- package/dist/conformance/runner.d.ts.map +1 -0
- package/dist/conformance/runner.js +441 -0
- package/dist/conformance/runner.js.map +1 -0
- package/dist/conformance/types.d.ts +26 -0
- package/dist/conformance/types.d.ts.map +1 -0
- package/dist/conformance/types.js +13 -0
- package/dist/conformance/types.js.map +1 -0
- package/dist/conformance/version.d.ts +6 -0
- package/dist/conformance/version.d.ts.map +1 -0
- package/dist/conformance/version.js +14 -0
- package/dist/conformance/version.js.map +1 -0
- package/dist/design-system/components.d.ts +64 -0
- package/dist/design-system/components.d.ts.map +1 -0
- package/dist/design-system/components.js +50 -0
- package/dist/design-system/components.js.map +1 -0
- package/dist/design-system/context.d.ts +11 -0
- package/dist/design-system/context.d.ts.map +1 -0
- package/dist/design-system/context.js +17 -0
- package/dist/design-system/context.js.map +1 -0
- package/dist/design-system/default-adapter.d.ts +3 -0
- package/dist/design-system/default-adapter.d.ts.map +1 -0
- package/dist/design-system/default-adapter.js +166 -0
- package/dist/design-system/default-adapter.js.map +1 -0
- package/dist/design-system/default-adapter.spec.d.ts +2 -0
- package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
- package/dist/design-system/default-adapter.spec.js +96 -0
- package/dist/design-system/default-adapter.spec.js.map +1 -0
- package/dist/design-system/definition.d.ts +9 -0
- package/dist/design-system/definition.d.ts.map +1 -0
- package/dist/design-system/definition.js +4 -0
- package/dist/design-system/definition.js.map +1 -0
- package/dist/design-system/design-system.spec.d.ts +2 -0
- package/dist/design-system/design-system.spec.d.ts.map +1 -0
- package/dist/design-system/design-system.spec.js +121 -0
- package/dist/design-system/design-system.spec.js.map +1 -0
- package/dist/design-system/error-boundary.d.ts +18 -0
- package/dist/design-system/error-boundary.d.ts.map +1 -0
- package/dist/design-system/error-boundary.js +16 -0
- package/dist/design-system/error-boundary.js.map +1 -0
- package/dist/design-system/index.d.ts +8 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +8 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/theme.d.ts +30 -0
- package/dist/design-system/theme.d.ts.map +1 -0
- package/dist/design-system/theme.js +95 -0
- package/dist/design-system/theme.js.map +1 -0
- package/dist/design-system/theme.spec.d.ts +2 -0
- package/dist/design-system/theme.spec.d.ts.map +1 -0
- package/dist/design-system/theme.spec.js +43 -0
- package/dist/design-system/theme.spec.js.map +1 -0
- package/dist/design-system/types.d.ts +288 -0
- package/dist/design-system/types.d.ts.map +1 -0
- package/dist/design-system/types.js +2 -0
- package/dist/design-system/types.js.map +1 -0
- package/dist/provider.d.ts +4 -3
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +46 -4
- package/dist/provider.js.map +1 -1
- package/dist/ui/button.d.ts +5 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +25 -4
- package/dist/ui/button.js.map +1 -1
- package/dist/ui/dialog.d.ts +4 -0
- package/dist/ui/dialog.d.ts.map +1 -1
- package/dist/ui/dialog.js +2 -2
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/popover.d.ts +1 -0
- package/dist/ui/popover.d.ts.map +1 -1
- package/dist/ui/popover.js +2 -2
- package/dist/ui/popover.js.map +1 -1
- package/dist/ui/select.d.ts +3 -1
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +1 -1
- package/dist/ui/select.js.map +1 -1
- package/dist/ui/sidebar.d.ts +6 -2
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +12 -15
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/tooltip.d.ts +3 -1
- package/dist/ui/tooltip.d.ts.map +1 -1
- package/dist/ui/tooltip.js +2 -2
- package/dist/ui/tooltip.js.map +1 -1
- package/package.json +31 -1
- package/src/chat-history/ChatHistoryRail.spec.tsx +174 -0
- package/src/chat-history/ChatHistoryRail.tsx +100 -39
- package/src/chat-history/index.ts +9 -0
- package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
- package/src/chat-history/useChatHistoryRailController.ts +76 -0
- package/src/chat-history.css +2 -0
- package/src/composer/ComposerPlusMenu.tsx +11 -8
- package/src/composer/PromptComposer.tsx +27 -6
- package/src/composer/useRealtimeVoiceMode.spec.ts +82 -8
- package/src/composer/useRealtimeVoiceMode.tsx +107 -22
- package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
- package/src/conformance/conformance.spec.tsx +83 -0
- package/src/conformance/index.ts +3 -0
- package/src/conformance/runner.tsx +710 -0
- package/src/conformance/types.ts +45 -0
- package/src/conformance/version.ts +22 -0
- package/src/design-system/components.tsx +146 -0
- package/src/design-system/context.tsx +31 -0
- package/src/design-system/default-adapter.spec.tsx +143 -0
- package/src/design-system/default-adapter.tsx +915 -0
- package/src/design-system/definition.ts +14 -0
- package/src/design-system/design-system.spec.tsx +183 -0
- package/src/design-system/error-boundary.tsx +37 -0
- package/src/design-system/index.ts +7 -0
- package/src/design-system/theme.spec.ts +61 -0
- package/src/design-system/theme.ts +145 -0
- package/src/design-system/types.ts +363 -0
- package/src/provider.tsx +98 -5
- package/src/ui/button.tsx +95 -6
- package/src/ui/dialog.tsx +48 -23
- package/src/ui/popover.tsx +16 -2
- package/src/ui/select.tsx +7 -3
- package/src/ui/sidebar.tsx +47 -48
- 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
|
|
1202
|
+
const responseCoordinator = useMemo(
|
|
1145
1203
|
() =>
|
|
1146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
+
});
|