@discourser/design-system 0.15.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/{chunk-UNWXE6UB.cjs → chunk-2P7Z5PVP.cjs} +817 -16
  2. package/dist/chunk-2P7Z5PVP.cjs.map +1 -0
  3. package/dist/{chunk-ABC7N32K.cjs → chunk-PFWU7QSM.cjs} +464 -8
  4. package/dist/chunk-PFWU7QSM.cjs.map +1 -0
  5. package/dist/{chunk-GD6Q2FUE.js → chunk-QC7LGFM3.js} +808 -18
  6. package/dist/chunk-QC7LGFM3.js.map +1 -0
  7. package/dist/{chunk-SBKRSXSZ.js → chunk-SNUJBT5R.js} +464 -8
  8. package/dist/chunk-SNUJBT5R.js.map +1 -0
  9. package/dist/components/Accordion.figma.d.ts +2 -0
  10. package/dist/components/Accordion.figma.d.ts.map +1 -0
  11. package/dist/components/Breadcrumb.d.ts +2 -0
  12. package/dist/components/Breadcrumb.d.ts.map +1 -1
  13. package/dist/components/Breadcrumb.figma.d.ts +2 -0
  14. package/dist/components/Breadcrumb.figma.d.ts.map +1 -0
  15. package/dist/components/ContentCard/ContentCard.d.ts +13 -0
  16. package/dist/components/ContentCard/ContentCard.d.ts.map +1 -0
  17. package/dist/components/ContentCard/ContentCard.figma.d.ts +2 -0
  18. package/dist/components/ContentCard/ContentCard.figma.d.ts.map +1 -0
  19. package/dist/components/ContentCard/index.d.ts +2 -0
  20. package/dist/components/ContentCard/index.d.ts.map +1 -0
  21. package/dist/components/{Heading.d.ts → Header.d.ts} +3 -3
  22. package/dist/components/Header.d.ts.map +1 -0
  23. package/dist/components/Header.figma.d.ts +2 -0
  24. package/dist/components/Header.figma.d.ts.map +1 -0
  25. package/dist/components/Icons/AccountIcon.d.ts +6 -0
  26. package/dist/components/Icons/AccountIcon.d.ts.map +1 -0
  27. package/dist/components/Icons/AudienceIcon.d.ts +6 -0
  28. package/dist/components/Icons/AudienceIcon.d.ts.map +1 -0
  29. package/dist/components/Icons/AudienceIcon.figma.d.ts +2 -0
  30. package/dist/components/Icons/AudienceIcon.figma.d.ts.map +1 -0
  31. package/dist/components/Icons/AudioSpeakerIcon.d.ts +6 -0
  32. package/dist/components/Icons/AudioSpeakerIcon.d.ts.map +1 -0
  33. package/dist/components/Icons/AudioSpeakerIcon.figma.d.ts +2 -0
  34. package/dist/components/Icons/AudioSpeakerIcon.figma.d.ts.map +1 -0
  35. package/dist/components/Icons/BookmarkPlusIcon.d.ts +6 -0
  36. package/dist/components/Icons/BookmarkPlusIcon.d.ts.map +1 -0
  37. package/dist/components/Icons/BookmarkPlusIcon.figma.d.ts +2 -0
  38. package/dist/components/Icons/BookmarkPlusIcon.figma.d.ts.map +1 -0
  39. package/dist/components/Icons/ChevronUpIcon.d.ts +6 -0
  40. package/dist/components/Icons/ChevronUpIcon.d.ts.map +1 -0
  41. package/dist/components/Icons/ClipBoardIcon.d.ts +6 -0
  42. package/dist/components/Icons/ClipBoardIcon.d.ts.map +1 -0
  43. package/dist/components/Icons/ClipBoardIcon.figma.d.ts +2 -0
  44. package/dist/components/Icons/ClipBoardIcon.figma.d.ts.map +1 -0
  45. package/dist/components/Icons/ClockIcon.d.ts.map +1 -1
  46. package/dist/components/Icons/DashboardIcon.d.ts +6 -0
  47. package/dist/components/Icons/DashboardIcon.d.ts.map +1 -0
  48. package/dist/components/Icons/DiscourserLogo.d.ts +6 -0
  49. package/dist/components/Icons/DiscourserLogo.d.ts.map +1 -0
  50. package/dist/components/Icons/DiscourserLogo.figma.d.ts +2 -0
  51. package/dist/components/Icons/DiscourserLogo.figma.d.ts.map +1 -0
  52. package/dist/components/Icons/ExitStudioIcon.d.ts +6 -0
  53. package/dist/components/Icons/ExitStudioIcon.d.ts.map +1 -0
  54. package/dist/components/Icons/ExitStudioIcon.figma.d.ts +2 -0
  55. package/dist/components/Icons/ExitStudioIcon.figma.d.ts.map +1 -0
  56. package/dist/components/Icons/GripDotsVerticalIcon.d.ts.map +1 -1
  57. package/dist/components/Icons/HelpIcon.d.ts +6 -0
  58. package/dist/components/Icons/HelpIcon.d.ts.map +1 -0
  59. package/dist/components/Icons/MicrophoneIcon.d.ts +6 -0
  60. package/dist/components/Icons/MicrophoneIcon.d.ts.map +1 -0
  61. package/dist/components/Icons/MicrophoneIcon.figma.d.ts +2 -0
  62. package/dist/components/Icons/MicrophoneIcon.figma.d.ts.map +1 -0
  63. package/dist/components/Icons/NotebookIcon.d.ts +6 -0
  64. package/dist/components/Icons/NotebookIcon.d.ts.map +1 -0
  65. package/dist/components/Icons/NotebookPenIcon.d.ts +6 -0
  66. package/dist/components/Icons/NotebookPenIcon.d.ts.map +1 -0
  67. package/dist/components/Icons/NotebookPenIcon.figma.d.ts +2 -0
  68. package/dist/components/Icons/NotebookPenIcon.figma.d.ts.map +1 -0
  69. package/dist/components/Icons/PausePlayIcon.d.ts +6 -0
  70. package/dist/components/Icons/PausePlayIcon.d.ts.map +1 -0
  71. package/dist/components/Icons/PausePlayIcon.figma.d.ts +2 -0
  72. package/dist/components/Icons/PausePlayIcon.figma.d.ts.map +1 -0
  73. package/dist/components/Icons/PlayIcon.d.ts +6 -0
  74. package/dist/components/Icons/PlayIcon.d.ts.map +1 -0
  75. package/dist/components/Icons/PlayIcon.figma.d.ts +2 -0
  76. package/dist/components/Icons/PlayIcon.figma.d.ts.map +1 -0
  77. package/dist/components/Icons/RecordIcon.d.ts +6 -0
  78. package/dist/components/Icons/RecordIcon.d.ts.map +1 -0
  79. package/dist/components/Icons/RecordIcon.figma.d.ts +2 -0
  80. package/dist/components/Icons/RecordIcon.figma.d.ts.map +1 -0
  81. package/dist/components/Icons/RepeatQuestionIcon.d.ts +6 -0
  82. package/dist/components/Icons/RepeatQuestionIcon.d.ts.map +1 -0
  83. package/dist/components/Icons/RepeatQuestionIcon.figma.d.ts +2 -0
  84. package/dist/components/Icons/RepeatQuestionIcon.figma.d.ts.map +1 -0
  85. package/dist/components/Icons/RightArrowIcon.d.ts +6 -0
  86. package/dist/components/Icons/RightArrowIcon.d.ts.map +1 -0
  87. package/dist/components/Icons/ScenarioIcon.d.ts +6 -0
  88. package/dist/components/Icons/ScenarioIcon.d.ts.map +1 -0
  89. package/dist/components/Icons/ScrollTextIcon.d.ts +6 -0
  90. package/dist/components/Icons/ScrollTextIcon.d.ts.map +1 -0
  91. package/dist/components/Icons/ScrollTextIcon.figma.d.ts +2 -0
  92. package/dist/components/Icons/ScrollTextIcon.figma.d.ts.map +1 -0
  93. package/dist/components/Icons/SparklesIcon.d.ts +6 -0
  94. package/dist/components/Icons/SparklesIcon.d.ts.map +1 -0
  95. package/dist/components/Icons/SparklesIcon.figma.d.ts +2 -0
  96. package/dist/components/Icons/SparklesIcon.figma.d.ts.map +1 -0
  97. package/dist/components/Icons/SpeechIcon.d.ts +6 -0
  98. package/dist/components/Icons/SpeechIcon.d.ts.map +1 -0
  99. package/dist/components/Icons/SpeechIcon.figma.d.ts +2 -0
  100. package/dist/components/Icons/SpeechIcon.figma.d.ts.map +1 -0
  101. package/dist/components/Icons/StopPlayIcon.d.ts +6 -0
  102. package/dist/components/Icons/StopPlayIcon.d.ts.map +1 -0
  103. package/dist/components/Icons/StopPlayIcon.figma.d.ts +2 -0
  104. package/dist/components/Icons/StopPlayIcon.figma.d.ts.map +1 -0
  105. package/dist/components/Icons/TimerIcon.d.ts +6 -0
  106. package/dist/components/Icons/TimerIcon.d.ts.map +1 -0
  107. package/dist/components/Icons/TimerIcon.figma.d.ts +2 -0
  108. package/dist/components/Icons/TimerIcon.figma.d.ts.map +1 -0
  109. package/dist/components/Icons/UserProfileIcon.d.ts +6 -0
  110. package/dist/components/Icons/UserProfileIcon.d.ts.map +1 -0
  111. package/dist/components/Icons/UserProfileIcon.figma.d.ts +2 -0
  112. package/dist/components/Icons/UserProfileIcon.figma.d.ts.map +1 -0
  113. package/dist/components/Icons/index.d.ts +26 -1
  114. package/dist/components/Icons/index.d.ts.map +1 -1
  115. package/dist/components/NavigationMenu/NavigationMenu.d.ts +3 -0
  116. package/dist/components/NavigationMenu/NavigationMenu.d.ts.map +1 -0
  117. package/dist/components/NavigationMenu/NavigationMenu.figma.d.ts +2 -0
  118. package/dist/components/NavigationMenu/NavigationMenu.figma.d.ts.map +1 -0
  119. package/dist/components/NavigationMenu/index.d.ts +3 -0
  120. package/dist/components/NavigationMenu/index.d.ts.map +1 -0
  121. package/dist/components/NavigationMenu/types.d.ts +25 -0
  122. package/dist/components/NavigationMenu/types.d.ts.map +1 -0
  123. package/dist/components/QuickStartPage/QuickStartPage.d.ts +21 -0
  124. package/dist/components/QuickStartPage/QuickStartPage.d.ts.map +1 -0
  125. package/dist/components/QuickStartPage/index.d.ts +3 -0
  126. package/dist/components/QuickStartPage/index.d.ts.map +1 -0
  127. package/dist/components/ScenarioQueue/ScenarioQueue.figma.d.ts +2 -0
  128. package/dist/components/ScenarioQueue/ScenarioQueue.figma.d.ts.map +1 -0
  129. package/dist/components/ScenarioSettings/ScenarioSettings.d.ts +3 -0
  130. package/dist/components/ScenarioSettings/ScenarioSettings.d.ts.map +1 -0
  131. package/dist/components/ScenarioSettings/ScenarioSettings.figma.d.ts +2 -0
  132. package/dist/components/ScenarioSettings/ScenarioSettings.figma.d.ts.map +1 -0
  133. package/dist/components/ScenarioSettings/index.d.ts +3 -0
  134. package/dist/components/ScenarioSettings/index.d.ts.map +1 -0
  135. package/dist/components/ScenarioSettings/types.d.ts +54 -0
  136. package/dist/components/ScenarioSettings/types.d.ts.map +1 -0
  137. package/dist/components/index.cjs +86 -42
  138. package/dist/components/index.d.ts +14 -3
  139. package/dist/components/index.d.ts.map +1 -1
  140. package/dist/components/index.js +1 -1
  141. package/dist/figma-codex/config.d.ts +8 -0
  142. package/dist/figma-codex/config.d.ts.map +1 -0
  143. package/dist/figma-codex/fixtures/CompoundComponent/CompoundComponent.d.ts +6 -0
  144. package/dist/figma-codex/fixtures/CompoundComponent/CompoundComponent.d.ts.map +1 -0
  145. package/dist/figma-codex/fixtures/CompoundComponent/index.d.ts +2 -0
  146. package/dist/figma-codex/fixtures/CompoundComponent/index.d.ts.map +1 -0
  147. package/dist/figma-codex/fixtures/CompoundComponent.figma.d.ts +2 -0
  148. package/dist/figma-codex/fixtures/CompoundComponent.figma.d.ts.map +1 -0
  149. package/dist/figma-codex/fixtures/SimpleComponent.d.ts +8 -0
  150. package/dist/figma-codex/fixtures/SimpleComponent.d.ts.map +1 -0
  151. package/dist/figma-codex/fixtures/SimpleComponent.figma.d.ts +2 -0
  152. package/dist/figma-codex/fixtures/SimpleComponent.figma.d.ts.map +1 -0
  153. package/dist/figma-codex/generate.d.ts +6 -0
  154. package/dist/figma-codex/generate.d.ts.map +1 -0
  155. package/dist/figma-codex/parser.d.ts +18 -0
  156. package/dist/figma-codex/parser.d.ts.map +1 -0
  157. package/dist/figma-codex/resolver.d.ts +5 -0
  158. package/dist/figma-codex/resolver.d.ts.map +1 -0
  159. package/dist/figma-codex/schema.d.ts +60 -0
  160. package/dist/figma-codex/schema.d.ts.map +1 -0
  161. package/dist/figma-codex/writer.d.ts +8 -0
  162. package/dist/figma-codex/writer.d.ts.map +1 -0
  163. package/dist/figma-codex.json +679 -0
  164. package/dist/index.cjs +90 -46
  165. package/dist/index.js +2 -2
  166. package/dist/preset/index.cjs +2 -2
  167. package/dist/preset/index.d.ts.map +1 -1
  168. package/dist/preset/index.js +1 -1
  169. package/dist/preset/recipes/accordion.d.ts.map +1 -1
  170. package/dist/preset/recipes/breadcrumb.d.ts.map +1 -1
  171. package/dist/preset/recipes/content-card.d.ts +2 -0
  172. package/dist/preset/recipes/content-card.d.ts.map +1 -0
  173. package/dist/preset/recipes/index.d.ts +4 -0
  174. package/dist/preset/recipes/index.d.ts.map +1 -1
  175. package/dist/preset/recipes/navigation-menu.d.ts +2 -0
  176. package/dist/preset/recipes/navigation-menu.d.ts.map +1 -0
  177. package/dist/preset/recipes/scenario-settings.d.ts +2 -0
  178. package/dist/preset/recipes/scenario-settings.d.ts.map +1 -0
  179. package/package.json +26 -2
  180. package/src/components/Accordion.figma.tsx +20 -0
  181. package/src/components/Breadcrumb.figma.tsx +18 -0
  182. package/src/components/Breadcrumb.tsx +33 -15
  183. package/src/components/ContentCard/ContentCard.figma.tsx +21 -0
  184. package/src/components/ContentCard/ContentCard.test.tsx +197 -0
  185. package/src/components/ContentCard/ContentCard.tsx +19 -0
  186. package/src/components/ContentCard/index.ts +13 -0
  187. package/src/components/Header.figma.tsx +25 -0
  188. package/src/components/{Heading.tsx → Header.tsx} +2 -2
  189. package/src/components/Icons/AccountIcon.tsx +26 -0
  190. package/src/components/Icons/AudienceIcon.figma.tsx +10 -0
  191. package/src/components/Icons/AudienceIcon.tsx +20 -0
  192. package/src/components/Icons/AudioSpeakerIcon.figma.tsx +10 -0
  193. package/src/components/Icons/AudioSpeakerIcon.tsx +26 -0
  194. package/src/components/Icons/BookmarkPlusIcon.figma.tsx +10 -0
  195. package/src/components/Icons/BookmarkPlusIcon.tsx +26 -0
  196. package/src/components/Icons/ChevronUpIcon.tsx +24 -0
  197. package/src/components/Icons/ClipBoardIcon.figma.tsx +10 -0
  198. package/src/components/Icons/ClipBoardIcon.tsx +61 -0
  199. package/src/components/Icons/ClockIcon.tsx +6 -6
  200. package/src/components/Icons/DashboardIcon.tsx +47 -0
  201. package/src/components/Icons/Discourser-Logo.svg +14 -0
  202. package/src/components/Icons/DiscourserLogo.figma.tsx +10 -0
  203. package/src/components/Icons/DiscourserLogo.tsx +72 -0
  204. package/src/components/Icons/ExitStudioIcon.figma.tsx +10 -0
  205. package/src/components/Icons/ExitStudioIcon.tsx +34 -0
  206. package/src/components/Icons/GripDotsVerticalIcon.tsx +6 -6
  207. package/src/components/Icons/HelpIcon.tsx +26 -0
  208. package/src/components/Icons/MicrophoneIcon.figma.tsx +10 -0
  209. package/src/components/Icons/MicrophoneIcon.tsx +40 -0
  210. package/src/components/Icons/NotebookIcon.tsx +26 -0
  211. package/src/components/Icons/NotebookPenIcon.figma.tsx +10 -0
  212. package/src/components/Icons/NotebookPenIcon.tsx +26 -0
  213. package/src/components/Icons/PausePlayIcon.figma.tsx +10 -0
  214. package/src/components/Icons/PausePlayIcon.tsx +41 -0
  215. package/src/components/Icons/PlayIcon.figma.tsx +10 -0
  216. package/src/components/Icons/PlayIcon.tsx +33 -0
  217. package/src/components/Icons/RecordIcon.figma.tsx +10 -0
  218. package/src/components/Icons/RecordIcon.tsx +41 -0
  219. package/src/components/Icons/RepeatQuestionIcon.figma.tsx +10 -0
  220. package/src/components/Icons/RepeatQuestionIcon.tsx +26 -0
  221. package/src/components/Icons/RightArrowIcon.tsx +23 -0
  222. package/src/components/Icons/ScenarioIcon.tsx +26 -0
  223. package/src/components/Icons/ScrollTextIcon.figma.tsx +10 -0
  224. package/src/components/Icons/ScrollTextIcon.tsx +26 -0
  225. package/src/components/Icons/SparklesIcon.figma.tsx +10 -0
  226. package/src/components/Icons/SparklesIcon.tsx +26 -0
  227. package/src/components/Icons/SpeechIcon.figma.tsx +10 -0
  228. package/src/components/Icons/SpeechIcon.tsx +26 -0
  229. package/src/components/Icons/StopPlayIcon.figma.tsx +10 -0
  230. package/src/components/Icons/StopPlayIcon.tsx +35 -0
  231. package/src/components/Icons/TimerIcon.figma.tsx +10 -0
  232. package/src/components/Icons/TimerIcon.tsx +26 -0
  233. package/src/components/Icons/UserProfileIcon.figma.tsx +10 -0
  234. package/src/components/Icons/UserProfileIcon.tsx +26 -0
  235. package/src/components/Icons/index.ts +39 -2
  236. package/src/components/NavigationMenu/NavigationMenu.figma.tsx +26 -0
  237. package/src/components/NavigationMenu/NavigationMenu.test.tsx +524 -0
  238. package/src/components/NavigationMenu/NavigationMenu.tsx +102 -0
  239. package/src/components/NavigationMenu/index.ts +2 -0
  240. package/src/components/NavigationMenu/types.ts +27 -0
  241. package/src/components/QuickStartPage/QuickStartPage.tsx +627 -0
  242. package/src/components/QuickStartPage/index.ts +2 -0
  243. package/src/components/ScenarioQueue/ScenarioQueue.figma.tsx +37 -0
  244. package/src/components/ScenarioSettings/ScenarioSettings.figma.tsx +12 -0
  245. package/src/components/ScenarioSettings/ScenarioSettings.test.tsx +406 -0
  246. package/src/components/ScenarioSettings/ScenarioSettings.tsx +386 -0
  247. package/src/components/ScenarioSettings/index.ts +11 -0
  248. package/src/components/ScenarioSettings/types.ts +70 -0
  249. package/src/components/__tests__/Breadcrumb.test.tsx +94 -0
  250. package/src/components/index.ts +38 -4
  251. package/src/figma-codex/README.md +186 -0
  252. package/src/figma-codex/__tests__/config.test.ts +63 -0
  253. package/src/figma-codex/__tests__/generate.test.ts +78 -0
  254. package/src/figma-codex/__tests__/parser.test.ts +138 -0
  255. package/src/figma-codex/__tests__/resolver.test.ts +196 -0
  256. package/src/figma-codex/__tests__/writer.test.ts +111 -0
  257. package/src/figma-codex/config.ts +42 -0
  258. package/src/figma-codex/fixtures/CompoundComponent/CompoundComponent.tsx +17 -0
  259. package/src/figma-codex/fixtures/CompoundComponent/index.ts +1 -0
  260. package/src/figma-codex/fixtures/CompoundComponent.figma.tsx +14 -0
  261. package/src/figma-codex/fixtures/SimpleComponent.figma.tsx +10 -0
  262. package/src/figma-codex/fixtures/SimpleComponent.tsx +10 -0
  263. package/src/figma-codex/fixtures/expected-output.json +78 -0
  264. package/src/figma-codex/generate.ts +106 -0
  265. package/src/figma-codex/parser.ts +138 -0
  266. package/src/figma-codex/resolver.ts +280 -0
  267. package/src/figma-codex/schema.ts +79 -0
  268. package/src/figma-codex/writer.ts +54 -0
  269. package/src/preset/index.ts +6 -0
  270. package/src/preset/recipes/accordion.ts +8 -5
  271. package/src/preset/recipes/breadcrumb.ts +34 -2
  272. package/src/preset/recipes/content-card.ts +124 -0
  273. package/src/preset/recipes/index.ts +4 -0
  274. package/src/preset/recipes/navigation-menu.ts +97 -0
  275. package/src/preset/recipes/scenario-settings.ts +182 -0
  276. package/src/test/setup.ts +12 -9
  277. package/dist/chunk-ABC7N32K.cjs.map +0 -1
  278. package/dist/chunk-GD6Q2FUE.js.map +0 -1
  279. package/dist/chunk-SBKRSXSZ.js.map +0 -1
  280. package/dist/chunk-UNWXE6UB.cjs.map +0 -1
  281. package/dist/components/Heading.d.ts.map +0 -1
@@ -0,0 +1,524 @@
1
+ /* global describe, it, expect, vi */
2
+ import React from 'react';
3
+ import { render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { axe } from 'jest-axe';
6
+ import { NavigationMenu } from './NavigationMenu';
7
+ import type { NavSection } from './types';
8
+
9
+ // ── Mock icon ─────────────────────────────────────────────────────────────────
10
+ // Avoids importing real DS icon SVGs — keeps tests free of styled-system deps.
11
+
12
+ const MockIcon = () =>
13
+ React.createElement('svg', { 'data-testid': 'section-icon' });
14
+
15
+ // ── Fixtures ──────────────────────────────────────────────────────────────────
16
+ // Mirrors the mock data in NavigationMenu.stories.tsx
17
+
18
+ const MOCK_SECTIONS: NavSection[] = [
19
+ {
20
+ value: 'dashboard',
21
+ title: 'Dashboard',
22
+ icon: React.createElement(MockIcon),
23
+ items: [
24
+ { label: 'Quick Start', href: '/dashboard/quick-start' },
25
+ { label: 'Resume Session', href: '/dashboard/resume-session' },
26
+ { label: 'Progress', href: '/dashboard/progress' },
27
+ ],
28
+ },
29
+ {
30
+ value: 'scenarios',
31
+ title: 'Scenarios',
32
+ icon: React.createElement(MockIcon),
33
+ items: [
34
+ { label: 'MyQueue', href: '/scenarios/my-queue' },
35
+ { label: 'Conversation Studio', href: '/scenarios/conversation-studio' },
36
+ ],
37
+ },
38
+ {
39
+ value: 'help',
40
+ title: 'Help',
41
+ icon: React.createElement(MockIcon),
42
+ items: [
43
+ { label: 'How it Works', href: '/help/how-it-works' },
44
+ { label: 'Contact Support', href: '/help/contact-support' },
45
+ ],
46
+ },
47
+ ];
48
+
49
+ // ── Tests ─────────────────────────────────────────────────────────────────────
50
+
51
+ describe('NavigationMenu', () => {
52
+ // ── Rendering ───────────────────────────────────────────────────────────────
53
+
54
+ describe('Rendering', () => {
55
+ it('renders a <nav> with the provided aria-label', () => {
56
+ render(
57
+ <NavigationMenu
58
+ sections={MOCK_SECTIONS}
59
+ ariaLabel="Dashboard navigation"
60
+ />,
61
+ );
62
+
63
+ expect(
64
+ screen.getByRole('navigation', { name: 'Dashboard navigation' }),
65
+ ).toBeInTheDocument();
66
+ });
67
+
68
+ it('defaults ariaLabel to "Navigation" when not provided', () => {
69
+ render(<NavigationMenu sections={MOCK_SECTIONS} />);
70
+
71
+ expect(
72
+ screen.getByRole('navigation', { name: 'Navigation' }),
73
+ ).toBeInTheDocument();
74
+ });
75
+
76
+ it('renders one trigger button per section', () => {
77
+ render(<NavigationMenu sections={MOCK_SECTIONS} />);
78
+
79
+ expect(
80
+ screen.getByRole('button', { name: /Dashboard/i }),
81
+ ).toBeInTheDocument();
82
+ expect(
83
+ screen.getByRole('button', { name: /Scenarios/i }),
84
+ ).toBeInTheDocument();
85
+ expect(screen.getByRole('button', { name: /Help/i })).toBeInTheDocument();
86
+ });
87
+
88
+ it('renders icons for each section', () => {
89
+ render(<NavigationMenu sections={MOCK_SECTIONS} />);
90
+
91
+ expect(screen.getAllByTestId('section-icon')).toHaveLength(
92
+ MOCK_SECTIONS.length,
93
+ );
94
+ });
95
+
96
+ it('renders nav items for sections that are open by default', () => {
97
+ render(
98
+ <NavigationMenu
99
+ sections={MOCK_SECTIONS}
100
+ defaultOpenSections={['dashboard']}
101
+ />,
102
+ );
103
+
104
+ expect(
105
+ screen.getByRole('link', { name: 'Quick Start' }),
106
+ ).toBeInTheDocument();
107
+ expect(
108
+ screen.getByRole('link', { name: 'Resume Session' }),
109
+ ).toBeInTheDocument();
110
+ expect(
111
+ screen.getByRole('link', { name: 'Progress' }),
112
+ ).toBeInTheDocument();
113
+ });
114
+
115
+ it('does not render items for sections that are closed by default', () => {
116
+ render(
117
+ <NavigationMenu
118
+ sections={MOCK_SECTIONS}
119
+ defaultOpenSections={['dashboard']}
120
+ />,
121
+ );
122
+
123
+ // Scenarios section is closed — its items are not in the DOM
124
+ expect(
125
+ screen.queryByRole('link', { name: 'MyQueue' }),
126
+ ).not.toBeInTheDocument();
127
+ });
128
+ });
129
+
130
+ // ── Expand / Collapse ───────────────────────────────────────────────────────
131
+
132
+ describe('Expand / Collapse', () => {
133
+ it('closed section trigger has aria-expanded="false"', () => {
134
+ render(
135
+ <NavigationMenu sections={MOCK_SECTIONS} defaultOpenSections={[]} />,
136
+ );
137
+
138
+ const trigger = screen.getByRole('button', { name: /Dashboard/i });
139
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
140
+ });
141
+
142
+ it('open section trigger has aria-expanded="true"', () => {
143
+ render(
144
+ <NavigationMenu
145
+ sections={MOCK_SECTIONS}
146
+ defaultOpenSections={['dashboard']}
147
+ />,
148
+ );
149
+
150
+ const trigger = screen.getByRole('button', { name: /Dashboard/i });
151
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
152
+ });
153
+
154
+ it('clicking a closed section opens it and shows its items', async () => {
155
+ const user = userEvent.setup();
156
+ render(
157
+ <NavigationMenu sections={MOCK_SECTIONS} defaultOpenSections={[]} />,
158
+ );
159
+
160
+ const dashboardTrigger = screen.getByRole('button', {
161
+ name: /Dashboard/i,
162
+ });
163
+ await user.click(dashboardTrigger);
164
+
165
+ expect(dashboardTrigger).toHaveAttribute('aria-expanded', 'true');
166
+ expect(
167
+ screen.getByRole('link', { name: 'Quick Start' }),
168
+ ).toBeInTheDocument();
169
+ });
170
+
171
+ it('clicking an open section closes it', async () => {
172
+ const user = userEvent.setup();
173
+ render(
174
+ <NavigationMenu
175
+ sections={MOCK_SECTIONS}
176
+ defaultOpenSections={['dashboard']}
177
+ />,
178
+ );
179
+
180
+ const dashboardTrigger = screen.getByRole('button', {
181
+ name: /Dashboard/i,
182
+ });
183
+ await user.click(dashboardTrigger);
184
+
185
+ expect(dashboardTrigger).toHaveAttribute('aria-expanded', 'false');
186
+ });
187
+
188
+ it('multiple sections can be open simultaneously', async () => {
189
+ const user = userEvent.setup();
190
+ render(
191
+ <NavigationMenu
192
+ sections={MOCK_SECTIONS}
193
+ defaultOpenSections={['dashboard']}
194
+ />,
195
+ );
196
+
197
+ // Open Scenarios section too
198
+ await user.click(screen.getByRole('button', { name: /Scenarios/i }));
199
+
200
+ expect(
201
+ screen.getByRole('button', { name: /Dashboard/i }),
202
+ ).toHaveAttribute('aria-expanded', 'true');
203
+ expect(
204
+ screen.getByRole('button', { name: /Scenarios/i }),
205
+ ).toHaveAttribute('aria-expanded', 'true');
206
+ expect(
207
+ screen.getByRole('link', { name: 'Quick Start' }),
208
+ ).toBeInTheDocument();
209
+ expect(screen.getByRole('link', { name: 'MyQueue' })).toBeInTheDocument();
210
+ });
211
+
212
+ it('all sections expand correctly when defaultOpenSections covers all values', () => {
213
+ render(
214
+ <NavigationMenu
215
+ sections={MOCK_SECTIONS}
216
+ defaultOpenSections={['dashboard', 'scenarios', 'help']}
217
+ />,
218
+ );
219
+
220
+ expect(
221
+ screen.getByRole('link', { name: 'Quick Start' }),
222
+ ).toBeInTheDocument();
223
+ expect(screen.getByRole('link', { name: 'MyQueue' })).toBeInTheDocument();
224
+ expect(
225
+ screen.getByRole('link', { name: 'How it Works' }),
226
+ ).toBeInTheDocument();
227
+ });
228
+ });
229
+
230
+ // ── Active Item ─────────────────────────────────────────────────────────────
231
+
232
+ describe('Active Item', () => {
233
+ it('active link has aria-current="page"', () => {
234
+ render(
235
+ <NavigationMenu
236
+ sections={MOCK_SECTIONS}
237
+ defaultOpenSections={['dashboard']}
238
+ activeHref="/dashboard/quick-start"
239
+ />,
240
+ );
241
+
242
+ const activeLink = screen.getByRole('link', { name: 'Quick Start' });
243
+ expect(activeLink).toHaveAttribute('aria-current', 'page');
244
+ });
245
+
246
+ it('active link has data-active="true"', () => {
247
+ render(
248
+ <NavigationMenu
249
+ sections={MOCK_SECTIONS}
250
+ defaultOpenSections={['dashboard']}
251
+ activeHref="/dashboard/quick-start"
252
+ />,
253
+ );
254
+
255
+ const activeLink = screen.getByRole('link', { name: 'Quick Start' });
256
+ expect(activeLink).toHaveAttribute('data-active', 'true');
257
+ });
258
+
259
+ it('non-active links do not have aria-current', () => {
260
+ render(
261
+ <NavigationMenu
262
+ sections={MOCK_SECTIONS}
263
+ defaultOpenSections={['dashboard']}
264
+ activeHref="/dashboard/quick-start"
265
+ />,
266
+ );
267
+
268
+ const inactiveLink = screen.getByRole('link', { name: 'Resume Session' });
269
+ expect(inactiveLink).not.toHaveAttribute('aria-current');
270
+ });
271
+
272
+ it('non-active links do not have data-active', () => {
273
+ render(
274
+ <NavigationMenu
275
+ sections={MOCK_SECTIONS}
276
+ defaultOpenSections={['dashboard']}
277
+ activeHref="/dashboard/quick-start"
278
+ />,
279
+ );
280
+
281
+ const inactiveLink = screen.getByRole('link', { name: 'Resume Session' });
282
+ expect(inactiveLink).not.toHaveAttribute('data-active');
283
+ });
284
+
285
+ it('only the matching href is marked active', () => {
286
+ render(
287
+ <NavigationMenu
288
+ sections={MOCK_SECTIONS}
289
+ defaultOpenSections={['dashboard', 'scenarios']}
290
+ activeHref="/dashboard/progress"
291
+ />,
292
+ );
293
+
294
+ const links = screen.getAllByRole('link');
295
+ const activeLinks = links.filter((l) => l.hasAttribute('aria-current'));
296
+ expect(activeLinks).toHaveLength(1);
297
+ expect(activeLinks[0]).toHaveAccessibleName('Progress');
298
+ });
299
+
300
+ it('no link is marked active when activeHref does not match any item', () => {
301
+ render(
302
+ <NavigationMenu
303
+ sections={MOCK_SECTIONS}
304
+ defaultOpenSections={['dashboard']}
305
+ activeHref="/unmatched/path"
306
+ />,
307
+ );
308
+
309
+ const links = screen.getAllByRole('link');
310
+ for (const link of links) {
311
+ expect(link).not.toHaveAttribute('aria-current');
312
+ }
313
+ });
314
+ });
315
+
316
+ // ── Navigation (onNavigate) ─────────────────────────────────────────────────
317
+
318
+ describe('Navigation', () => {
319
+ it('clicking a nav item calls onNavigate with the correct href', async () => {
320
+ const user = userEvent.setup();
321
+ const onNavigate = vi.fn();
322
+
323
+ render(
324
+ <NavigationMenu
325
+ sections={MOCK_SECTIONS}
326
+ defaultOpenSections={['dashboard']}
327
+ onNavigate={onNavigate}
328
+ />,
329
+ );
330
+
331
+ await user.click(screen.getByRole('link', { name: 'Quick Start' }));
332
+
333
+ expect(onNavigate).toHaveBeenCalledWith('/dashboard/quick-start');
334
+ expect(onNavigate).toHaveBeenCalledTimes(1);
335
+ });
336
+
337
+ it("clicking different items calls onNavigate with each item's href", async () => {
338
+ const user = userEvent.setup();
339
+ const onNavigate = vi.fn();
340
+
341
+ render(
342
+ <NavigationMenu
343
+ sections={MOCK_SECTIONS}
344
+ defaultOpenSections={['dashboard']}
345
+ onNavigate={onNavigate}
346
+ />,
347
+ );
348
+
349
+ await user.click(screen.getByRole('link', { name: 'Quick Start' }));
350
+ await user.click(screen.getByRole('link', { name: 'Resume Session' }));
351
+
352
+ expect(onNavigate).toHaveBeenNthCalledWith(1, '/dashboard/quick-start');
353
+ expect(onNavigate).toHaveBeenNthCalledWith(
354
+ 2,
355
+ '/dashboard/resume-session',
356
+ );
357
+ });
358
+
359
+ it('default link href attribute matches the nav item href', () => {
360
+ render(
361
+ <NavigationMenu
362
+ sections={MOCK_SECTIONS}
363
+ defaultOpenSections={['dashboard']}
364
+ />,
365
+ );
366
+
367
+ const link = screen.getByRole('link', { name: 'Quick Start' });
368
+ expect(link).toHaveAttribute('href', '/dashboard/quick-start');
369
+ });
370
+ });
371
+
372
+ // ── Custom Link Renderer ────────────────────────────────────────────────────
373
+
374
+ describe('Custom Link Renderer', () => {
375
+ it('renders the element returned by renderLink', () => {
376
+ render(
377
+ <NavigationMenu
378
+ sections={MOCK_SECTIONS}
379
+ defaultOpenSections={['dashboard']}
380
+ renderLink={({ href, children, className }) =>
381
+ React.createElement(
382
+ 'a',
383
+ { href, className, 'data-custom': 'true' },
384
+ children,
385
+ )
386
+ }
387
+ />,
388
+ );
389
+
390
+ const link = screen.getByRole('link', { name: 'Quick Start' });
391
+ expect(link).toHaveAttribute('data-custom', 'true');
392
+ });
393
+
394
+ it('passes isActive=true to renderLink for the matching href', () => {
395
+ const renderLink = vi.fn(
396
+ ({
397
+ href,
398
+ children,
399
+ }: {
400
+ href: string;
401
+ children: React.ReactNode;
402
+ isActive: boolean;
403
+ className: string;
404
+ }) => React.createElement('a', { href }, children),
405
+ );
406
+
407
+ render(
408
+ <NavigationMenu
409
+ sections={MOCK_SECTIONS}
410
+ defaultOpenSections={['dashboard']}
411
+ activeHref="/dashboard/quick-start"
412
+ renderLink={renderLink}
413
+ />,
414
+ );
415
+
416
+ const quickStartCall = renderLink.mock.calls.find(
417
+ ([props]) => props.href === '/dashboard/quick-start',
418
+ );
419
+ expect(quickStartCall?.[0].isActive).toBe(true);
420
+ });
421
+
422
+ it('passes isActive=false to renderLink for non-matching hrefs', () => {
423
+ const renderLink = vi.fn(
424
+ ({
425
+ href,
426
+ children,
427
+ }: {
428
+ href: string;
429
+ children: React.ReactNode;
430
+ isActive: boolean;
431
+ className: string;
432
+ }) => React.createElement('a', { href }, children),
433
+ );
434
+
435
+ render(
436
+ <NavigationMenu
437
+ sections={MOCK_SECTIONS}
438
+ defaultOpenSections={['dashboard']}
439
+ activeHref="/dashboard/quick-start"
440
+ renderLink={renderLink}
441
+ />,
442
+ );
443
+
444
+ const resumeCall = renderLink.mock.calls.find(
445
+ ([props]) => props.href === '/dashboard/resume-session',
446
+ );
447
+ expect(resumeCall?.[0].isActive).toBe(false);
448
+ });
449
+
450
+ it('passes the recipe className string to renderLink', () => {
451
+ let receivedClassName = '';
452
+
453
+ render(
454
+ <NavigationMenu
455
+ sections={MOCK_SECTIONS}
456
+ defaultOpenSections={['dashboard']}
457
+ renderLink={({ href, children, className }) => {
458
+ receivedClassName = className;
459
+ return React.createElement('a', { href }, children);
460
+ }}
461
+ />,
462
+ );
463
+
464
+ expect(typeof receivedClassName).toBe('string');
465
+ expect(receivedClassName.length).toBeGreaterThan(0);
466
+ });
467
+ });
468
+
469
+ // ── Accessibility ───────────────────────────────────────────────────────────
470
+
471
+ describe('Accessibility', () => {
472
+ it('passes axe audit with all sections open', async () => {
473
+ const { container } = render(
474
+ <NavigationMenu
475
+ sections={MOCK_SECTIONS}
476
+ defaultOpenSections={['dashboard', 'scenarios', 'help']}
477
+ activeHref="/dashboard/quick-start"
478
+ ariaLabel="Dashboard navigation"
479
+ />,
480
+ );
481
+
482
+ const results = await axe(container);
483
+ expect(results).toHaveNoViolations();
484
+ });
485
+
486
+ it('passes axe audit with all sections closed', async () => {
487
+ const { container } = render(
488
+ <NavigationMenu
489
+ sections={MOCK_SECTIONS}
490
+ defaultOpenSections={[]}
491
+ ariaLabel="Dashboard navigation"
492
+ />,
493
+ );
494
+
495
+ const results = await axe(container);
496
+ expect(results).toHaveNoViolations();
497
+ });
498
+
499
+ it('section triggers are keyboard-focusable buttons', () => {
500
+ render(<NavigationMenu sections={MOCK_SECTIONS} />);
501
+
502
+ const buttons = screen.getAllByRole('button');
503
+ expect(buttons).toHaveLength(MOCK_SECTIONS.length);
504
+ for (const btn of buttons) {
505
+ expect(btn.tagName).toBe('BUTTON');
506
+ }
507
+ });
508
+
509
+ it('nav items are rendered as links', () => {
510
+ render(
511
+ <NavigationMenu
512
+ sections={MOCK_SECTIONS}
513
+ defaultOpenSections={['dashboard']}
514
+ />,
515
+ );
516
+
517
+ const links = screen.getAllByRole('link');
518
+ expect(links.length).toBeGreaterThan(0);
519
+ for (const link of links) {
520
+ expect(link.tagName).toBe('A');
521
+ }
522
+ });
523
+ });
524
+ });
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { Fragment } from 'react';
4
+ import * as Accordion from '../Accordion';
5
+ import { HStack } from 'styled-system/jsx';
6
+ import { navigationMenu } from 'styled-system/recipes';
7
+ import type { NavigationMenuProps } from './types';
8
+
9
+ export function NavigationMenu({
10
+ sections,
11
+ activeHref,
12
+ defaultOpenSections,
13
+ onNavigate,
14
+ renderLink,
15
+ ariaLabel = 'Navigation',
16
+ }: NavigationMenuProps) {
17
+ const styles = navigationMenu();
18
+
19
+ const defaultRenderLink: NavigationMenuProps['renderLink'] = ({
20
+ href,
21
+ children,
22
+ isActive,
23
+ className,
24
+ }) => (
25
+ <a
26
+ href={href}
27
+ aria-current={isActive ? 'page' : undefined}
28
+ data-active={isActive || undefined}
29
+ className={className}
30
+ onClick={(e) => {
31
+ e.preventDefault();
32
+ onNavigate?.(href);
33
+ }}
34
+ >
35
+ {children}
36
+ </a>
37
+ );
38
+
39
+ const resolvedRenderLink = renderLink ?? defaultRenderLink;
40
+
41
+ return (
42
+ <nav className={styles.root} aria-label={ariaLabel}>
43
+ <Accordion.Root
44
+ defaultValue={defaultOpenSections}
45
+ multiple
46
+ variant="plain"
47
+ >
48
+ {sections.map((section) => (
49
+ <Accordion.Item
50
+ key={section.value}
51
+ value={section.value}
52
+ className={styles.section}
53
+ >
54
+ <Accordion.ItemTrigger
55
+ className={styles.sectionTrigger}
56
+ css={{
57
+ // These override accordion recipe via utilities layer (same pattern as ScenarioCard)
58
+ fontSize: 'lg', // 18px — Figma spec (accordion forces 'md'/16px)
59
+ fontWeight: 'medium', // 500 — Figma spec (accordion forces semibold/600)
60
+ borderRadius: 'l3', // 8px — Figma spec (accordion forces l2/6px)
61
+ p: '2', // 8px — Figma: 5px, spacing.2 is closest
62
+ bg: 'surface.container', // Figma: #eeefe3 section header bg
63
+ color: 'onSurface', // Figma: #363636 (accordion forces fg.default)
64
+ }}
65
+ >
66
+ <HStack gap="2">
67
+ <span className={styles.sectionIcon}>{section.icon}</span>
68
+ <span className={styles.sectionTitle}>{section.title}</span>
69
+ </HStack>
70
+ <Accordion.ItemIndicator
71
+ className={styles.sectionIndicator}
72
+ css={{
73
+ color: 'primary.50', // olive green chevrons — Figma: #518500
74
+ }}
75
+ />
76
+ </Accordion.ItemTrigger>
77
+
78
+ <Accordion.ItemContent className={styles.sectionContent}>
79
+ <div className={styles.itemList}>
80
+ {section.items.map((item) => {
81
+ const isActive = activeHref === item.href;
82
+ return (
83
+ <Fragment key={item.href}>
84
+ <div className={styles.item}>
85
+ {resolvedRenderLink({
86
+ href: item.href,
87
+ children: item.label,
88
+ isActive,
89
+ className: styles.itemLink,
90
+ })}
91
+ </div>
92
+ </Fragment>
93
+ );
94
+ })}
95
+ </div>
96
+ </Accordion.ItemContent>
97
+ </Accordion.Item>
98
+ ))}
99
+ </Accordion.Root>
100
+ </nav>
101
+ );
102
+ }
@@ -0,0 +1,2 @@
1
+ export { NavigationMenu } from './NavigationMenu';
2
+ export type { NavigationMenuProps, NavSection, NavItem } from './types';
@@ -0,0 +1,27 @@
1
+ import type React from 'react';
2
+
3
+ export interface NavItem {
4
+ label: string;
5
+ href: string;
6
+ }
7
+
8
+ export interface NavSection {
9
+ value: string;
10
+ title: string;
11
+ icon: React.ReactNode;
12
+ items: NavItem[];
13
+ }
14
+
15
+ export interface NavigationMenuProps {
16
+ sections: NavSection[];
17
+ activeHref?: string;
18
+ defaultOpenSections?: string[];
19
+ onNavigate?: (href: string) => void;
20
+ renderLink?: (props: {
21
+ href: string;
22
+ children: React.ReactNode;
23
+ isActive: boolean;
24
+ className: string;
25
+ }) => React.ReactNode;
26
+ ariaLabel?: string;
27
+ }