@agent-native/toolkit 0.9.0 → 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/README.md +12 -3
- package/agent-native.eject.json +71 -0
- package/dist/chat-history/ChatHistoryRail.d.ts +33 -0
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryRail.js +35 -0
- package/dist/chat-history/ChatHistoryRail.js.map +1 -0
- package/dist/chat-history/ChatHistoryRail.spec.d.ts +2 -0
- package/dist/chat-history/ChatHistoryRail.spec.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryRail.spec.js +157 -0
- package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -0
- package/dist/chat-history/index.d.ts +2 -0
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +2 -0
- 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 +61 -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/RealtimeVoiceMode.d.ts +5 -1
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +27 -8
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.spec.js +51 -11
- package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
- package/dist/composer/VoiceButton.d.ts +2 -0
- package/dist/composer/VoiceButton.d.ts.map +1 -1
- package/dist/composer/VoiceButton.js +33 -1
- package/dist/composer/VoiceButton.js.map +1 -1
- package/dist/composer/VoiceButton.spec.js +9 -1
- package/dist/composer/VoiceButton.spec.js.map +1 -1
- package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -1
- package/dist/composer/realtime-voice-audio-level.js +2 -1
- package/dist/composer/realtime-voice-audio-level.js.map +1 -1
- package/dist/composer/realtime-voice-audio-level.spec.js +7 -0
- package/dist/composer/realtime-voice-audio-level.spec.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 +95 -23
- 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/styles.css +35 -0
- 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 +312 -0
- package/src/chat-history/ChatHistoryRail.tsx +150 -0
- package/src/chat-history/index.ts +14 -0
- package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
- package/src/chat-history/useChatHistoryRailController.ts +76 -0
- package/src/chat-history.css +61 -0
- package/src/composer/ComposerPlusMenu.tsx +11 -8
- package/src/composer/PromptComposer.tsx +27 -6
- package/src/composer/RealtimeVoiceMode.spec.tsx +116 -11
- package/src/composer/RealtimeVoiceMode.tsx +106 -36
- package/src/composer/VoiceButton.spec.ts +17 -1
- package/src/composer/VoiceButton.tsx +49 -0
- package/src/composer/realtime-voice-audio-level.spec.ts +15 -0
- package/src/composer/realtime-voice-audio-level.ts +2 -1
- package/src/composer/useRealtimeVoiceMode.spec.ts +82 -8
- package/src/composer/useRealtimeVoiceMode.tsx +112 -25
- 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/styles.css +35 -0
- 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) => {
|
|
@@ -1018,6 +1076,7 @@ function voiceCopy(t: (key: string) => string): RealtimeVoiceModeCopy {
|
|
|
1018
1076
|
startWithOpenAiKey: t("agentPanel.voiceMode.startWithOpenAiKey"),
|
|
1019
1077
|
startVoiceMode: t("agentPanel.voiceMode.start"),
|
|
1020
1078
|
keepDictating: t("agentPanel.voiceMode.keepDictating"),
|
|
1079
|
+
rememberPreference: t("agentPanel.voiceMode.rememberPreference"),
|
|
1021
1080
|
showChat: t("agentPanel.voiceMode.showChat"),
|
|
1022
1081
|
hideChat: t("agentPanel.voiceMode.hideChat"),
|
|
1023
1082
|
endVoiceMode: t("agentPanel.voiceMode.end"),
|
|
@@ -1140,13 +1199,24 @@ function useRealtimeVoiceModeController(
|
|
|
1140
1199
|
const preferencesHydratedRef = useRef(false);
|
|
1141
1200
|
const preferencesEditedRef = useRef(false);
|
|
1142
1201
|
const preferenceWriteChainRef = useRef<Promise<void>>(Promise.resolve());
|
|
1143
|
-
const
|
|
1202
|
+
const responseCoordinator = useMemo(
|
|
1144
1203
|
() =>
|
|
1145
|
-
|
|
1204
|
+
createRealtimeVoiceResponseCoordinator((event) => {
|
|
1146
1205
|
sendDataChannelEvent(channelRef.current, event);
|
|
1147
1206
|
}),
|
|
1148
1207
|
[],
|
|
1149
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
|
+
);
|
|
1150
1220
|
const switchMicrophoneRef = useRef<
|
|
1151
1221
|
(deviceId: string, force?: boolean) => Promise<void>
|
|
1152
1222
|
>(async () => undefined);
|
|
@@ -1353,7 +1423,7 @@ function useRealtimeVoiceModeController(
|
|
|
1353
1423
|
analyser.fftSize = 512;
|
|
1354
1424
|
const source = context.createMediaStreamSource(stream);
|
|
1355
1425
|
source.connect(analyser);
|
|
1356
|
-
const buffer = new Uint8Array(analyser.
|
|
1426
|
+
const buffer = new Uint8Array(analyser.fftSize);
|
|
1357
1427
|
if (channel === "input") {
|
|
1358
1428
|
inputSourceRef.current?.disconnect();
|
|
1359
1429
|
inputSourceRef.current = source;
|
|
@@ -1460,8 +1530,9 @@ function useRealtimeVoiceModeController(
|
|
|
1460
1530
|
if (audioContext) void audioContext.close().catch(() => undefined);
|
|
1461
1531
|
audioLevels.reset();
|
|
1462
1532
|
handledCallsRef.current.clear();
|
|
1533
|
+
responseCoordinator.reset();
|
|
1463
1534
|
toolManifestCoordinator.reset();
|
|
1464
|
-
}, [audioLevels, toolManifestCoordinator]);
|
|
1535
|
+
}, [audioLevels, responseCoordinator, toolManifestCoordinator]);
|
|
1465
1536
|
|
|
1466
1537
|
const fail = useCallback(
|
|
1467
1538
|
(message: string, options?: { openKeySettings?: boolean }) => {
|
|
@@ -1536,13 +1607,14 @@ function useRealtimeVoiceModeController(
|
|
|
1536
1607
|
const greetingStarter = useMemo(
|
|
1537
1608
|
() =>
|
|
1538
1609
|
createRealtimeVoiceGreetingStarter((event) => {
|
|
1539
|
-
|
|
1610
|
+
responseCoordinator.request(event);
|
|
1540
1611
|
}),
|
|
1541
|
-
[],
|
|
1612
|
+
[responseCoordinator],
|
|
1542
1613
|
);
|
|
1543
1614
|
|
|
1544
1615
|
const handleServerEvent = useCallback(
|
|
1545
1616
|
(event: RealtimeServerEvent) => {
|
|
1617
|
+
responseCoordinator.handleEvent(event);
|
|
1546
1618
|
transcriptSequencer.handle(event);
|
|
1547
1619
|
const sessionTools = extractRealtimeVoiceSessionTools(event);
|
|
1548
1620
|
if (sessionTools) toolManifestCoordinator.setSessionTools(sessionTools);
|
|
@@ -1561,6 +1633,7 @@ function useRealtimeVoiceModeController(
|
|
|
1561
1633
|
transition("listening");
|
|
1562
1634
|
} else if (event.type === "input_audio_buffer.speech_stopped") {
|
|
1563
1635
|
transition("working");
|
|
1636
|
+
responseCoordinator.request();
|
|
1564
1637
|
} else if (event.type === "response.created") {
|
|
1565
1638
|
// From this point onward the response is committed to audio output, so
|
|
1566
1639
|
// changing voices risks violating Realtime's per-session voice lock.
|
|
@@ -1592,12 +1665,20 @@ function useRealtimeVoiceModeController(
|
|
|
1592
1665
|
? (response as { status?: unknown }).status
|
|
1593
1666
|
: undefined;
|
|
1594
1667
|
if (status === "failed") {
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
);
|
|
1668
|
+
transportGenerationRef.current += 1;
|
|
1669
|
+
handledCallsRef.current.clear();
|
|
1670
|
+
responseCoordinator.reset();
|
|
1671
|
+
toolManifestCoordinator.reset();
|
|
1672
|
+
transition("listening");
|
|
1599
1673
|
return;
|
|
1600
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;
|
|
1601
1682
|
} else if (event.type === "error") {
|
|
1602
1683
|
const detail = event.error;
|
|
1603
1684
|
const message =
|
|
@@ -1618,6 +1699,10 @@ function useRealtimeVoiceModeController(
|
|
|
1618
1699
|
) {
|
|
1619
1700
|
return;
|
|
1620
1701
|
}
|
|
1702
|
+
if (responseCoordinator.handleError(message)) {
|
|
1703
|
+
transition("working");
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1621
1706
|
fail(
|
|
1622
1707
|
message ||
|
|
1623
1708
|
copy?.errors.sessionFailed ||
|
|
@@ -1640,6 +1725,7 @@ function useRealtimeVoiceModeController(
|
|
|
1640
1725
|
syncAppState,
|
|
1641
1726
|
transcriptSequencer,
|
|
1642
1727
|
transition,
|
|
1728
|
+
responseCoordinator,
|
|
1643
1729
|
toolManifestCoordinator,
|
|
1644
1730
|
updateLivePreferences,
|
|
1645
1731
|
],
|
|
@@ -1728,9 +1814,10 @@ function useRealtimeVoiceModeController(
|
|
|
1728
1814
|
audio.setAttribute("playsinline", "");
|
|
1729
1815
|
audioRef.current = audio;
|
|
1730
1816
|
peer.ontrack = (trackEvent) => {
|
|
1731
|
-
const remoteStream =
|
|
1817
|
+
const remoteStream =
|
|
1818
|
+
trackEvent.streams[0] ?? new MediaStream([trackEvent.track]);
|
|
1732
1819
|
audio.srcObject = remoteStream;
|
|
1733
|
-
|
|
1820
|
+
attachAudioMeter(remoteStream, "output");
|
|
1734
1821
|
void audio.play().catch(() => undefined);
|
|
1735
1822
|
};
|
|
1736
1823
|
|
|
@@ -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
|
+
});
|