@adia-ai/web-components 0.0.29 → 0.0.34

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 (295) hide show
  1. package/components/accordion/accordion.a2ui.json +1 -1
  2. package/components/accordion/accordion.js +6 -6
  3. package/components/accordion/accordion.yaml +1 -1
  4. package/components/action-list/action-list.a2ui.json +1 -1
  5. package/components/action-list/action-list.js +6 -6
  6. package/components/action-list/action-list.yaml +1 -1
  7. package/components/agent-artifact/agent-artifact.a2ui.json +1 -1
  8. package/components/agent-artifact/agent-artifact.js +4 -4
  9. package/components/agent-artifact/agent-artifact.yaml +1 -1
  10. package/components/agent-feedback-bar/agent-feedback-bar.a2ui.json +1 -1
  11. package/components/agent-feedback-bar/agent-feedback-bar.js +4 -4
  12. package/components/agent-feedback-bar/agent-feedback-bar.yaml +1 -1
  13. package/components/agent-questions/agent-questions.a2ui.json +1 -1
  14. package/components/agent-questions/agent-questions.js +4 -4
  15. package/components/agent-questions/agent-questions.yaml +1 -1
  16. package/components/agent-reasoning/agent-reasoning.a2ui.json +3 -3
  17. package/components/agent-reasoning/agent-reasoning.js +4 -4
  18. package/components/agent-reasoning/agent-reasoning.yaml +3 -3
  19. package/components/agent-suggestions/agent-suggestions.a2ui.json +1 -1
  20. package/components/agent-suggestions/agent-suggestions.js +4 -4
  21. package/components/agent-suggestions/agent-suggestions.yaml +1 -1
  22. package/components/agent-trace/agent-trace.a2ui.json +1 -1
  23. package/components/agent-trace/agent-trace.js +4 -4
  24. package/components/agent-trace/agent-trace.yaml +1 -1
  25. package/components/alert/alert.a2ui.json +1 -1
  26. package/components/alert/alert.js +4 -4
  27. package/components/alert/alert.yaml +1 -1
  28. package/components/aside/aside.a2ui.json +1 -1
  29. package/components/aside/aside.yaml +1 -1
  30. package/components/avatar/avatar.a2ui.json +1 -1
  31. package/components/avatar/avatar.js +8 -8
  32. package/components/avatar/avatar.yaml +1 -1
  33. package/components/badge/badge.a2ui.json +1 -1
  34. package/components/badge/badge.js +4 -4
  35. package/components/badge/badge.yaml +1 -1
  36. package/components/block/block.a2ui.json +1 -1
  37. package/components/block/block.js +4 -4
  38. package/components/block/block.yaml +1 -1
  39. package/components/breadcrumb/breadcrumb.a2ui.json +1 -1
  40. package/components/breadcrumb/breadcrumb.js +4 -4
  41. package/components/breadcrumb/breadcrumb.yaml +1 -1
  42. package/components/button/button.a2ui.json +1 -1
  43. package/components/button/button.js +4 -4
  44. package/components/button/button.yaml +1 -1
  45. package/components/calendar-picker/calendar-picker.a2ui.json +1 -1
  46. package/components/calendar-picker/calendar-picker.js +6 -6
  47. package/components/calendar-picker/calendar-picker.yaml +1 -1
  48. package/components/canvas/canvas.a2ui.json +1 -1
  49. package/components/canvas/canvas.js +4 -4
  50. package/components/canvas/canvas.yaml +1 -1
  51. package/components/card/card.a2ui.json +1 -1
  52. package/components/card/card.js +4 -4
  53. package/components/card/card.yaml +1 -1
  54. package/components/chart/chart.a2ui.json +1 -1
  55. package/components/chart/chart.js +5 -5
  56. package/components/chart/chart.yaml +1 -1
  57. package/components/chart-legend/chart-legend.a2ui.json +1 -1
  58. package/components/chart-legend/chart-legend.js +7 -7
  59. package/components/chart-legend/chart-legend.yaml +1 -1
  60. package/components/{chat → chat-thread}/chat-input.js +5 -5
  61. package/components/{chat/chat.a2ui.json → chat-thread/chat-thread.a2ui.json} +6 -6
  62. package/components/{chat/chat.css → chat-thread/chat-thread.css} +2 -2
  63. package/components/{chat/chat.js → chat-thread/chat-thread.js} +7 -7
  64. package/components/{chat/chat.yaml → chat-thread/chat-thread.yaml} +4 -4
  65. package/components/check/check.a2ui.json +1 -1
  66. package/components/check/check.js +5 -5
  67. package/components/check/check.yaml +1 -1
  68. package/components/code/code.a2ui.json +1 -1
  69. package/components/code/code.js +4 -4
  70. package/components/code/code.yaml +1 -1
  71. package/components/col/col.a2ui.json +1 -1
  72. package/components/col/col.js +4 -4
  73. package/components/col/col.yaml +1 -1
  74. package/components/color-picker/color-picker.a2ui.json +1 -1
  75. package/components/color-picker/color-picker.js +6 -6
  76. package/components/color-picker/color-picker.yaml +1 -1
  77. package/components/command/command.a2ui.json +1 -1
  78. package/components/command/command.js +5 -5
  79. package/components/command/command.yaml +1 -1
  80. package/components/description-list/description-list.a2ui.json +1 -1
  81. package/components/description-list/description-list.js +4 -4
  82. package/components/description-list/description-list.yaml +1 -1
  83. package/components/divider/divider.a2ui.json +1 -1
  84. package/components/divider/divider.js +4 -4
  85. package/components/divider/divider.yaml +1 -1
  86. package/components/drawer/drawer.a2ui.json +1 -1
  87. package/components/drawer/drawer.js +4 -4
  88. package/components/drawer/drawer.yaml +1 -1
  89. package/components/embed/embed.a2ui.json +1 -1
  90. package/components/embed/embed.js +4 -4
  91. package/components/embed/embed.yaml +1 -1
  92. package/components/empty-state/empty-state.a2ui.json +1 -1
  93. package/components/empty-state/empty-state.js +4 -4
  94. package/components/empty-state/empty-state.yaml +1 -1
  95. package/components/feed/feed-item.yaml +2 -2
  96. package/components/feed/feed.a2ui.json +2 -2
  97. package/components/feed/feed.css +12 -3
  98. package/components/feed/feed.js +22 -22
  99. package/components/feed/feed.yaml +2 -2
  100. package/components/field/field.a2ui.json +1 -1
  101. package/components/field/field.js +10 -10
  102. package/components/field/field.yaml +2 -2
  103. package/components/footer/footer.a2ui.json +1 -1
  104. package/components/footer/footer.yaml +1 -1
  105. package/components/grid/grid.a2ui.json +1 -1
  106. package/components/grid/grid.js +4 -4
  107. package/components/grid/grid.yaml +1 -1
  108. package/components/header/header.a2ui.json +1 -1
  109. package/components/header/header.yaml +1 -1
  110. package/components/heatmap/heatmap.a2ui.json +1 -1
  111. package/components/heatmap/heatmap.js +4 -4
  112. package/components/heatmap/heatmap.yaml +1 -1
  113. package/components/icon/icon.a2ui.json +1 -1
  114. package/components/icon/icon.js +4 -4
  115. package/components/icon/icon.yaml +1 -1
  116. package/components/image/image.a2ui.json +1 -1
  117. package/components/image/image.js +4 -4
  118. package/components/image/image.yaml +1 -1
  119. package/components/index.js +89 -85
  120. package/components/input/input.a2ui.json +1 -1
  121. package/components/input/input.js +7 -7
  122. package/components/input/input.yaml +1 -1
  123. package/components/inspector/inspector.a2ui.json +1 -1
  124. package/components/inspector/inspector.js +4 -4
  125. package/components/inspector/inspector.yaml +1 -1
  126. package/components/kbd/kbd.a2ui.json +1 -1
  127. package/components/kbd/kbd.js +4 -4
  128. package/components/kbd/kbd.yaml +1 -1
  129. package/components/list/list.a2ui.json +1 -1
  130. package/components/list/list.js +6 -6
  131. package/components/list/list.yaml +1 -1
  132. package/components/menu/menu.a2ui.json +1 -1
  133. package/components/menu/menu.js +8 -8
  134. package/components/menu/menu.yaml +1 -1
  135. package/components/modal/modal.a2ui.json +1 -1
  136. package/components/modal/modal.js +4 -4
  137. package/components/modal/modal.yaml +1 -1
  138. package/components/nav/nav.a2ui.json +98 -0
  139. package/components/nav/nav.css +133 -0
  140. package/components/nav/nav.js +140 -0
  141. package/components/nav/nav.test.js +428 -0
  142. package/components/nav/nav.yaml +114 -0
  143. package/components/nav-group/nav-group.a2ui.json +100 -0
  144. package/components/nav-group/nav-group.css +317 -0
  145. package/components/nav-group/nav-group.js +142 -0
  146. package/components/nav-group/nav-group.yaml +69 -0
  147. package/components/nav-item/nav-item.a2ui.json +106 -0
  148. package/components/nav-item/nav-item.css +194 -0
  149. package/components/nav-item/nav-item.js +76 -0
  150. package/components/nav-item/nav-item.yaml +73 -0
  151. package/components/noodles/noodles.a2ui.json +1 -1
  152. package/components/noodles/noodles.js +4 -4
  153. package/components/noodles/noodles.yaml +1 -1
  154. package/components/option-card/option-card.a2ui.json +1 -1
  155. package/components/option-card/option-card.js +6 -6
  156. package/components/option-card/option-card.yaml +1 -1
  157. package/components/otp-input/otp-input.a2ui.json +1 -1
  158. package/components/otp-input/otp-input.js +5 -5
  159. package/components/otp-input/otp-input.yaml +1 -1
  160. package/components/page/page.a2ui.json +3 -3
  161. package/components/page/page.js +4 -4
  162. package/components/page/page.yaml +3 -3
  163. package/components/pagination/pagination.a2ui.json +1 -1
  164. package/components/pagination/pagination.js +4 -4
  165. package/components/pagination/pagination.yaml +1 -1
  166. package/components/pane/pane.a2ui.json +1 -1
  167. package/components/pane/pane.js +4 -4
  168. package/components/pane/pane.yaml +1 -1
  169. package/components/pipeline-status/pipeline-status.a2ui.json +1 -1
  170. package/components/pipeline-status/pipeline-status.js +4 -4
  171. package/components/pipeline-status/pipeline-status.yaml +1 -1
  172. package/components/popover/popover.a2ui.json +1 -1
  173. package/components/popover/popover.js +4 -4
  174. package/components/popover/popover.yaml +1 -1
  175. package/components/progress/progress.a2ui.json +1 -1
  176. package/components/progress/progress.js +4 -4
  177. package/components/progress/progress.yaml +1 -1
  178. package/components/progress-row/progress-row.a2ui.json +1 -1
  179. package/components/progress-row/progress-row.js +4 -4
  180. package/components/progress-row/progress-row.yaml +1 -1
  181. package/components/radio/radio.a2ui.json +1 -1
  182. package/components/radio/radio.js +5 -5
  183. package/components/radio/radio.yaml +1 -1
  184. package/components/range/range.a2ui.json +1 -1
  185. package/components/range/range.js +7 -7
  186. package/components/range/range.yaml +1 -1
  187. package/components/rating/rating.a2ui.json +1 -1
  188. package/components/rating/rating.js +6 -6
  189. package/components/rating/rating.yaml +1 -1
  190. package/components/richtext/richtext.a2ui.json +1 -1
  191. package/components/richtext/richtext.js +4 -4
  192. package/components/richtext/richtext.yaml +1 -1
  193. package/components/row/row.a2ui.json +1 -1
  194. package/components/row/row.js +4 -4
  195. package/components/row/row.yaml +1 -1
  196. package/components/search/search.a2ui.json +1 -1
  197. package/components/search/search.js +5 -5
  198. package/components/search/search.yaml +1 -1
  199. package/components/section/section.a2ui.json +1 -1
  200. package/components/section/section.yaml +1 -1
  201. package/components/segment/segment.a2ui.json +1 -1
  202. package/components/segment/segment.js +4 -4
  203. package/components/segment/segment.yaml +1 -1
  204. package/components/segmented/segmented.a2ui.json +1 -1
  205. package/components/segmented/segmented.css +6 -0
  206. package/components/segmented/segmented.js +7 -7
  207. package/components/segmented/segmented.yaml +1 -1
  208. package/components/select/select.a2ui.json +1 -1
  209. package/components/select/select.js +5 -5
  210. package/components/select/select.yaml +1 -1
  211. package/components/skeleton/skeleton.a2ui.json +1 -1
  212. package/components/skeleton/skeleton.js +4 -4
  213. package/components/skeleton/skeleton.yaml +1 -1
  214. package/components/slider/slider.a2ui.json +1 -1
  215. package/components/slider/slider.js +7 -7
  216. package/components/slider/slider.yaml +1 -1
  217. package/components/stack/stack.a2ui.json +1 -1
  218. package/components/stack/stack.js +4 -4
  219. package/components/stack/stack.yaml +1 -1
  220. package/components/stat/stat.a2ui.json +1 -1
  221. package/components/stat/stat.js +4 -4
  222. package/components/stat/stat.yaml +1 -1
  223. package/components/step-progress/step-progress.a2ui.json +111 -0
  224. package/components/step-progress/step-progress.css +61 -0
  225. package/components/step-progress/step-progress.js +88 -0
  226. package/components/step-progress/step-progress.test.js +118 -0
  227. package/components/step-progress/step-progress.yaml +93 -0
  228. package/components/stepper/stepper.a2ui.json +1 -1
  229. package/components/stepper/stepper.js +6 -6
  230. package/components/stepper/stepper.yaml +1 -1
  231. package/components/stream/stream.a2ui.json +1 -1
  232. package/components/stream/stream.js +4 -4
  233. package/components/stream/stream.yaml +1 -1
  234. package/components/swatch/swatch.a2ui.json +1 -1
  235. package/components/swatch/swatch.js +4 -4
  236. package/components/swatch/swatch.yaml +1 -1
  237. package/components/swiper/swiper.a2ui.json +1 -1
  238. package/components/swiper/swiper.js +4 -4
  239. package/components/swiper/swiper.yaml +1 -1
  240. package/components/switch/switch.a2ui.json +1 -1
  241. package/components/switch/switch.js +5 -5
  242. package/components/switch/switch.yaml +1 -1
  243. package/components/table/table.a2ui.json +1 -1
  244. package/components/table/table.js +4 -4
  245. package/components/table/table.yaml +1 -1
  246. package/components/table-toolbar/table-toolbar.a2ui.json +1 -1
  247. package/components/table-toolbar/table-toolbar.js +4 -4
  248. package/components/table-toolbar/table-toolbar.yaml +1 -1
  249. package/components/tabs/tab.js +4 -4
  250. package/components/tabs/tabs.a2ui.json +1 -1
  251. package/components/tabs/tabs.js +5 -5
  252. package/components/tabs/tabs.yaml +1 -1
  253. package/components/tag/tag.a2ui.json +1 -1
  254. package/components/tag/tag.js +4 -4
  255. package/components/tag/tag.yaml +1 -1
  256. package/components/text/text.a2ui.json +1 -1
  257. package/components/text/text.js +4 -4
  258. package/components/text/text.yaml +1 -1
  259. package/components/textarea/textarea.a2ui.json +1 -1
  260. package/components/textarea/textarea.js +5 -5
  261. package/components/textarea/textarea.yaml +1 -1
  262. package/components/timeline/timeline.a2ui.json +1 -1
  263. package/components/timeline/timeline.js +6 -6
  264. package/components/timeline/timeline.yaml +1 -1
  265. package/components/toast/toast.a2ui.json +1 -1
  266. package/components/toast/toast.js +18 -18
  267. package/components/toast/toast.yaml +1 -1
  268. package/components/toggle-group/toggle-group.a2ui.json +1 -1
  269. package/components/toggle-group/toggle-group.js +6 -6
  270. package/components/toggle-group/toggle-group.yaml +1 -1
  271. package/components/toolbar/toolbar.a2ui.json +1 -1
  272. package/components/toolbar/toolbar.js +6 -6
  273. package/components/toolbar/toolbar.yaml +1 -1
  274. package/components/tooltip/tooltip.a2ui.json +1 -1
  275. package/components/tooltip/tooltip.js +7 -7
  276. package/components/tooltip/tooltip.yaml +1 -1
  277. package/components/tree/tree.a2ui.json +1 -1
  278. package/components/tree/tree.js +6 -6
  279. package/components/tree/tree.yaml +1 -1
  280. package/components/upload/upload.a2ui.json +1 -1
  281. package/components/upload/upload.js +6 -6
  282. package/components/upload/upload.yaml +1 -1
  283. package/core/element.js +4 -4
  284. package/core/element.test.js +18 -18
  285. package/core/form.js +9 -9
  286. package/core/index.js +2 -2
  287. package/core/provider.js +7 -7
  288. package/core/template.js +1 -1
  289. package/index.css +1 -1
  290. package/index.js +10 -8
  291. package/package.json +1 -1
  292. package/styles/components.css +11 -6
  293. package/styles/resets.css +1 -1
  294. package/traits/define.js +2 -2
  295. /package/components/{chat → chat-thread}/chat-input.css +0 -0
@@ -0,0 +1,428 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import '../../core/element.js';
3
+ import './nav.js';
4
+ import '../nav-group/nav-group.js';
5
+ import '../nav-item/nav-item.js';
6
+
7
+ const tick = () => new Promise((r) => queueMicrotask(r));
8
+
9
+ function mount(html) {
10
+ const wrap = document.createElement('div');
11
+ wrap.innerHTML = html;
12
+ document.body.appendChild(wrap);
13
+ return wrap.firstElementChild;
14
+ }
15
+
16
+ describe('nav-ui', () => {
17
+ beforeEach(() => { document.body.innerHTML = ''; });
18
+
19
+ it('registers nav-ui, nav-group-ui, and nav-item-ui as custom elements', () => {
20
+ expect(customElements.get('nav-ui')).toBeDefined();
21
+ expect(customElements.get('nav-group-ui')).toBeDefined();
22
+ expect(customElements.get('nav-item-ui')).toBeDefined();
23
+ });
24
+
25
+ it('defaults to variant="primary" with role=navigation', () => {
26
+ const nav = mount('<nav-ui></nav-ui>');
27
+ expect(nav.variant).toBe('primary');
28
+ expect(nav.getAttribute('role')).toBe('navigation');
29
+ });
30
+
31
+ it('reflects [variant="section"] from the property', () => {
32
+ const nav = mount('<nav-ui variant="section"></nav-ui>');
33
+ expect(nav.variant).toBe('section');
34
+ expect(nav.getAttribute('variant')).toBe('section');
35
+ });
36
+
37
+ it('uses [heading] as aria-label and removes it when cleared', async () => {
38
+ const nav = mount('<nav-ui heading="On this page"></nav-ui>');
39
+ expect(nav.getAttribute('aria-label')).toBe('On this page');
40
+ nav.heading = '';
41
+ await tick();
42
+ expect(nav.hasAttribute('aria-label')).toBe(false);
43
+ });
44
+
45
+ it('section variant skips the ResizeObserver wiring', () => {
46
+ const primary = mount('<nav-ui></nav-ui>');
47
+ const section = mount('<nav-ui variant="section"></nav-ui>');
48
+ // The internal ResizeObserver is private (`#ro`); verify by the
49
+ // observable surface — toggle() is a no-op on section, but the
50
+ // primary nav can be flipped collapsed/uncollapsed.
51
+ section.toggle();
52
+ expect(section.collapsed).toBe(false);
53
+ primary.toggle();
54
+ expect(primary.collapsed).toBe(true);
55
+ });
56
+
57
+ it('select(item) sets [selected], clears prior selection, and bubbles nav-select', () => {
58
+ const nav = mount(`
59
+ <nav-ui>
60
+ <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
61
+ <nav-item-ui id="b" text="Bravo" value="/b" selected></nav-item-ui>
62
+ </nav-ui>
63
+ `);
64
+ const a = nav.querySelector('#a');
65
+ const b = nav.querySelector('#b');
66
+
67
+ const events = [];
68
+ nav.addEventListener('nav-select', (e) => events.push(e.detail));
69
+
70
+ nav.select(a);
71
+
72
+ expect(a.hasAttribute('selected')).toBe(true);
73
+ expect(b.hasAttribute('selected')).toBe(false);
74
+ expect(nav.selectedItem).toBe(a);
75
+ expect(events).toHaveLength(1);
76
+ expect(events[0]).toMatchObject({ item: a, text: 'Alpha', value: '/a' });
77
+ });
78
+
79
+ it('selectedItem getter returns null when no item is selected', () => {
80
+ const nav = mount(`
81
+ <nav-ui>
82
+ <nav-item-ui text="Alpha" value="/a"></nav-item-ui>
83
+ </nav-ui>
84
+ `);
85
+ expect(nav.selectedItem).toBeNull();
86
+ });
87
+
88
+ it('toggle() is a no-op on variant="section"', () => {
89
+ const nav = mount('<nav-ui variant="section"></nav-ui>');
90
+ nav.toggle();
91
+ expect(nav.collapsed).toBe(false);
92
+ nav.toggle();
93
+ expect(nav.collapsed).toBe(false);
94
+ });
95
+
96
+ it('survives disconnect without throwing', () => {
97
+ const nav = mount(`
98
+ <nav-ui>
99
+ <nav-item-ui text="Alpha"></nav-item-ui>
100
+ </nav-ui>
101
+ `);
102
+ expect(() => nav.remove()).not.toThrow();
103
+ });
104
+ });
105
+
106
+ describe('nav-item-ui', () => {
107
+ beforeEach(() => { document.body.innerHTML = ''; });
108
+
109
+ it('sets role=link and tabindex=0 by default', () => {
110
+ const nav = mount(`
111
+ <nav-ui>
112
+ <nav-item-ui text="Alpha"></nav-item-ui>
113
+ </nav-ui>
114
+ `);
115
+ const item = nav.querySelector('nav-item-ui');
116
+ expect(item.getAttribute('role')).toBe('link');
117
+ expect(item.getAttribute('tabindex')).toBe('0');
118
+ });
119
+
120
+ it('switches tabindex to -1 when [disabled]', async () => {
121
+ const nav = mount(`
122
+ <nav-ui>
123
+ <nav-item-ui text="Alpha"></nav-item-ui>
124
+ </nav-ui>
125
+ `);
126
+ const item = nav.querySelector('nav-item-ui');
127
+ item.disabled = true;
128
+ await tick();
129
+ expect(item.getAttribute('tabindex')).toBe('-1');
130
+ });
131
+
132
+ it('click on the item calls parent nav-ui.select(this)', () => {
133
+ const nav = mount(`
134
+ <nav-ui>
135
+ <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
136
+ </nav-ui>
137
+ `);
138
+ const item = nav.querySelector('#a');
139
+ const spy = vi.spyOn(nav, 'select');
140
+
141
+ item.click();
142
+
143
+ expect(spy).toHaveBeenCalledWith(item);
144
+ });
145
+
146
+ it('Enter key activates a nav-item-ui (same as click)', () => {
147
+ const nav = mount(`
148
+ <nav-ui>
149
+ <nav-item-ui id="a" text="Alpha" value="/a"></nav-item-ui>
150
+ </nav-ui>
151
+ `);
152
+ const item = nav.querySelector('#a');
153
+ const spy = vi.spyOn(nav, 'select');
154
+
155
+ item.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
156
+
157
+ expect(spy).toHaveBeenCalledWith(item);
158
+ });
159
+
160
+ it('Space key activates a nav-item-ui', () => {
161
+ const nav = mount(`
162
+ <nav-ui>
163
+ <nav-item-ui id="a" text="Alpha"></nav-item-ui>
164
+ </nav-ui>
165
+ `);
166
+ const item = nav.querySelector('#a');
167
+ const spy = vi.spyOn(nav, 'select');
168
+
169
+ item.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
170
+
171
+ expect(spy).toHaveBeenCalledWith(item);
172
+ });
173
+
174
+ it('disabled nav-item-ui does not activate on click or keyboard', () => {
175
+ const nav = mount(`
176
+ <nav-ui>
177
+ <nav-item-ui id="a" text="Alpha" disabled></nav-item-ui>
178
+ </nav-ui>
179
+ `);
180
+ const item = nav.querySelector('#a');
181
+ const spy = vi.spyOn(nav, 'select');
182
+
183
+ item.click();
184
+ item.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
185
+
186
+ expect(spy).not.toHaveBeenCalled();
187
+ });
188
+
189
+ it('selected item carries aria-current="page"', async () => {
190
+ const nav = mount(`
191
+ <nav-ui>
192
+ <nav-item-ui id="a" text="Alpha"></nav-item-ui>
193
+ </nav-ui>
194
+ `);
195
+ const item = nav.querySelector('#a');
196
+ nav.select(item);
197
+ await tick();
198
+ expect(item.getAttribute('aria-current')).toBe('page');
199
+ });
200
+
201
+ it('clearing selected removes aria-current', async () => {
202
+ const nav = mount(`
203
+ <nav-ui>
204
+ <nav-item-ui id="a" text="Alpha"></nav-item-ui>
205
+ <nav-item-ui id="b" text="Bravo"></nav-item-ui>
206
+ </nav-ui>
207
+ `);
208
+ const a = nav.querySelector('#a');
209
+ const b = nav.querySelector('#b');
210
+ nav.select(a);
211
+ await tick();
212
+ expect(a.getAttribute('aria-current')).toBe('page');
213
+ nav.select(b);
214
+ await tick();
215
+ expect(a.hasAttribute('aria-current')).toBe(false);
216
+ expect(b.getAttribute('aria-current')).toBe('page');
217
+ });
218
+ });
219
+
220
+ describe('nav-group-ui', () => {
221
+ beforeEach(() => { document.body.innerHTML = ''; });
222
+
223
+ it('auto-mints a header div with icon/text/caret slots', () => {
224
+ const nav = mount(`
225
+ <nav-ui>
226
+ <nav-group-ui text="Settings" icon="gear">
227
+ <nav-item-ui text="General"></nav-item-ui>
228
+ </nav-group-ui>
229
+ </nav-ui>
230
+ `);
231
+ const group = nav.querySelector('nav-group-ui');
232
+ const header = group.querySelector(':scope > [slot="header"]');
233
+ expect(header).not.toBeNull();
234
+ expect(header.querySelector('[slot="text"]').textContent).toBe('Settings');
235
+ expect(header.querySelector('icon-ui[slot="caret"]')).not.toBeNull();
236
+ });
237
+
238
+ it('renders [badge] in the header when set', () => {
239
+ const nav = mount(`
240
+ <nav-ui>
241
+ <nav-group-ui text="Settings" badge="3"></nav-group-ui>
242
+ </nav-ui>
243
+ `);
244
+ const group = nav.querySelector('nav-group-ui');
245
+ const badge = group.querySelector('[slot="header"] [slot="badge"]');
246
+ expect(badge).not.toBeNull();
247
+ expect(badge.textContent).toBe('3');
248
+ });
249
+
250
+ it('header click toggles [open] when not collapsed (delegated through nav-ui)', async () => {
251
+ const nav = mount(`
252
+ <nav-ui>
253
+ <nav-group-ui text="Settings"></nav-group-ui>
254
+ </nav-ui>
255
+ `);
256
+ // happy-dom returns 0 for getBoundingClientRect width, which makes the
257
+ // nav-ui treat itself as collapsed and route clicks to popover instead
258
+ // of inline expand. Stub the rect so the inline-expand path fires.
259
+ nav.getBoundingClientRect = () => ({ width: 240, height: 600, top: 0, left: 0, right: 240, bottom: 600, x: 0, y: 0 });
260
+
261
+ const group = nav.querySelector('nav-group-ui');
262
+ const header = group.querySelector(':scope > [slot="header"]');
263
+ expect(group.open).toBe(false);
264
+
265
+ header.click();
266
+ await tick();
267
+
268
+ expect(group.open).toBe(true);
269
+ });
270
+
271
+ it('keydown Enter on header is ignored when collapsible=false', () => {
272
+ const nav = mount(`
273
+ <nav-ui>
274
+ <nav-group-ui text="Settings" open></nav-group-ui>
275
+ </nav-ui>
276
+ `);
277
+ const group = nav.querySelector('nav-group-ui');
278
+ group.collapsible = false;
279
+
280
+ const header = group.querySelector(':scope > [slot="header"]');
281
+ const events = [];
282
+ group.addEventListener('group-toggle', (e) => events.push(e.detail));
283
+
284
+ header.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
285
+
286
+ expect(events).toHaveLength(0);
287
+ expect(group.open).toBe(true);
288
+ });
289
+
290
+ it('aria-expanded reflects the open state', async () => {
291
+ const nav = mount(`
292
+ <nav-ui>
293
+ <nav-group-ui text="Settings"></nav-group-ui>
294
+ </nav-ui>
295
+ `);
296
+ const group = nav.querySelector('nav-group-ui');
297
+ expect(group.getAttribute('aria-expanded')).toBe('false');
298
+ group.open = true;
299
+ await tick();
300
+ expect(group.getAttribute('aria-expanded')).toBe('true');
301
+ });
302
+
303
+ it('Enter on the header dispatches group-toggle when collapsible', () => {
304
+ const nav = mount(`
305
+ <nav-ui>
306
+ <nav-group-ui text="Settings"></nav-group-ui>
307
+ </nav-ui>
308
+ `);
309
+ const group = nav.querySelector('nav-group-ui');
310
+ const header = group.querySelector(':scope > [slot="header"]');
311
+
312
+ const events = [];
313
+ group.addEventListener('group-toggle', (e) => events.push(e.detail));
314
+
315
+ header.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
316
+
317
+ expect(events).toHaveLength(1);
318
+ expect(events[0]).toMatchObject({ text: 'Settings', open: true });
319
+ });
320
+
321
+ it('respects an existing [slot="header"] instead of auto-minting one', () => {
322
+ const nav = mount(`
323
+ <nav-ui>
324
+ <nav-group-ui text="Settings">
325
+ <div slot="header" data-custom>my header</div>
326
+ </nav-group-ui>
327
+ </nav-ui>
328
+ `);
329
+ const group = nav.querySelector('nav-group-ui');
330
+ const headers = group.querySelectorAll(':scope > [slot="header"]');
331
+ expect(headers.length).toBe(1);
332
+ expect(headers[0].dataset.custom).toBe('');
333
+ });
334
+
335
+ it('survives disconnect without throwing', () => {
336
+ const nav = mount(`
337
+ <nav-ui>
338
+ <nav-group-ui text="Settings">
339
+ <nav-item-ui text="General"></nav-item-ui>
340
+ </nav-group-ui>
341
+ </nav-ui>
342
+ `);
343
+ expect(() => nav.remove()).not.toThrow();
344
+ });
345
+
346
+ it('section-variant subnav stamps groups + items per the docs site pattern', () => {
347
+ // Mirrors site/site.js subnav stamping for `subnav: true` sections:
348
+ // dividers in the sitemap become nav-group-ui kickers; following items
349
+ // become nav-item-ui children of the most recent group.
350
+ const nav = mount(`
351
+ <nav-ui variant="section" aria-label="Authentication">
352
+ <nav-group-ui text="Sign in">
353
+ <nav-item-ui text="Sign In" value="/auth/sign-in"></nav-item-ui>
354
+ <nav-item-ui text="Password" value="/auth/sign-in/password"></nav-item-ui>
355
+ </nav-group-ui>
356
+ <nav-group-ui text="Sign up">
357
+ <nav-item-ui text="Sign Up" value="/auth/sign-up"></nav-item-ui>
358
+ </nav-group-ui>
359
+ </nav-ui>
360
+ `);
361
+ const groups = nav.querySelectorAll(':scope > nav-group-ui');
362
+ expect(groups.length).toBe(2);
363
+ // Each group has its expected children attached (not stripped at mount).
364
+ expect(groups[0].querySelectorAll('nav-item-ui').length).toBe(2);
365
+ expect(groups[1].querySelectorAll('nav-item-ui').length).toBe(1);
366
+ // Group headers are auto-minted with the kicker text.
367
+ expect(groups[0].querySelector('[slot="header"] [slot="text"]').textContent).toBe('Sign in');
368
+ expect(groups[1].querySelector('[slot="header"] [slot="text"]').textContent).toBe('Sign up');
369
+ });
370
+ });
371
+
372
+ describe('section variant — explicit + cascade', () => {
373
+ beforeEach(() => { document.body.innerHTML = ''; });
374
+
375
+ it('nav-group-ui[variant="section"] reflects the property', () => {
376
+ const group = mount('<nav-group-ui variant="section" text="Settings"></nav-group-ui>');
377
+ expect(group.variant).toBe('section');
378
+ expect(group.getAttribute('variant')).toBe('section');
379
+ });
380
+
381
+ it('nav-item-ui[variant="section"] reflects the property', () => {
382
+ const item = mount('<nav-item-ui variant="section" text="General"></nav-item-ui>');
383
+ expect(item.variant).toBe('section');
384
+ expect(item.getAttribute('variant')).toBe('section');
385
+ });
386
+
387
+ it('nav-group-ui defaults to variant="" (empty) when not set', () => {
388
+ const group = mount('<nav-group-ui text="Settings"></nav-group-ui>');
389
+ expect(group.variant).toBe('');
390
+ });
391
+
392
+ it('nav-item-ui defaults to variant="" (empty) when not set', () => {
393
+ const item = mount('<nav-item-ui text="General"></nav-item-ui>');
394
+ expect(item.variant).toBe('');
395
+ });
396
+
397
+ it('cascade: nav-ui[variant="section"] does NOT mutate the child variant prop', () => {
398
+ // The cascade is CSS-only — children keep their own (empty) variant.
399
+ // CSS rules target `nav-ui[variant="section"] > x:not([variant])` so
400
+ // the parent's variant only governs styling, not the JS state.
401
+ const nav = mount(`
402
+ <nav-ui variant="section">
403
+ <nav-group-ui text="Settings">
404
+ <nav-item-ui text="General"></nav-item-ui>
405
+ </nav-group-ui>
406
+ </nav-ui>
407
+ `);
408
+ const group = nav.querySelector('nav-group-ui');
409
+ const item = nav.querySelector('nav-item-ui');
410
+ expect(group.variant).toBe('');
411
+ expect(item.variant).toBe('');
412
+ });
413
+
414
+ it('explicit variant on a child wins over the parent cascade', () => {
415
+ // A child can opt out of the section cascade by setting its own
416
+ // variant. The CSS rules use `:not([variant])` for the cascade path,
417
+ // so an explicit non-section variant on the child suppresses kicker
418
+ // styling and the child renders primary-style.
419
+ const nav = mount(`
420
+ <nav-ui variant="section">
421
+ <nav-group-ui text="Override" variant="primary"></nav-group-ui>
422
+ </nav-ui>
423
+ `);
424
+ const group = nav.querySelector('nav-group-ui');
425
+ expect(group.variant).toBe('primary');
426
+ expect(group.matches('[variant]')).toBe(true);
427
+ });
428
+ });
@@ -0,0 +1,114 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UINav
3
+ tag: nav-ui
4
+ component: Nav
5
+ category: layout
6
+ version: 1
7
+ description: |
8
+ Navigation rail. Consolidates the prior `app-nav-ui` + `section-nav-ui`
9
+ pair per ADR-0015 § Nav consolidation. [variant] drives visual
10
+ treatment; behavior is unified.
11
+
12
+ Default variant ("primary") is the app-sidebar nav: ResizeObserver
13
+ collapses to icon-only below 96px, groups open a popover when
14
+ collapsed. [variant="section"] is a subnav rail with quieter chrome
15
+ and a [heading] kicker rendered via CSS.
16
+
17
+ props:
18
+ variant:
19
+ type: string
20
+ default: primary
21
+ enum: [primary, section]
22
+ description: "Visual treatment. primary = app sidebar; section = subnav rail."
23
+ collapsed:
24
+ type: boolean
25
+ default: false
26
+ description: "Primary-variant only. Force icon-only collapse regardless of viewport width."
27
+ divider:
28
+ type: boolean
29
+ default: false
30
+ description: "Auto-place dividers between adjacent groups + items."
31
+ heading:
32
+ type: string
33
+ default: ''
34
+ description: "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only."
35
+
36
+ events:
37
+ nav-select:
38
+ description: "Bubbles from <nav-item-ui> children when one is selected. Detail: { item, text, value }."
39
+
40
+ slots:
41
+ default:
42
+ description: "Primary slot — accepts <nav-group-ui> + <nav-item-ui> children, plus <hr data-nav-divider> for hand-placed dividers."
43
+
44
+ states:
45
+ - name: idle
46
+ description: Default, not collapsed.
47
+ - name: collapsed
48
+ description: Primary variant when [collapsed] or container width <= 96px.
49
+
50
+ traits: []
51
+ tokens: {}
52
+ a2ui:
53
+ rules: []
54
+ anti_patterns: []
55
+
56
+ examples:
57
+ - name: primary
58
+ description: App sidebar nav with groups + items.
59
+ a2ui: >-
60
+ [
61
+ {
62
+ "id": "root",
63
+ "component": "Nav",
64
+ "children": ["g1", "i1"]
65
+ },
66
+ {
67
+ "id": "g1",
68
+ "component": "NavGroup",
69
+ "text": "Settings",
70
+ "icon": "gear",
71
+ "children": ["g1i1"]
72
+ },
73
+ {
74
+ "id": "g1i1",
75
+ "component": "NavItem",
76
+ "text": "General",
77
+ "value": "/settings/general"
78
+ },
79
+ {
80
+ "id": "i1",
81
+ "component": "NavItem",
82
+ "text": "Profile",
83
+ "icon": "user",
84
+ "value": "/profile"
85
+ }
86
+ ]
87
+ - name: section
88
+ description: Subnav rail with heading.
89
+ a2ui: >-
90
+ [
91
+ {
92
+ "id": "root",
93
+ "component": "Nav",
94
+ "variant": "section",
95
+ "heading": "On this page",
96
+ "children": ["i1", "i2"]
97
+ },
98
+ {
99
+ "id": "i1",
100
+ "component": "NavItem",
101
+ "text": "Overview",
102
+ "value": "#overview"
103
+ },
104
+ {
105
+ "id": "i2",
106
+ "component": "NavItem",
107
+ "text": "API",
108
+ "value": "#api"
109
+ }
110
+ ]
111
+
112
+ keywords: [nav, navigation, sidebar, menu, links]
113
+ synonyms: {}
114
+ related: []
@@ -0,0 +1,100 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/NavGroup.json",
4
+ "title": "NavGroup",
5
+ "description": "Collapsible labeled group of <nav-item-ui> children inside <nav-ui>.\nConsolidates the prior `app-nav-group-ui` + `section-nav-group-ui`\nper ADR-0015 § Nav consolidation.\n\nWhen the parent <nav-ui> is collapsed (primary variant), clicking the\ngroup opens a popover with its children instead of toggling inline\nexpansion. Inline click + keyboard (Enter/Space) toggle is supported\nwhen [collapsible] (default true).\n",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "badge": {
17
+ "description": "Optional trailing badge (count, label).",
18
+ "type": "string",
19
+ "default": ""
20
+ },
21
+ "collapsible": {
22
+ "description": "When true, the header row toggles the open state on click/keyboard.",
23
+ "type": "boolean",
24
+ "default": true
25
+ },
26
+ "component": {
27
+ "const": "NavGroup"
28
+ },
29
+ "icon": {
30
+ "description": "Optional leading icon name (resolved via <icon-ui>).",
31
+ "type": "string",
32
+ "default": ""
33
+ },
34
+ "open": {
35
+ "description": "Inline-expanded state. Toggled by header click when [collapsible].",
36
+ "type": "boolean",
37
+ "default": false
38
+ },
39
+ "text": {
40
+ "description": "Visible group label.",
41
+ "type": "string",
42
+ "default": ""
43
+ },
44
+ "variant": {
45
+ "description": "Visual treatment. Default ('') renders as a primary-rail group (icon row, caret, collapsible). 'section' renders the header as a static kicker label with always-visible children — matches the prior <section-nav-group-ui>. When the parent <nav-ui> carries variant=\"section\", this group inherits it via CSS cascade unless an explicit variant is set on the group.",
46
+ "type": "string",
47
+ "enum": [
48
+ "",
49
+ "section"
50
+ ],
51
+ "default": ""
52
+ }
53
+ },
54
+ "required": [
55
+ "component"
56
+ ],
57
+ "unevaluatedProperties": false,
58
+ "x-adiaui": {
59
+ "anti_patterns": [],
60
+ "category": "layout",
61
+ "events": {
62
+ "group-toggle": {
63
+ "description": "Fired when the header toggles via click/keyboard. Detail: { text, open }."
64
+ }
65
+ },
66
+ "examples": [],
67
+ "keywords": [
68
+ "nav",
69
+ "navigation",
70
+ "group",
71
+ "sidebar",
72
+ "menu"
73
+ ],
74
+ "name": "UINavGroup",
75
+ "related": [],
76
+ "slots": {
77
+ "default": {
78
+ "description": "Children — typically <nav-item-ui> rows."
79
+ },
80
+ "header": {
81
+ "description": "Optional custom header. Auto-generated when missing."
82
+ }
83
+ },
84
+ "states": [
85
+ {
86
+ "description": "Default. Children hidden.",
87
+ "name": "closed"
88
+ },
89
+ {
90
+ "description": "Children visible inline.",
91
+ "name": "open"
92
+ }
93
+ ],
94
+ "synonyms": {},
95
+ "tag": "nav-group-ui",
96
+ "tokens": {},
97
+ "traits": [],
98
+ "version": 1
99
+ }
100
+ }