@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.
Files changed (51) hide show
  1. package/dist/composer/PromptComposer.d.ts +1 -0
  2. package/dist/composer/PromptComposer.d.ts.map +1 -1
  3. package/dist/composer/PromptComposer.js +4 -1
  4. package/dist/composer/PromptComposer.js.map +1 -1
  5. package/dist/composer/PromptComposer.spec.js +8 -1
  6. package/dist/composer/PromptComposer.spec.js.map +1 -1
  7. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
  8. package/dist/composer/RealtimeVoiceMode.js +56 -11
  9. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  10. package/dist/composer/RealtimeVoiceMode.spec.js +59 -7
  11. package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
  12. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  13. package/dist/composer/useRealtimeVoiceMode.js +32 -0
  14. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  15. package/dist/composer/useRealtimeVoiceMode.spec.js +25 -2
  16. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
  17. package/dist/conformance/conformance.spec.js +30 -1
  18. package/dist/conformance/conformance.spec.js.map +1 -1
  19. package/dist/conformance/runner.d.ts.map +1 -1
  20. package/dist/conformance/runner.js +75 -1
  21. package/dist/conformance/runner.js.map +1 -1
  22. package/dist/editor/SharedRichEditor.d.ts.map +1 -1
  23. package/dist/editor/SharedRichEditor.js +3 -2
  24. package/dist/editor/SharedRichEditor.js.map +1 -1
  25. package/dist/editor/SlashCommandMenu.d.ts +8 -0
  26. package/dist/editor/SlashCommandMenu.d.ts.map +1 -1
  27. package/dist/editor/SlashCommandMenu.js +6 -0
  28. package/dist/editor/SlashCommandMenu.js.map +1 -1
  29. package/dist/editor/SlashCommandMenu.spec.d.ts +2 -0
  30. package/dist/editor/SlashCommandMenu.spec.d.ts.map +1 -0
  31. package/dist/editor/SlashCommandMenu.spec.js +20 -0
  32. package/dist/editor/SlashCommandMenu.spec.js.map +1 -0
  33. package/dist/editor/index.d.ts +1 -1
  34. package/dist/editor/index.d.ts.map +1 -1
  35. package/dist/editor/index.js +1 -1
  36. package/dist/editor/index.js.map +1 -1
  37. package/dist/styles.css +52 -23
  38. package/package.json +1 -1
  39. package/src/composer/PromptComposer.spec.ts +12 -1
  40. package/src/composer/PromptComposer.tsx +5 -1
  41. package/src/composer/RealtimeVoiceMode.spec.tsx +117 -9
  42. package/src/composer/RealtimeVoiceMode.tsx +119 -31
  43. package/src/composer/useRealtimeVoiceMode.spec.ts +28 -2
  44. package/src/composer/useRealtimeVoiceMode.tsx +30 -0
  45. package/src/conformance/conformance.spec.tsx +61 -1
  46. package/src/conformance/runner.tsx +153 -1
  47. package/src/editor/SharedRichEditor.tsx +13 -2
  48. package/src/editor/SlashCommandMenu.spec.ts +33 -0
  49. package/src/editor/SlashCommandMenu.tsx +20 -0
  50. package/src/editor/index.ts +2 -0
  51. 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
- { value: "first", label: "First", content: "One" },
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 { SlashCommandMenu, type SlashCommandItem } from "./SlashCommandMenu.js";
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={slashItems} />
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()
@@ -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-working::after {
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
- @media (prefers-reduced-motion: no-preference) {
33
- .agent-realtime-voice-working::after {
34
- background: linear-gradient(
35
- 90deg,
36
- transparent 0%,
37
- transparent 36%,
38
- hsl(var(--foreground) / 0.14) 50%,
39
- transparent 64%,
40
- transparent 100%
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
- to {
53
- background-position: 14rem 0;
54
- }
81
+ @media (prefers-reduced-motion: reduce) {
82
+ .agent-realtime-voice-glow {
83
+ transform: rotate(35deg);
55
84
  }
56
85
  }