@adia-ai/web-components 0.0.28 → 0.0.33

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 (349) hide show
  1. package/README.md +4 -8
  2. package/a2ui/index.js +1 -1
  3. package/components/accordion/accordion.a2ui.json +1 -1
  4. package/components/accordion/accordion.js +6 -6
  5. package/components/accordion/accordion.yaml +1 -1
  6. package/components/action-list/action-list.a2ui.json +1 -1
  7. package/components/action-list/action-list.js +6 -6
  8. package/components/action-list/action-list.yaml +1 -1
  9. package/components/agent-artifact/agent-artifact.a2ui.json +1 -1
  10. package/components/agent-artifact/agent-artifact.js +4 -4
  11. package/components/agent-artifact/agent-artifact.yaml +1 -1
  12. package/components/agent-feedback-bar/agent-feedback-bar.a2ui.json +1 -1
  13. package/components/agent-feedback-bar/agent-feedback-bar.js +4 -4
  14. package/components/agent-feedback-bar/agent-feedback-bar.yaml +1 -1
  15. package/components/agent-questions/agent-questions.a2ui.json +1 -1
  16. package/components/agent-questions/agent-questions.js +4 -4
  17. package/components/agent-questions/agent-questions.yaml +1 -1
  18. package/components/agent-reasoning/agent-reasoning.a2ui.json +3 -3
  19. package/components/agent-reasoning/agent-reasoning.js +4 -4
  20. package/components/agent-reasoning/agent-reasoning.yaml +3 -3
  21. package/components/agent-suggestions/agent-suggestions.a2ui.json +1 -1
  22. package/components/agent-suggestions/agent-suggestions.js +4 -4
  23. package/components/agent-suggestions/agent-suggestions.yaml +1 -1
  24. package/components/agent-trace/agent-trace.a2ui.json +1 -1
  25. package/components/agent-trace/agent-trace.js +4 -4
  26. package/components/agent-trace/agent-trace.yaml +1 -1
  27. package/components/alert/alert.a2ui.json +1 -1
  28. package/components/alert/alert.js +4 -4
  29. package/components/alert/alert.yaml +1 -1
  30. package/components/aside/aside.a2ui.json +1 -1
  31. package/components/aside/aside.yaml +1 -1
  32. package/components/avatar/avatar.a2ui.json +1 -1
  33. package/components/avatar/avatar.js +8 -8
  34. package/components/avatar/avatar.yaml +1 -1
  35. package/components/badge/badge.a2ui.json +1 -1
  36. package/components/badge/badge.js +4 -4
  37. package/components/badge/badge.yaml +1 -1
  38. package/components/block/block.a2ui.json +1 -1
  39. package/components/block/block.js +4 -4
  40. package/components/block/block.yaml +1 -1
  41. package/components/breadcrumb/breadcrumb.a2ui.json +1 -1
  42. package/components/breadcrumb/breadcrumb.js +4 -4
  43. package/components/breadcrumb/breadcrumb.yaml +1 -1
  44. package/components/button/button.a2ui.json +1 -1
  45. package/components/button/button.js +4 -4
  46. package/components/button/button.yaml +1 -1
  47. package/components/calendar-picker/calendar-picker.a2ui.json +1 -1
  48. package/components/calendar-picker/calendar-picker.js +6 -6
  49. package/components/calendar-picker/calendar-picker.yaml +1 -1
  50. package/components/canvas/canvas.a2ui.json +1 -1
  51. package/components/canvas/canvas.js +5 -5
  52. package/components/canvas/canvas.yaml +1 -1
  53. package/components/card/card.a2ui.json +1 -1
  54. package/components/card/card.js +4 -4
  55. package/components/card/card.yaml +1 -1
  56. package/components/chart/chart.a2ui.json +1 -1
  57. package/components/chart/chart.js +5 -5
  58. package/components/chart/chart.yaml +1 -1
  59. package/components/chart-legend/chart-legend.a2ui.json +1 -1
  60. package/components/chart-legend/chart-legend.js +7 -7
  61. package/components/chart-legend/chart-legend.yaml +1 -1
  62. package/components/{chat → chat-thread}/chat-input.js +5 -5
  63. package/components/{chat/chat.a2ui.json → chat-thread/chat-thread.a2ui.json} +6 -6
  64. package/components/{chat/chat.css → chat-thread/chat-thread.css} +2 -2
  65. package/components/{chat/chat.js → chat-thread/chat-thread.js} +7 -7
  66. package/components/{chat/chat.yaml → chat-thread/chat-thread.yaml} +4 -4
  67. package/components/check/check.a2ui.json +1 -1
  68. package/components/check/check.js +5 -5
  69. package/components/check/check.yaml +1 -1
  70. package/components/code/code.a2ui.json +1 -1
  71. package/components/code/code.js +4 -4
  72. package/components/code/code.yaml +1 -1
  73. package/components/col/col.a2ui.json +1 -1
  74. package/components/col/col.js +4 -4
  75. package/components/col/col.yaml +1 -1
  76. package/components/color-picker/color-picker.a2ui.json +1 -1
  77. package/components/color-picker/color-picker.js +6 -6
  78. package/components/color-picker/color-picker.yaml +1 -1
  79. package/components/command/command.a2ui.json +1 -1
  80. package/components/command/command.js +5 -5
  81. package/components/command/command.yaml +1 -1
  82. package/components/description-list/description-list.a2ui.json +1 -1
  83. package/components/description-list/description-list.js +4 -4
  84. package/components/description-list/description-list.yaml +1 -1
  85. package/components/divider/divider.a2ui.json +1 -1
  86. package/components/divider/divider.js +4 -4
  87. package/components/divider/divider.yaml +1 -1
  88. package/components/drawer/drawer.a2ui.json +1 -1
  89. package/components/drawer/drawer.js +4 -4
  90. package/components/drawer/drawer.yaml +1 -1
  91. package/components/embed/embed.a2ui.json +1 -1
  92. package/components/embed/embed.js +4 -4
  93. package/components/embed/embed.yaml +1 -1
  94. package/components/empty-state/empty-state.a2ui.json +1 -1
  95. package/components/empty-state/empty-state.js +4 -4
  96. package/components/empty-state/empty-state.yaml +1 -1
  97. package/components/feed/feed-item.yaml +2 -2
  98. package/components/feed/feed.a2ui.json +2 -2
  99. package/components/feed/feed.css +21 -3
  100. package/components/feed/feed.js +140 -31
  101. package/components/feed/feed.yaml +2 -2
  102. package/components/field/field.a2ui.json +1 -1
  103. package/components/field/field.js +10 -10
  104. package/components/field/field.yaml +2 -2
  105. package/components/footer/footer.a2ui.json +1 -1
  106. package/components/footer/footer.yaml +1 -1
  107. package/components/grid/grid.a2ui.json +1 -1
  108. package/components/grid/grid.js +4 -4
  109. package/components/grid/grid.yaml +1 -1
  110. package/components/header/header.a2ui.json +1 -1
  111. package/components/header/header.yaml +1 -1
  112. package/components/heatmap/heatmap.a2ui.json +1 -1
  113. package/components/heatmap/heatmap.js +4 -4
  114. package/components/heatmap/heatmap.yaml +1 -1
  115. package/components/icon/icon.a2ui.json +1 -1
  116. package/components/icon/icon.js +4 -4
  117. package/components/icon/icon.yaml +1 -1
  118. package/components/image/image.a2ui.json +1 -1
  119. package/components/image/image.js +4 -4
  120. package/components/image/image.yaml +1 -1
  121. package/components/index.js +88 -85
  122. package/components/input/input.a2ui.json +1 -1
  123. package/components/input/input.js +7 -7
  124. package/components/input/input.yaml +1 -1
  125. package/components/inspector/inspector.a2ui.json +1 -1
  126. package/components/inspector/inspector.js +4 -4
  127. package/components/inspector/inspector.yaml +1 -1
  128. package/components/kbd/kbd.a2ui.json +1 -1
  129. package/components/kbd/kbd.js +4 -4
  130. package/components/kbd/kbd.yaml +1 -1
  131. package/components/list/list.a2ui.json +1 -1
  132. package/components/list/list.js +6 -6
  133. package/components/list/list.yaml +1 -1
  134. package/components/menu/menu.a2ui.json +1 -1
  135. package/components/menu/menu.js +8 -8
  136. package/components/menu/menu.yaml +1 -1
  137. package/components/modal/modal.a2ui.json +1 -1
  138. package/components/modal/modal.js +4 -4
  139. package/components/modal/modal.yaml +1 -1
  140. package/components/nav/nav.a2ui.json +98 -0
  141. package/components/nav/nav.css +133 -0
  142. package/components/nav/nav.js +140 -0
  143. package/components/nav/nav.test.js +428 -0
  144. package/components/nav/nav.yaml +114 -0
  145. package/components/nav-group/nav-group.a2ui.json +100 -0
  146. package/{patterns/app-nav-group/app-nav-group.css → components/nav-group/nav-group.css} +71 -18
  147. package/{patterns/app-nav-group/app-nav-group.js → components/nav-group/nav-group.js} +51 -25
  148. package/components/nav-group/nav-group.yaml +69 -0
  149. package/components/nav-item/nav-item.a2ui.json +106 -0
  150. package/{patterns/app-nav-item/app-nav-item.css → components/nav-item/nav-item.css} +42 -10
  151. package/components/nav-item/nav-item.js +76 -0
  152. package/components/nav-item/nav-item.yaml +73 -0
  153. package/components/noodles/noodles.a2ui.json +1 -1
  154. package/components/noodles/noodles.js +4 -4
  155. package/components/noodles/noodles.yaml +1 -1
  156. package/components/option-card/option-card.a2ui.json +1 -1
  157. package/components/option-card/option-card.js +6 -6
  158. package/components/option-card/option-card.yaml +1 -1
  159. package/components/otp-input/otp-input.a2ui.json +1 -1
  160. package/components/otp-input/otp-input.js +5 -5
  161. package/components/otp-input/otp-input.yaml +1 -1
  162. package/components/page/page.a2ui.json +3 -3
  163. package/components/page/page.js +4 -4
  164. package/components/page/page.yaml +3 -3
  165. package/components/pagination/pagination.a2ui.json +1 -1
  166. package/components/pagination/pagination.js +4 -4
  167. package/components/pagination/pagination.yaml +1 -1
  168. package/components/pane/pane.a2ui.json +1 -1
  169. package/components/pane/pane.js +4 -4
  170. package/components/pane/pane.yaml +1 -1
  171. package/components/pipeline-status/pipeline-status.a2ui.json +1 -1
  172. package/components/pipeline-status/pipeline-status.js +4 -4
  173. package/components/pipeline-status/pipeline-status.yaml +1 -1
  174. package/components/popover/popover.a2ui.json +1 -1
  175. package/components/popover/popover.js +4 -4
  176. package/components/popover/popover.yaml +1 -1
  177. package/components/progress/progress.a2ui.json +1 -1
  178. package/components/progress/progress.js +4 -4
  179. package/components/progress/progress.yaml +1 -1
  180. package/components/progress-row/progress-row.a2ui.json +1 -1
  181. package/components/progress-row/progress-row.js +4 -4
  182. package/components/progress-row/progress-row.yaml +1 -1
  183. package/components/radio/radio.a2ui.json +1 -1
  184. package/components/radio/radio.js +5 -5
  185. package/components/radio/radio.yaml +1 -1
  186. package/components/range/range.a2ui.json +1 -1
  187. package/components/range/range.js +7 -7
  188. package/components/range/range.yaml +1 -1
  189. package/components/rating/rating.a2ui.json +1 -1
  190. package/components/rating/rating.js +6 -6
  191. package/components/rating/rating.yaml +1 -1
  192. package/components/richtext/richtext.a2ui.json +1 -1
  193. package/components/richtext/richtext.js +4 -4
  194. package/components/richtext/richtext.yaml +1 -1
  195. package/components/row/row.a2ui.json +1 -1
  196. package/components/row/row.js +4 -4
  197. package/components/row/row.yaml +1 -1
  198. package/components/search/search.a2ui.json +1 -1
  199. package/components/search/search.js +5 -5
  200. package/components/search/search.yaml +1 -1
  201. package/components/section/section.a2ui.json +1 -1
  202. package/components/section/section.yaml +1 -1
  203. package/components/segment/segment.a2ui.json +1 -1
  204. package/components/segment/segment.js +4 -4
  205. package/components/segment/segment.yaml +1 -1
  206. package/components/segmented/segmented.a2ui.json +1 -1
  207. package/components/segmented/segmented.css +6 -0
  208. package/components/segmented/segmented.js +7 -7
  209. package/components/segmented/segmented.yaml +1 -1
  210. package/components/select/select.a2ui.json +1 -1
  211. package/components/select/select.js +5 -5
  212. package/components/select/select.yaml +1 -1
  213. package/components/skeleton/skeleton.a2ui.json +1 -1
  214. package/components/skeleton/skeleton.js +4 -4
  215. package/components/skeleton/skeleton.yaml +1 -1
  216. package/components/slider/slider.a2ui.json +1 -1
  217. package/components/slider/slider.js +7 -7
  218. package/components/slider/slider.yaml +1 -1
  219. package/components/stack/stack.a2ui.json +1 -1
  220. package/components/stack/stack.js +4 -4
  221. package/components/stack/stack.yaml +1 -1
  222. package/components/stat/stat.a2ui.json +1 -1
  223. package/components/stat/stat.js +4 -4
  224. package/components/stat/stat.yaml +1 -1
  225. package/components/stepper/stepper.a2ui.json +1 -1
  226. package/components/stepper/stepper.js +6 -6
  227. package/components/stepper/stepper.yaml +1 -1
  228. package/components/stream/stream.a2ui.json +1 -1
  229. package/components/stream/stream.js +4 -4
  230. package/components/stream/stream.yaml +1 -1
  231. package/components/swatch/swatch.a2ui.json +1 -1
  232. package/components/swatch/swatch.js +4 -4
  233. package/components/swatch/swatch.yaml +1 -1
  234. package/components/swiper/swiper.a2ui.json +1 -1
  235. package/components/swiper/swiper.js +4 -4
  236. package/components/swiper/swiper.yaml +1 -1
  237. package/components/switch/switch.a2ui.json +1 -1
  238. package/components/switch/switch.js +5 -5
  239. package/components/switch/switch.yaml +1 -1
  240. package/components/table/table.a2ui.json +1 -1
  241. package/components/table/table.js +4 -4
  242. package/components/table/table.yaml +1 -1
  243. package/components/table-toolbar/table-toolbar.a2ui.json +1 -1
  244. package/components/table-toolbar/table-toolbar.js +4 -4
  245. package/components/table-toolbar/table-toolbar.yaml +1 -1
  246. package/components/tabs/tab.js +4 -4
  247. package/components/tabs/tabs.a2ui.json +1 -1
  248. package/components/tabs/tabs.js +5 -5
  249. package/components/tabs/tabs.yaml +1 -1
  250. package/components/tag/tag.a2ui.json +1 -1
  251. package/components/tag/tag.js +4 -4
  252. package/components/tag/tag.yaml +1 -1
  253. package/components/text/text.a2ui.json +1 -1
  254. package/components/text/text.js +4 -4
  255. package/components/text/text.yaml +1 -1
  256. package/components/textarea/textarea.a2ui.json +1 -1
  257. package/components/textarea/textarea.js +5 -5
  258. package/components/textarea/textarea.yaml +1 -1
  259. package/components/timeline/timeline.a2ui.json +1 -1
  260. package/components/timeline/timeline.js +6 -6
  261. package/components/timeline/timeline.yaml +1 -1
  262. package/components/toast/toast.a2ui.json +1 -1
  263. package/components/toast/toast.js +54 -184
  264. package/components/toast/toast.yaml +1 -1
  265. package/components/toggle-group/toggle-group.a2ui.json +1 -1
  266. package/components/toggle-group/toggle-group.js +6 -6
  267. package/components/toggle-group/toggle-group.yaml +1 -1
  268. package/components/toolbar/toolbar.a2ui.json +1 -1
  269. package/components/toolbar/toolbar.js +6 -6
  270. package/components/toolbar/toolbar.yaml +1 -1
  271. package/components/tooltip/tooltip.a2ui.json +1 -1
  272. package/components/tooltip/tooltip.js +7 -7
  273. package/components/tooltip/tooltip.yaml +1 -1
  274. package/components/tree/tree.a2ui.json +1 -1
  275. package/components/tree/tree.js +6 -6
  276. package/components/tree/tree.yaml +1 -1
  277. package/components/upload/upload.a2ui.json +1 -1
  278. package/components/upload/upload.js +6 -6
  279. package/components/upload/upload.yaml +1 -1
  280. package/core/element.js +4 -4
  281. package/core/element.test.js +18 -18
  282. package/core/form.js +9 -9
  283. package/core/index.js +2 -2
  284. package/core/provider.js +7 -7
  285. package/core/template.js +1 -1
  286. package/index.css +3 -2
  287. package/index.js +17 -7
  288. package/package.json +1 -5
  289. package/styles/components.css +10 -6
  290. package/styles/resets.css +1 -1
  291. package/traits/define.js +2 -2
  292. package/patterns/a2ui-root/a2ui-root.a2ui.json +0 -125
  293. package/patterns/a2ui-root/a2ui-root.js +0 -191
  294. package/patterns/a2ui-root/a2ui-root.yaml +0 -87
  295. package/patterns/adia-chat/adia-chat.a2ui.json +0 -149
  296. package/patterns/adia-chat/adia-chat.css +0 -10
  297. package/patterns/adia-chat/adia-chat.js +0 -297
  298. package/patterns/adia-chat/adia-chat.yaml +0 -118
  299. package/patterns/adia-chat/css/adia-chat.empty.css +0 -12
  300. package/patterns/adia-chat/css/adia-chat.layout.css +0 -60
  301. package/patterns/adia-chat/css/adia-chat.markdown.css +0 -74
  302. package/patterns/adia-chat/css/adia-chat.messages.css +0 -87
  303. package/patterns/adia-chat/css/adia-chat.streaming.css +0 -30
  304. package/patterns/adia-chat/css/adia-chat.tokens.css +0 -95
  305. package/patterns/adia-editor/adia-editor.a2ui.json +0 -73
  306. package/patterns/adia-editor/adia-editor.css +0 -6
  307. package/patterns/adia-editor/adia-editor.js +0 -56
  308. package/patterns/adia-editor/adia-editor.yaml +0 -59
  309. package/patterns/adia-editor/css/adia-editor.layout.css +0 -171
  310. package/patterns/adia-editor/css/adia-editor.tokens.css +0 -28
  311. package/patterns/app-nav/app-nav.a2ui.json +0 -89
  312. package/patterns/app-nav/app-nav.css +0 -92
  313. package/patterns/app-nav/app-nav.js +0 -112
  314. package/patterns/app-nav/app-nav.yaml +0 -54
  315. package/patterns/app-nav-group/app-nav-group.a2ui.json +0 -82
  316. package/patterns/app-nav-group/app-nav-group.yaml +0 -59
  317. package/patterns/app-nav-item/app-nav-item.a2ui.json +0 -83
  318. package/patterns/app-nav-item/app-nav-item.js +0 -42
  319. package/patterns/app-nav-item/app-nav-item.yaml +0 -62
  320. package/patterns/app-shell/app-shell.a2ui.json +0 -129
  321. package/patterns/app-shell/app-shell.css +0 -14
  322. package/patterns/app-shell/app-shell.js +0 -251
  323. package/patterns/app-shell/app-shell.yaml +0 -89
  324. package/patterns/app-shell/css/app-shell.collapsed.css +0 -86
  325. package/patterns/app-shell/css/app-shell.helpers.css +0 -42
  326. package/patterns/app-shell/css/app-shell.main.css +0 -172
  327. package/patterns/app-shell/css/app-shell.shell.css +0 -44
  328. package/patterns/app-shell/css/app-shell.sidebar.css +0 -161
  329. package/patterns/app-shell/css/app-shell.templates.css +0 -214
  330. package/patterns/app-shell/css/app-shell.tokens.css +0 -119
  331. package/patterns/gen-ui/gen-ui.a2ui.json +0 -72
  332. package/patterns/gen-ui/gen-ui.css +0 -83
  333. package/patterns/gen-ui/gen-ui.js +0 -136
  334. package/patterns/gen-ui/gen-ui.yaml +0 -43
  335. package/patterns/index.js +0 -11
  336. package/patterns/section-nav/section-nav.a2ui.json +0 -91
  337. package/patterns/section-nav/section-nav.css +0 -60
  338. package/patterns/section-nav/section-nav.js +0 -42
  339. package/patterns/section-nav/section-nav.yaml +0 -58
  340. package/patterns/section-nav-group/section-nav-group.a2ui.json +0 -95
  341. package/patterns/section-nav-group/section-nav-group.css +0 -74
  342. package/patterns/section-nav-group/section-nav-group.js +0 -84
  343. package/patterns/section-nav-group/section-nav-group.yaml +0 -66
  344. package/patterns/section-nav-item/section-nav-item.a2ui.json +0 -97
  345. package/patterns/section-nav-item/section-nav-item.css +0 -106
  346. package/patterns/section-nav-item/section-nav-item.js +0 -66
  347. package/patterns/section-nav-item/section-nav-item.yaml +0 -70
  348. package/styles/layouts/admin.css +0 -7
  349. /package/components/{chat → chat-thread}/chat-input.css +0 -0
@@ -0,0 +1,133 @@
1
+ /* ═══════════════════════════════════════════
2
+ nav-ui — Consolidated navigation rail
3
+ Variants: primary (default, app sidebar) / section (subnav rail)
4
+ ═══════════════════════════════════════════ */
5
+
6
+ @scope (nav-ui) {
7
+ :where(:scope) {
8
+ /* Shared */
9
+ --nav-gap: var(--a-space-1);
10
+ --nav-px: var(--a-space-2);
11
+ --nav-py: var(--a-space-2);
12
+ --nav-font-size: var(--a-ui-size);
13
+ --nav-bg: transparent;
14
+ --nav-divider-bg: var(--a-border-subtle);
15
+ --nav-divider-my: var(--a-space-1);
16
+
17
+ /* Primary-variant collapse */
18
+ --nav-width: auto;
19
+ --nav-width-collapsed: 48px;
20
+ --nav-duration: var(--a-duration-base, 200ms);
21
+ --nav-easing: var(--a-easing, ease);
22
+
23
+ /* Group-heading kicker */
24
+ --nav-label-fg: var(--a-fg-muted);
25
+ --nav-label-font-size: var(--a-kicker-sm);
26
+ --nav-label-weight: var(--a-weight-medium);
27
+ --nav-label-px: var(--a-space-2);
28
+ --nav-label-py: var(--a-space-3) var(--a-space-1);
29
+ }
30
+
31
+ :scope {
32
+ box-sizing: border-box;
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: var(--nav-gap);
36
+ padding: var(--nav-py) var(--nav-px);
37
+ background: var(--nav-bg);
38
+ font-size: var(--nav-font-size);
39
+ min-width: 0;
40
+ overflow-y: auto;
41
+ overflow-x: hidden;
42
+ scrollbar-width: none;
43
+ }
44
+
45
+ :scope::-webkit-scrollbar { display: none; }
46
+
47
+ /* ── Primary variant (default) ── */
48
+
49
+ :scope:not([variant="section"]) {
50
+ width: var(--nav-width);
51
+ transition: width var(--nav-duration) var(--nav-easing);
52
+ }
53
+
54
+ :scope:not([variant="section"])[collapsed] {
55
+ width: var(--nav-width-collapsed);
56
+ --nav-px: var(--a-space-1);
57
+ }
58
+
59
+ /* Container-query collapse — when nav lives in a sidebar that goes
60
+ narrow, behave as if [collapsed] regardless of attribute. */
61
+ @container sidebar (max-width: 96px) {
62
+ :scope:not([variant="section"]) {
63
+ --nav-px: var(--a-space-1);
64
+ }
65
+ }
66
+
67
+ /* Group label kicker (group header text inside nav-group) */
68
+ [data-nav-label] {
69
+ display: block;
70
+ padding: var(--nav-label-py) var(--nav-label-px);
71
+ font-size: var(--nav-label-font-size);
72
+ font-weight: var(--nav-label-weight);
73
+ color: var(--nav-label-fg);
74
+ text-transform: uppercase;
75
+ letter-spacing: 0.06em;
76
+ white-space: nowrap;
77
+ overflow: hidden;
78
+ }
79
+
80
+ /* Hand-placed dividers */
81
+ [data-nav-divider] {
82
+ border: none;
83
+ height: 1px;
84
+ background: var(--nav-divider-bg);
85
+ margin: var(--nav-divider-my) 0;
86
+ }
87
+
88
+ /* Auto-dividers between groups when [divider] set */
89
+ :scope[divider] > nav-group-ui + nav-group-ui,
90
+ :scope[divider] > nav-item-ui + nav-group-ui,
91
+ :scope[divider] > nav-group-ui + nav-item-ui {
92
+ border-top: 1px solid var(--nav-divider-bg);
93
+ margin-top: var(--nav-divider-my);
94
+ padding-top: var(--nav-divider-my);
95
+ }
96
+
97
+ /* Spacer — pushes items below to bottom */
98
+ [data-nav-spacer] { flex: 1; }
99
+
100
+ /* Collapsed (primary): hide labels + dividers */
101
+ :scope:not([variant="section"])[collapsed] [data-nav-label],
102
+ :scope:not([variant="section"])[collapsed] [data-nav-divider] {
103
+ display: none;
104
+ }
105
+
106
+ @container sidebar (max-width: 96px) {
107
+ :scope:not([variant="section"]) [data-nav-label],
108
+ :scope:not([variant="section"]) [data-nav-divider] {
109
+ display: none;
110
+ }
111
+ }
112
+
113
+ /* ── Section variant ── */
114
+
115
+ :scope[variant="section"] {
116
+ --nav-px: 0;
117
+ --nav-py: 0;
118
+ }
119
+
120
+ /* Heading rendered from [heading] attribute on section variant. */
121
+ :scope[variant="section"][heading]:not([heading=""])::before {
122
+ content: attr(heading);
123
+ display: block;
124
+ padding: var(--nav-label-py) var(--nav-label-px);
125
+ font-size: var(--nav-label-font-size);
126
+ font-weight: var(--nav-label-weight);
127
+ color: var(--nav-label-fg);
128
+ text-transform: uppercase;
129
+ letter-spacing: 0.06em;
130
+ white-space: nowrap;
131
+ overflow: hidden;
132
+ }
133
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * <nav-ui> — Navigation rail (consolidated primary + section variants).
3
+ *
4
+ * Consolidates the prior `app-nav-ui` + `section-nav-ui` dual-family
5
+ * per ADR-0015 § Nav consolidation. Variant drives visual treatment;
6
+ * behavior is unified.
7
+ *
8
+ * Variants:
9
+ * primary — default. App sidebar. ResizeObserver collapses to icon-only
10
+ * below 96px container width or when [collapsed] set; groups
11
+ * open a popover with their children when collapsed.
12
+ * section — subnav rail. Quieter chrome; no collapse behavior.
13
+ * Optional [heading] attribute renders an uppercase label
14
+ * via ::before pseudo (CSS-only).
15
+ *
16
+ * Structure:
17
+ * <nav-ui>
18
+ * <nav-group-ui icon="gear" text="Settings" badge="3">
19
+ * <nav-item-ui text="General" value="/settings/general"></nav-item-ui>
20
+ * </nav-group-ui>
21
+ * <nav-item-ui icon="user" text="Profile" value="/profile"></nav-item-ui>
22
+ * </nav-ui>
23
+ *
24
+ * <nav-ui variant="section" heading="On this page">
25
+ * <nav-item-ui text="Overview" value="#overview"></nav-item-ui>
26
+ * </nav-ui>
27
+ *
28
+ * Event: nav-select (bubbles from nav-item-ui). detail: { item, text, value }
29
+ */
30
+
31
+ import { UIElement } from '../../core/element.js';
32
+
33
+ class UINav extends UIElement {
34
+ static properties = {
35
+ variant: { type: String, default: 'primary', reflect: true },
36
+ collapsed: { type: Boolean, default: false, reflect: true },
37
+ divider: { type: Boolean, default: false, reflect: true },
38
+ heading: { type: String, default: '', reflect: true },
39
+ };
40
+
41
+ static template = () => null;
42
+
43
+ #ro = null;
44
+
45
+ connected() {
46
+ this.setAttribute('role', 'navigation');
47
+ if (this.heading) this.setAttribute('aria-label', this.heading);
48
+ this.addEventListener('click', this.#onClick);
49
+
50
+ // ResizeObserver only meaningful for primary variant — section is static.
51
+ if (this.variant !== 'section') {
52
+ this.#ro = new ResizeObserver(() => this.#updateTooltips());
53
+ this.#ro.observe(this);
54
+ }
55
+ }
56
+
57
+ render() {
58
+ if (this.heading) this.setAttribute('aria-label', this.heading);
59
+ else this.removeAttribute('aria-label');
60
+ if (this.variant !== 'section') this.#updateTooltips();
61
+ }
62
+
63
+ #updateTooltips() {
64
+ const isCollapsed = this.collapsed || this.getBoundingClientRect().width <= 96;
65
+ for (const group of this.querySelectorAll(':scope > nav-group-ui')) {
66
+ const header = group.querySelector(':scope > [slot="header"]');
67
+ if (header) header.title = isCollapsed ? group.text : '';
68
+ }
69
+ for (const item of this.querySelectorAll(':scope > nav-item-ui')) {
70
+ item.title = isCollapsed ? item.text : '';
71
+ }
72
+ }
73
+
74
+ get selectedItem() {
75
+ return this.querySelector('nav-item-ui[selected]');
76
+ }
77
+
78
+ select(item) {
79
+ const prev = this.selectedItem;
80
+ if (prev && prev !== item) prev.removeAttribute('selected');
81
+ if (item) {
82
+ item.setAttribute('selected', '');
83
+ this.dispatchEvent(new CustomEvent('nav-select', {
84
+ bubbles: true,
85
+ detail: { item, text: item.text, value: item.value },
86
+ }));
87
+ }
88
+ }
89
+
90
+ toggle() {
91
+ if (this.variant === 'section') return; // no-op for section variant
92
+ this.collapsed = !this.collapsed;
93
+ }
94
+
95
+ #onClick = (e) => {
96
+ const item = e.target.closest('nav-item-ui');
97
+ if (item && this.contains(item)) {
98
+ if (item.disabled) return;
99
+ this.select(item);
100
+ this.#flushHoverState();
101
+ return;
102
+ }
103
+
104
+ // Group expand/popover — primary variant only.
105
+ if (this.variant === 'section') return;
106
+
107
+ const group = e.target.closest('nav-group-ui');
108
+ if (group && this.contains(group)) {
109
+ const isCollapsed = this.collapsed || this.getBoundingClientRect().width <= 96;
110
+ if (isCollapsed) {
111
+ group.showPopover?.();
112
+ } else {
113
+ const header = group.querySelector(':scope > [slot="header"]');
114
+ if (header && (e.target === header || header.contains(e.target))) {
115
+ group.open = !group.open;
116
+ }
117
+ }
118
+ }
119
+ };
120
+
121
+ // Safari macOS leaves :hover stuck on items the cursor passed through
122
+ // when the DOM mutates during click+route navigation. Toggling pointer-
123
+ // events forces re-evaluation of hover state on next paint.
124
+ // See docs/BROWSER-COMPAT.md §3a.
125
+ #flushHoverState() {
126
+ this.style.pointerEvents = 'none';
127
+ requestAnimationFrame(() => {
128
+ this.style.pointerEvents = '';
129
+ });
130
+ }
131
+
132
+ disconnected() {
133
+ this.removeEventListener('click', this.#onClick);
134
+ this.#ro?.disconnect();
135
+ this.#ro = null;
136
+ }
137
+ }
138
+
139
+ customElements.define('nav-ui', UINav);
140
+ export { UINav };
@@ -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
+ });