@agent-native/toolkit 0.10.0 → 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.
- package/dist/composer/PromptComposer.d.ts +1 -0
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +4 -1
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/PromptComposer.spec.js +8 -1
- package/dist/composer/PromptComposer.spec.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +56 -11
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.spec.js +59 -7
- package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +32 -0
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.spec.js +25 -2
- package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
- package/dist/conformance/conformance.spec.js +30 -1
- package/dist/conformance/conformance.spec.js.map +1 -1
- package/dist/conformance/runner.d.ts.map +1 -1
- package/dist/conformance/runner.js +75 -1
- package/dist/conformance/runner.js.map +1 -1
- package/dist/editor/SharedRichEditor.d.ts.map +1 -1
- package/dist/editor/SharedRichEditor.js +3 -2
- package/dist/editor/SharedRichEditor.js.map +1 -1
- package/dist/editor/SlashCommandMenu.d.ts +8 -0
- package/dist/editor/SlashCommandMenu.d.ts.map +1 -1
- package/dist/editor/SlashCommandMenu.js +6 -0
- package/dist/editor/SlashCommandMenu.js.map +1 -1
- package/dist/editor/SlashCommandMenu.spec.d.ts +2 -0
- package/dist/editor/SlashCommandMenu.spec.d.ts.map +1 -0
- package/dist/editor/SlashCommandMenu.spec.js +20 -0
- package/dist/editor/SlashCommandMenu.spec.js.map +1 -0
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/styles.css +52 -23
- package/package.json +1 -1
- package/src/composer/PromptComposer.spec.ts +12 -1
- package/src/composer/PromptComposer.tsx +5 -1
- package/src/composer/RealtimeVoiceMode.spec.tsx +117 -9
- package/src/composer/RealtimeVoiceMode.tsx +119 -31
- package/src/composer/useRealtimeVoiceMode.spec.ts +28 -2
- package/src/composer/useRealtimeVoiceMode.tsx +30 -0
- package/src/conformance/conformance.spec.tsx +61 -1
- package/src/conformance/runner.tsx +153 -1
- package/src/editor/SharedRichEditor.tsx +13 -2
- package/src/editor/SlashCommandMenu.spec.ts +33 -0
- package/src/editor/SlashCommandMenu.tsx +20 -0
- package/src/editor/index.ts +2 -0
- package/src/styles.css +52 -23
|
@@ -122,6 +122,87 @@ function elementWithText(document: Document, selector: string, text: string) {
|
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
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
|
+
|
|
125
206
|
const checks: readonly ConformanceCheck[] = [
|
|
126
207
|
{
|
|
127
208
|
id: "contract.components",
|
|
@@ -308,6 +389,21 @@ const checks: readonly ConformanceCheck[] = [
|
|
|
308
389
|
"Tooltip must honor controlled open and portalContainer.",
|
|
309
390
|
);
|
|
310
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);
|
|
311
407
|
portal.remove();
|
|
312
408
|
},
|
|
313
409
|
},
|
|
@@ -331,6 +427,21 @@ const checks: readonly ConformanceCheck[] = [
|
|
|
331
427
|
click(item, document);
|
|
332
428
|
invariant(selected === "archive", "Menu must report selected item ids.");
|
|
333
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);
|
|
334
445
|
},
|
|
335
446
|
},
|
|
336
447
|
{
|
|
@@ -393,6 +504,30 @@ const checks: readonly ConformanceCheck[] = [
|
|
|
393
504
|
"Dialog must not invent actions when footer is omitted.",
|
|
394
505
|
);
|
|
395
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);
|
|
396
531
|
portal.remove();
|
|
397
532
|
},
|
|
398
533
|
},
|
|
@@ -466,7 +601,16 @@ const checks: readonly ConformanceCheck[] = [
|
|
|
466
601
|
orientation="vertical"
|
|
467
602
|
onChange={(next) => (value = next)}
|
|
468
603
|
items={[
|
|
469
|
-
{
|
|
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
|
+
},
|
|
470
614
|
{ value: "second", label: "Second", content: "Two" },
|
|
471
615
|
]}
|
|
472
616
|
/>,
|
|
@@ -477,6 +621,14 @@ const checks: readonly ConformanceCheck[] = [
|
|
|
477
621
|
tabList.getAttribute("aria-orientation") === "vertical",
|
|
478
622
|
"Tabs must honor vertical orientation in their tablist semantics.",
|
|
479
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
|
+
);
|
|
480
632
|
const second = elementWithText(document, '[role="tab"]', "Second");
|
|
481
633
|
invariant(second, "Tabs must expose tab semantics.");
|
|
482
634
|
click(second, document);
|
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
type SharedEditorFeatures,
|
|
15
15
|
} from "./extensions.js";
|
|
16
16
|
import type { ImageUploadFn } from "./ImageExtension.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_SLASH_COMMANDS,
|
|
19
|
+
filterSlashCommandItems,
|
|
20
|
+
SlashCommandMenu,
|
|
21
|
+
type SlashCommandItem,
|
|
22
|
+
} from "./SlashCommandMenu.js";
|
|
18
23
|
import {
|
|
19
24
|
useCollabReconcile,
|
|
20
25
|
getEditorMarkdown,
|
|
@@ -200,6 +205,12 @@ export function SharedRichEditor({
|
|
|
200
205
|
|
|
201
206
|
const collab = !!ydoc;
|
|
202
207
|
|
|
208
|
+
const effectiveSlashItems = useMemo(
|
|
209
|
+
() =>
|
|
210
|
+
filterSlashCommandItems(slashItems ?? DEFAULT_SLASH_COMMANDS, features),
|
|
211
|
+
[features, slashItems],
|
|
212
|
+
);
|
|
213
|
+
|
|
203
214
|
// The collab hook needs the editor, but useEditor's `onUpdate` needs the
|
|
204
215
|
// hook's guards. Break the cycle with a ref: `onUpdate` reads the guards
|
|
205
216
|
// through `guardsRef`, which is populated right after the hook runs below.
|
|
@@ -313,7 +324,7 @@ export function SharedRichEditor({
|
|
|
313
324
|
<BubbleToolbar editor={editor} buildItems={buildBubbleItems} />
|
|
314
325
|
) : null}
|
|
315
326
|
{editable ? (
|
|
316
|
-
<SlashCommandMenu editor={editor} items={
|
|
327
|
+
<SlashCommandMenu editor={editor} items={effectiveSlashItems} />
|
|
317
328
|
) : null}
|
|
318
329
|
<EditorContent editor={editor} />
|
|
319
330
|
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_SLASH_COMMANDS,
|
|
5
|
+
filterSlashCommandItems,
|
|
6
|
+
} from "./SlashCommandMenu.js";
|
|
7
|
+
|
|
8
|
+
describe("filterSlashCommandItems", () => {
|
|
9
|
+
it("hides commands whose editor extensions are disabled", () => {
|
|
10
|
+
const commands = filterSlashCommandItems(DEFAULT_SLASH_COMMANDS, {
|
|
11
|
+
codeBlock: false,
|
|
12
|
+
tables: false,
|
|
13
|
+
tasks: false,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(commands.map((command) => command.title)).not.toEqual(
|
|
17
|
+
expect.arrayContaining(["Code block", "Table", "To-do list"]),
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps commands enabled when their feature is unspecified", () => {
|
|
22
|
+
const commands = filterSlashCommandItems(DEFAULT_SLASH_COMMANDS, {
|
|
23
|
+
tasks: false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(commands.map((command) => command.title)).toEqual(
|
|
27
|
+
expect.arrayContaining(["Code block", "Table"]),
|
|
28
|
+
);
|
|
29
|
+
expect(commands.map((command) => command.title)).not.toContain(
|
|
30
|
+
"To-do list",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -22,9 +22,26 @@ export interface SlashCommandItem {
|
|
|
22
22
|
searchText?: string;
|
|
23
23
|
/** Short text glyph shown in the menu (T, H1, tbl, …). */
|
|
24
24
|
icon: string;
|
|
25
|
+
/** Hide this command when the shared editor feature is disabled. */
|
|
26
|
+
requires?: "tables" | "tasks" | "codeBlock";
|
|
25
27
|
action: (editor: Editor) => void;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
export interface SlashCommandFeatureFlags {
|
|
31
|
+
tables?: boolean;
|
|
32
|
+
tasks?: boolean;
|
|
33
|
+
codeBlock?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function filterSlashCommandItems(
|
|
37
|
+
items: readonly SlashCommandItem[],
|
|
38
|
+
features?: SlashCommandFeatureFlags,
|
|
39
|
+
): SlashCommandItem[] {
|
|
40
|
+
return items.filter(
|
|
41
|
+
(item) => !item.requires || features?.[item.requires] !== false,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
/**
|
|
29
46
|
* The default block commands — Plan's current set. Apps pass their own `items`
|
|
30
47
|
* (typically `[...DEFAULT_SLASH_COMMANDS, ...extra]`) to extend it.
|
|
@@ -73,6 +90,7 @@ export const DEFAULT_SLASH_COMMANDS: SlashCommandItem[] = [
|
|
|
73
90
|
title: "To-do list",
|
|
74
91
|
description: "Checklist items",
|
|
75
92
|
icon: "[]",
|
|
93
|
+
requires: "tasks",
|
|
76
94
|
action: (editor) => editor.chain().focus().toggleTaskList().run(),
|
|
77
95
|
},
|
|
78
96
|
{
|
|
@@ -85,6 +103,7 @@ export const DEFAULT_SLASH_COMMANDS: SlashCommandItem[] = [
|
|
|
85
103
|
title: "Code block",
|
|
86
104
|
description: "Code snippet",
|
|
87
105
|
icon: "<>",
|
|
106
|
+
requires: "codeBlock",
|
|
88
107
|
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
|
|
89
108
|
},
|
|
90
109
|
{
|
|
@@ -97,6 +116,7 @@ export const DEFAULT_SLASH_COMMANDS: SlashCommandItem[] = [
|
|
|
97
116
|
title: "Table",
|
|
98
117
|
description: "Three by three table",
|
|
99
118
|
icon: "tbl",
|
|
119
|
+
requires: "tables",
|
|
100
120
|
action: (editor) =>
|
|
101
121
|
editor
|
|
102
122
|
.chain()
|
package/src/editor/index.ts
CHANGED
|
@@ -25,8 +25,10 @@ export {
|
|
|
25
25
|
SlashCommandMenu,
|
|
26
26
|
DEFAULT_SLASH_COMMANDS,
|
|
27
27
|
createImageSlashCommand,
|
|
28
|
+
filterSlashCommandItems,
|
|
28
29
|
type SlashCommandItem,
|
|
29
30
|
type SlashCommandMenuProps,
|
|
31
|
+
type SlashCommandFeatureFlags,
|
|
30
32
|
} from "./SlashCommandMenu.js";
|
|
31
33
|
export {
|
|
32
34
|
SharedImage,
|
package/src/styles.css
CHANGED
|
@@ -20,37 +20,66 @@
|
|
|
20
20
|
*/
|
|
21
21
|
@source "./**/*.{js,ts,tsx}";
|
|
22
22
|
|
|
23
|
-
.agent-realtime-voice-
|
|
23
|
+
.agent-realtime-voice-glow {
|
|
24
|
+
--agent-realtime-voice-edge-opacity: 0.52;
|
|
25
|
+
--agent-realtime-voice-halo-opacity: 0.28;
|
|
24
26
|
position: absolute;
|
|
25
27
|
z-index: 0;
|
|
28
|
+
inset: -4px;
|
|
29
|
+
border-radius: inherit;
|
|
30
|
+
pointer-events: none;
|
|
31
|
+
transform: rotate(0turn);
|
|
32
|
+
will-change: transform;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.agent-realtime-voice-working {
|
|
36
|
+
--agent-realtime-voice-edge-opacity: 0.92;
|
|
37
|
+
--agent-realtime-voice-halo-opacity: 0.58;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.agent-realtime-voice-glow::before,
|
|
41
|
+
.agent-realtime-voice-glow::after {
|
|
42
|
+
position: absolute;
|
|
26
43
|
inset: 0;
|
|
27
44
|
border-radius: inherit;
|
|
28
45
|
pointer-events: none;
|
|
29
46
|
content: "";
|
|
47
|
+
background: conic-gradient(
|
|
48
|
+
from -18deg,
|
|
49
|
+
transparent 0deg 228deg,
|
|
50
|
+
oklch(0.65 0.2 294 / 0.18) 244deg,
|
|
51
|
+
oklch(0.7 0.2 265 / 0.7) 274deg,
|
|
52
|
+
oklch(0.79 0.16 235 / 0.96) 306deg,
|
|
53
|
+
oklch(0.96 0.035 210) 326deg,
|
|
54
|
+
oklch(0.88 0.11 202 / 0.72) 340deg,
|
|
55
|
+
transparent 360deg
|
|
56
|
+
);
|
|
57
|
+
opacity: var(--agent-realtime-voice-edge-opacity);
|
|
58
|
+
padding: 1.5px;
|
|
59
|
+
-webkit-mask:
|
|
60
|
+
linear-gradient(#000 0 0) content-box,
|
|
61
|
+
linear-gradient(#000 0 0);
|
|
62
|
+
mask:
|
|
63
|
+
linear-gradient(#000 0 0) content-box,
|
|
64
|
+
linear-gradient(#000 0 0);
|
|
65
|
+
-webkit-mask-composite: xor;
|
|
66
|
+
mask-composite: exclude;
|
|
67
|
+
transition: opacity 220ms ease-out;
|
|
30
68
|
}
|
|
31
69
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
background-repeat: repeat;
|
|
43
|
-
background-size: 14rem 100%;
|
|
44
|
-
animation: agent-realtime-voice-shine 2.6s linear infinite;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@keyframes agent-realtime-voice-shine {
|
|
48
|
-
from {
|
|
49
|
-
background-position: 0 0;
|
|
50
|
-
}
|
|
70
|
+
.agent-realtime-voice-glow::before {
|
|
71
|
+
inset: -1px;
|
|
72
|
+
opacity: var(--agent-realtime-voice-halo-opacity);
|
|
73
|
+
padding: 2px;
|
|
74
|
+
filter: blur(1px) drop-shadow(0 0 2px oklch(0.9 0.07 215 / 0.95))
|
|
75
|
+
drop-shadow(0 0 6px oklch(0.78 0.15 236 / 0.82))
|
|
76
|
+
drop-shadow(0 0 14px oklch(0.67 0.2 258 / 0.48))
|
|
77
|
+
drop-shadow(0 0 28px oklch(0.62 0.18 276 / 0.26));
|
|
78
|
+
mix-blend-mode: plus-lighter;
|
|
79
|
+
}
|
|
51
80
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
@media (prefers-reduced-motion: reduce) {
|
|
82
|
+
.agent-realtime-voice-glow {
|
|
83
|
+
transform: rotate(35deg);
|
|
55
84
|
}
|
|
56
85
|
}
|