@wallarm-org/claude-design 0.0.0-stage → 4.8.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 (963) hide show
  1. package/README.md +114 -2
  2. package/bin/claude-design.js +347 -0
  3. package/dist/SETUP.md +130 -0
  4. package/dist/UPDATE.md +84 -0
  5. package/dist/assets/Icons/activity.svg +1 -0
  6. package/dist/assets/Icons/alt.svg +1 -0
  7. package/dist/assets/Icons/android-neutral.svg +1 -0
  8. package/dist/assets/Icons/android.svg +1 -0
  9. package/dist/assets/Icons/api.svg +1 -0
  10. package/dist/assets/Icons/apple-music-neutral.svg +1 -0
  11. package/dist/assets/Icons/apple-music.svg +1 -0
  12. package/dist/assets/Icons/apple-neutral.svg +1 -0
  13. package/dist/assets/Icons/apple-podcasts-neutral.svg +1 -0
  14. package/dist/assets/Icons/apple-podcasts.svg +1 -0
  15. package/dist/assets/Icons/apple.svg +1 -0
  16. package/dist/assets/Icons/archive.svg +1 -0
  17. package/dist/assets/Icons/arrow-big-up-dash.svg +1 -0
  18. package/dist/assets/Icons/arrow-big-up.svg +1 -0
  19. package/dist/assets/Icons/arrow-down-left.svg +1 -0
  20. package/dist/assets/Icons/arrow-down-right.svg +1 -0
  21. package/dist/assets/Icons/arrow-down.svg +1 -0
  22. package/dist/assets/Icons/arrow-left.svg +1 -0
  23. package/dist/assets/Icons/arrow-right-alt.svg +1 -0
  24. package/dist/assets/Icons/arrow-right.svg +1 -0
  25. package/dist/assets/Icons/arrow-up-down.svg +1 -0
  26. package/dist/assets/Icons/arrow-up-left.svg +1 -0
  27. package/dist/assets/Icons/arrow-up-right.svg +1 -0
  28. package/dist/assets/Icons/arrow-up.svg +1 -0
  29. package/dist/assets/Icons/artstation-neutral.svg +1 -0
  30. package/dist/assets/Icons/artstation.svg +1 -0
  31. package/dist/assets/Icons/asterisk.svg +1 -0
  32. package/dist/assets/Icons/audio-waveform.svg +1 -0
  33. package/dist/assets/Icons/aws-neutral.svg +1 -0
  34. package/dist/assets/Icons/aws.svg +1 -0
  35. package/dist/assets/Icons/badge-check.svg +1 -0
  36. package/dist/assets/Icons/baidu-neutral.svg +1 -0
  37. package/dist/assets/Icons/baidu.svg +1 -0
  38. package/dist/assets/Icons/bandage.svg +1 -0
  39. package/dist/assets/Icons/behance-neutral.svg +1 -0
  40. package/dist/assets/Icons/behance.svg +1 -0
  41. package/dist/assets/Icons/bell-cog.svg +1 -0
  42. package/dist/assets/Icons/bell.svg +1 -0
  43. package/dist/assets/Icons/binary.svg +1 -0
  44. package/dist/assets/Icons/book-open.svg +1 -0
  45. package/dist/assets/Icons/book.svg +1 -0
  46. package/dist/assets/Icons/boosty-neutral.svg +1 -0
  47. package/dist/assets/Icons/boosty.svg +1 -0
  48. package/dist/assets/Icons/bot.svg +1 -0
  49. package/dist/assets/Icons/brain.svg +1 -0
  50. package/dist/assets/Icons/braze-neutral.svg +1 -0
  51. package/dist/assets/Icons/braze.svg +1 -0
  52. package/dist/assets/Icons/brevo-neutral.svg +1 -0
  53. package/dist/assets/Icons/brevo.svg +1 -0
  54. package/dist/assets/Icons/bug-off.svg +1 -0
  55. package/dist/assets/Icons/bug-play.svg +1 -0
  56. package/dist/assets/Icons/bug.svg +1 -0
  57. package/dist/assets/Icons/building.svg +1 -0
  58. package/dist/assets/Icons/building2.svg +1 -0
  59. package/dist/assets/Icons/calendar-check.svg +1 -0
  60. package/dist/assets/Icons/calendar-check2.svg +1 -0
  61. package/dist/assets/Icons/calendar-clock.svg +1 -0
  62. package/dist/assets/Icons/calendar-cog.svg +1 -0
  63. package/dist/assets/Icons/calendar-days.svg +1 -0
  64. package/dist/assets/Icons/calendar-fold.svg +1 -0
  65. package/dist/assets/Icons/calendar-heart.svg +1 -0
  66. package/dist/assets/Icons/calendar-minus.svg +1 -0
  67. package/dist/assets/Icons/calendar-minus2.svg +1 -0
  68. package/dist/assets/Icons/calendar-off.svg +1 -0
  69. package/dist/assets/Icons/calendar-plus.svg +1 -0
  70. package/dist/assets/Icons/calendar-plus2.svg +1 -0
  71. package/dist/assets/Icons/calendar-range.svg +1 -0
  72. package/dist/assets/Icons/calendar-search.svg +1 -0
  73. package/dist/assets/Icons/calendar-x.svg +1 -0
  74. package/dist/assets/Icons/calendar-x2.svg +1 -0
  75. package/dist/assets/Icons/calendar.svg +1 -0
  76. package/dist/assets/Icons/chart-area.svg +1 -0
  77. package/dist/assets/Icons/chart-bar.svg +1 -0
  78. package/dist/assets/Icons/chart-column.svg +1 -0
  79. package/dist/assets/Icons/chart-no-axes-column.svg +1 -0
  80. package/dist/assets/Icons/check-check.svg +1 -0
  81. package/dist/assets/Icons/check.svg +1 -0
  82. package/dist/assets/Icons/checkbox-indeterminate.svg +1 -0
  83. package/dist/assets/Icons/chevron-down.svg +1 -0
  84. package/dist/assets/Icons/chevron-first.svg +1 -0
  85. package/dist/assets/Icons/chevron-last.svg +1 -0
  86. package/dist/assets/Icons/chevron-left.svg +1 -0
  87. package/dist/assets/Icons/chevron-right.svg +1 -0
  88. package/dist/assets/Icons/chevron-up-down.svg +1 -0
  89. package/dist/assets/Icons/chevron-up.svg +1 -0
  90. package/dist/assets/Icons/chevrons-down-up.svg +1 -0
  91. package/dist/assets/Icons/chevrons-down.svg +1 -0
  92. package/dist/assets/Icons/chevrons-left-right.svg +1 -0
  93. package/dist/assets/Icons/chevrons-left.svg +1 -0
  94. package/dist/assets/Icons/chevrons-right-left.svg +1 -0
  95. package/dist/assets/Icons/chevrons-right.svg +1 -0
  96. package/dist/assets/Icons/chevrons-up-down.svg +1 -0
  97. package/dist/assets/Icons/chevrons-up.svg +1 -0
  98. package/dist/assets/Icons/circle-arrow-down.svg +1 -0
  99. package/dist/assets/Icons/circle-arrow-left.svg +1 -0
  100. package/dist/assets/Icons/circle-arrow-out-down-left.svg +1 -0
  101. package/dist/assets/Icons/circle-arrow-out-down-right.svg +1 -0
  102. package/dist/assets/Icons/circle-arrow-out-up-left.svg +1 -0
  103. package/dist/assets/Icons/circle-arrow-out-up-right.svg +1 -0
  104. package/dist/assets/Icons/circle-arrow-right.svg +1 -0
  105. package/dist/assets/Icons/circle-arrow-up.svg +1 -0
  106. package/dist/assets/Icons/circle-check-big.svg +1 -0
  107. package/dist/assets/Icons/circle-check.svg +1 -0
  108. package/dist/assets/Icons/circle-chevron-down.svg +1 -0
  109. package/dist/assets/Icons/circle-chevron-left.svg +1 -0
  110. package/dist/assets/Icons/circle-chevron-right.svg +1 -0
  111. package/dist/assets/Icons/circle-chevron-up.svg +1 -0
  112. package/dist/assets/Icons/circle-dashed.svg +1 -0
  113. package/dist/assets/Icons/circle-dot-dashed.svg +1 -0
  114. package/dist/assets/Icons/circle-ellipsis.svg +1 -0
  115. package/dist/assets/Icons/circle-gauge.svg +1 -0
  116. package/dist/assets/Icons/circle-help.svg +1 -0
  117. package/dist/assets/Icons/circle-minus.svg +1 -0
  118. package/dist/assets/Icons/circle-plus.svg +1 -0
  119. package/dist/assets/Icons/circle-slash.svg +1 -0
  120. package/dist/assets/Icons/circle-slash2.svg +1 -0
  121. package/dist/assets/Icons/circle-stop.svg +1 -0
  122. package/dist/assets/Icons/circle.svg +1 -0
  123. package/dist/assets/Icons/clipboard-check.svg +1 -0
  124. package/dist/assets/Icons/clipboard-x.svg +1 -0
  125. package/dist/assets/Icons/clipboard.svg +1 -0
  126. package/dist/assets/Icons/clock-fading.svg +1 -0
  127. package/dist/assets/Icons/clock.svg +1 -0
  128. package/dist/assets/Icons/cloud-off.svg +1 -0
  129. package/dist/assets/Icons/cloud-sync.svg +1 -0
  130. package/dist/assets/Icons/cloud.svg +1 -0
  131. package/dist/assets/Icons/code.svg +1 -0
  132. package/dist/assets/Icons/collapse.svg +1 -0
  133. package/dist/assets/Icons/columns3-cog.svg +1 -0
  134. package/dist/assets/Icons/command.svg +1 -0
  135. package/dist/assets/Icons/compass.svg +1 -0
  136. package/dist/assets/Icons/copy-slash.svg +1 -0
  137. package/dist/assets/Icons/copy.svg +1 -0
  138. package/dist/assets/Icons/corner-down-left.svg +1 -0
  139. package/dist/assets/Icons/corner-down-right.svg +1 -0
  140. package/dist/assets/Icons/corner-left-down.svg +1 -0
  141. package/dist/assets/Icons/corner-left-up.svg +1 -0
  142. package/dist/assets/Icons/corner-right-down.svg +1 -0
  143. package/dist/assets/Icons/corner-right-up.svg +1 -0
  144. package/dist/assets/Icons/corner-up-left.svg +1 -0
  145. package/dist/assets/Icons/corner-up-right.svg +1 -0
  146. package/dist/assets/Icons/cpu.svg +1 -0
  147. package/dist/assets/Icons/credit-card.svg +1 -0
  148. package/dist/assets/Icons/crosshair.svg +1 -0
  149. package/dist/assets/Icons/ctrl.svg +1 -0
  150. package/dist/assets/Icons/database.svg +1 -0
  151. package/dist/assets/Icons/devianart-neutral.svg +1 -0
  152. package/dist/assets/Icons/devianart.svg +1 -0
  153. package/dist/assets/Icons/discord-neutral.svg +1 -0
  154. package/dist/assets/Icons/discord.svg +1 -0
  155. package/dist/assets/Icons/dollar-sign.svg +1 -0
  156. package/dist/assets/Icons/dot.svg +1 -0
  157. package/dist/assets/Icons/download.svg +1 -0
  158. package/dist/assets/Icons/dprofile-neutral.svg +1 -0
  159. package/dist/assets/Icons/dprofile.svg +1 -0
  160. package/dist/assets/Icons/dribbble-neutral.svg +1 -0
  161. package/dist/assets/Icons/dribbble.svg +1 -0
  162. package/dist/assets/Icons/droplet.svg +1 -0
  163. package/dist/assets/Icons/duplicate.svg +1 -0
  164. package/dist/assets/Icons/dzen-neutral.svg +1 -0
  165. package/dist/assets/Icons/dzen.svg +1 -0
  166. package/dist/assets/Icons/earth-lock.svg +1 -0
  167. package/dist/assets/Icons/earth.svg +1 -0
  168. package/dist/assets/Icons/ellipsis.svg +1 -0
  169. package/dist/assets/Icons/expand.svg +1 -0
  170. package/dist/assets/Icons/external-link.svg +1 -0
  171. package/dist/assets/Icons/eye-off.svg +1 -0
  172. package/dist/assets/Icons/eye.svg +1 -0
  173. package/dist/assets/Icons/facebook-neutral.svg +1 -0
  174. package/dist/assets/Icons/facebook.svg +1 -0
  175. package/dist/assets/Icons/fast-forward.svg +1 -0
  176. package/dist/assets/Icons/figma-neutral.svg +1 -0
  177. package/dist/assets/Icons/figma.svg +1 -0
  178. package/dist/assets/Icons/file-down.svg +1 -0
  179. package/dist/assets/Icons/file-plus.svg +1 -0
  180. package/dist/assets/Icons/file-text.svg +1 -0
  181. package/dist/assets/Icons/file.svg +1 -0
  182. package/dist/assets/Icons/film.svg +1 -0
  183. package/dist/assets/Icons/filter-x.svg +1 -0
  184. package/dist/assets/Icons/filter.svg +1 -0
  185. package/dist/assets/Icons/fingerprint-pattern.svg +1 -0
  186. package/dist/assets/Icons/flag.svg +1 -0
  187. package/dist/assets/Icons/flask-conical.svg +1 -0
  188. package/dist/assets/Icons/folder.svg +1 -0
  189. package/dist/assets/Icons/funnel-plus.svg +1 -0
  190. package/dist/assets/Icons/ghost.svg +1 -0
  191. package/dist/assets/Icons/git-branch.svg +1 -0
  192. package/dist/assets/Icons/git-commit-horizontal.svg +1 -0
  193. package/dist/assets/Icons/git-commit-vertical.svg +1 -0
  194. package/dist/assets/Icons/git-compare-arrows-alt.svg +1 -0
  195. package/dist/assets/Icons/git-compare-arrows.svg +1 -0
  196. package/dist/assets/Icons/git-compare.svg +1 -0
  197. package/dist/assets/Icons/git-fork.svg +1 -0
  198. package/dist/assets/Icons/git-graph.svg +1 -0
  199. package/dist/assets/Icons/git-merge.svg +1 -0
  200. package/dist/assets/Icons/git-pull-request-arrow.svg +1 -0
  201. package/dist/assets/Icons/git-pull-request-closed.svg +1 -0
  202. package/dist/assets/Icons/git-pull-request-create-arrow.svg +1 -0
  203. package/dist/assets/Icons/git-pull-request-create.svg +1 -0
  204. package/dist/assets/Icons/git-pull-request-draft.svg +1 -0
  205. package/dist/assets/Icons/git-pull-request.svg +1 -0
  206. package/dist/assets/Icons/github-neutral.svg +1 -0
  207. package/dist/assets/Icons/github.svg +1 -0
  208. package/dist/assets/Icons/globe-lock.svg +1 -0
  209. package/dist/assets/Icons/globe.svg +1 -0
  210. package/dist/assets/Icons/gmail-neutral.svg +1 -0
  211. package/dist/assets/Icons/gmail.svg +1 -0
  212. package/dist/assets/Icons/google-meet-neutral.svg +1 -0
  213. package/dist/assets/Icons/google-meet.svg +1 -0
  214. package/dist/assets/Icons/google-neutral.svg +1 -0
  215. package/dist/assets/Icons/google-play-neutral.svg +1 -0
  216. package/dist/assets/Icons/google-play.svg +1 -0
  217. package/dist/assets/Icons/google-podcast-neutral.svg +1 -0
  218. package/dist/assets/Icons/google-podcast.svg +1 -0
  219. package/dist/assets/Icons/google.svg +1 -0
  220. package/dist/assets/Icons/grip-horizontal.svg +1 -0
  221. package/dist/assets/Icons/grip-vertical.svg +1 -0
  222. package/dist/assets/Icons/hammer.svg +1 -0
  223. package/dist/assets/Icons/heart-pulse.svg +1 -0
  224. package/dist/assets/Icons/history.svg +1 -0
  225. package/dist/assets/Icons/home.svg +1 -0
  226. package/dist/assets/Icons/imo-neutral.svg +1 -0
  227. package/dist/assets/Icons/imo.svg +1 -0
  228. package/dist/assets/Icons/infinity.svg +1 -0
  229. package/dist/assets/Icons/info.svg +1 -0
  230. package/dist/assets/Icons/infobip-neutral.svg +1 -0
  231. package/dist/assets/Icons/infobip.svg +1 -0
  232. package/dist/assets/Icons/instagram-neutral.svg +1 -0
  233. package/dist/assets/Icons/instagram.svg +1 -0
  234. package/dist/assets/Icons/key-round.svg +1 -0
  235. package/dist/assets/Icons/key-square.svg +1 -0
  236. package/dist/assets/Icons/keyboard.svg +1 -0
  237. package/dist/assets/Icons/kickstarter-neutral.svg +1 -0
  238. package/dist/assets/Icons/kickstarter.svg +1 -0
  239. package/dist/assets/Icons/laptop.svg +1 -0
  240. package/dist/assets/Icons/layers.svg +1 -0
  241. package/dist/assets/Icons/layers2.svg +1 -0
  242. package/dist/assets/Icons/layers3.svg +1 -0
  243. package/dist/assets/Icons/layout-dashboard.svg +1 -0
  244. package/dist/assets/Icons/layout-grid.svg +1 -0
  245. package/dist/assets/Icons/layout-list.svg +1 -0
  246. package/dist/assets/Icons/layout-panel-left.svg +1 -0
  247. package/dist/assets/Icons/layout-panel-top.svg +1 -0
  248. package/dist/assets/Icons/layout-template.svg +1 -0
  249. package/dist/assets/Icons/library-big.svg +1 -0
  250. package/dist/assets/Icons/life-buoy.svg +1 -0
  251. package/dist/assets/Icons/lightbulb-off.svg +1 -0
  252. package/dist/assets/Icons/lightbulb.svg +1 -0
  253. package/dist/assets/Icons/line-neutral.svg +1 -0
  254. package/dist/assets/Icons/line.svg +1 -0
  255. package/dist/assets/Icons/link.svg +1 -0
  256. package/dist/assets/Icons/link2-off.svg +1 -0
  257. package/dist/assets/Icons/link2.svg +1 -0
  258. package/dist/assets/Icons/linkedin-neutral.svg +1 -0
  259. package/dist/assets/Icons/linkedin.svg +1 -0
  260. package/dist/assets/Icons/list-checks.svg +1 -0
  261. package/dist/assets/Icons/list-collapse.svg +1 -0
  262. package/dist/assets/Icons/list-filter.svg +1 -0
  263. package/dist/assets/Icons/list-minus.svg +1 -0
  264. package/dist/assets/Icons/list-ordered.svg +1 -0
  265. package/dist/assets/Icons/list-plus.svg +1 -0
  266. package/dist/assets/Icons/list-restart.svg +1 -0
  267. package/dist/assets/Icons/list-start.svg +1 -0
  268. package/dist/assets/Icons/list-todo.svg +1 -0
  269. package/dist/assets/Icons/list-tree.svg +1 -0
  270. package/dist/assets/Icons/list-video.svg +1 -0
  271. package/dist/assets/Icons/list-x.svg +1 -0
  272. package/dist/assets/Icons/list.svg +1 -0
  273. package/dist/assets/Icons/loader-circle.svg +1 -0
  274. package/dist/assets/Icons/loader.svg +1 -0
  275. package/dist/assets/Icons/lock-open.svg +1 -0
  276. package/dist/assets/Icons/lock.svg +1 -0
  277. package/dist/assets/Icons/log-in.svg +1 -0
  278. package/dist/assets/Icons/log-out.svg +1 -0
  279. package/dist/assets/Icons/mail-check.svg +1 -0
  280. package/dist/assets/Icons/mail-minus.svg +1 -0
  281. package/dist/assets/Icons/mail-open.svg +1 -0
  282. package/dist/assets/Icons/mail-plus.svg +1 -0
  283. package/dist/assets/Icons/mail-question.svg +1 -0
  284. package/dist/assets/Icons/mail-search.svg +1 -0
  285. package/dist/assets/Icons/mail-warning.svg +1 -0
  286. package/dist/assets/Icons/mail-x.svg +1 -0
  287. package/dist/assets/Icons/mail.svg +1 -0
  288. package/dist/assets/Icons/mailbox.svg +1 -0
  289. package/dist/assets/Icons/mailersend-neutral.svg +1 -0
  290. package/dist/assets/Icons/mailersend.svg +1 -0
  291. package/dist/assets/Icons/mailgun-neutral.svg +1 -0
  292. package/dist/assets/Icons/mailgun.svg +1 -0
  293. package/dist/assets/Icons/mailjet-neutral.svg +1 -0
  294. package/dist/assets/Icons/mailjet.svg +1 -0
  295. package/dist/assets/Icons/mails.svg +1 -0
  296. package/dist/assets/Icons/mailtrap-neutral.svg +1 -0
  297. package/dist/assets/Icons/mailtrap.svg +1 -0
  298. package/dist/assets/Icons/mandrill-neutral.svg +1 -0
  299. package/dist/assets/Icons/mandrill.svg +1 -0
  300. package/dist/assets/Icons/map-pin.svg +1 -0
  301. package/dist/assets/Icons/maqsam-neutral.svg +1 -0
  302. package/dist/assets/Icons/maqsam.svg +1 -0
  303. package/dist/assets/Icons/mattermost-neutral.svg +1 -0
  304. package/dist/assets/Icons/mattermost.svg +1 -0
  305. package/dist/assets/Icons/maximize.svg +1 -0
  306. package/dist/assets/Icons/maximize2.svg +1 -0
  307. package/dist/assets/Icons/medium-neutral.svg +1 -0
  308. package/dist/assets/Icons/medium.svg +1 -0
  309. package/dist/assets/Icons/megaphone-off.svg +1 -0
  310. package/dist/assets/Icons/megaphone.svg +1 -0
  311. package/dist/assets/Icons/menu.svg +1 -0
  312. package/dist/assets/Icons/merge.svg +1 -0
  313. package/dist/assets/Icons/message-square-code.svg +1 -0
  314. package/dist/assets/Icons/message-square-dashed.svg +1 -0
  315. package/dist/assets/Icons/message-square-diff.svg +1 -0
  316. package/dist/assets/Icons/message-square-dot.svg +1 -0
  317. package/dist/assets/Icons/message-square-heart.svg +1 -0
  318. package/dist/assets/Icons/message-square-off.svg +1 -0
  319. package/dist/assets/Icons/message-square-plus.svg +1 -0
  320. package/dist/assets/Icons/message-square-quote.svg +1 -0
  321. package/dist/assets/Icons/message-square-reply.svg +1 -0
  322. package/dist/assets/Icons/message-square-share.svg +1 -0
  323. package/dist/assets/Icons/message-square-text.svg +1 -0
  324. package/dist/assets/Icons/message-square-warning.svg +1 -0
  325. package/dist/assets/Icons/message-square-x.svg +1 -0
  326. package/dist/assets/Icons/message-square.svg +1 -0
  327. package/dist/assets/Icons/messages-square.svg +1 -0
  328. package/dist/assets/Icons/messenger-neutral.svg +1 -0
  329. package/dist/assets/Icons/messenger.svg +1 -0
  330. package/dist/assets/Icons/microsoft-neutral.svg +1 -0
  331. package/dist/assets/Icons/microsoft-teams-neutral.svg +1 -0
  332. package/dist/assets/Icons/microsoft-teams.svg +1 -0
  333. package/dist/assets/Icons/microsoft.svg +1 -0
  334. package/dist/assets/Icons/minimize.svg +1 -0
  335. package/dist/assets/Icons/minimize2.svg +1 -0
  336. package/dist/assets/Icons/minus.svg +1 -0
  337. package/dist/assets/Icons/moon.svg +1 -0
  338. package/dist/assets/Icons/mouse-pointer-ban.svg +1 -0
  339. package/dist/assets/Icons/mouse-pointer-click.svg +1 -0
  340. package/dist/assets/Icons/mouse-pointer.svg +1 -0
  341. package/dist/assets/Icons/mouse-pointer2.svg +1 -0
  342. package/dist/assets/Icons/mouse.svg +1 -0
  343. package/dist/assets/Icons/move-diagonal.svg +1 -0
  344. package/dist/assets/Icons/move-diagonal2.svg +1 -0
  345. package/dist/assets/Icons/move-down-left.svg +1 -0
  346. package/dist/assets/Icons/move-down-right.svg +1 -0
  347. package/dist/assets/Icons/move-down.svg +1 -0
  348. package/dist/assets/Icons/move-horizontal.svg +1 -0
  349. package/dist/assets/Icons/move-left.svg +1 -0
  350. package/dist/assets/Icons/move-right.svg +1 -0
  351. package/dist/assets/Icons/move-up-left.svg +1 -0
  352. package/dist/assets/Icons/move-up-right.svg +1 -0
  353. package/dist/assets/Icons/move-up.svg +1 -0
  354. package/dist/assets/Icons/move-vertical.svg +1 -0
  355. package/dist/assets/Icons/move3-d.svg +1 -0
  356. package/dist/assets/Icons/netcore-neutral.svg +1 -0
  357. package/dist/assets/Icons/netcore.svg +1 -0
  358. package/dist/assets/Icons/notebook-pen.svg +1 -0
  359. package/dist/assets/Icons/notebook-tabs.svg +1 -0
  360. package/dist/assets/Icons/notebook-text.svg +1 -0
  361. package/dist/assets/Icons/notebook.svg +1 -0
  362. package/dist/assets/Icons/notepad-text-dashed.svg +1 -0
  363. package/dist/assets/Icons/notepad-text.svg +1 -0
  364. package/dist/assets/Icons/notion-neutral.svg +1 -0
  365. package/dist/assets/Icons/notion.svg +1 -0
  366. package/dist/assets/Icons/octagon-alert.svg +1 -0
  367. package/dist/assets/Icons/octagon-pause.svg +1 -0
  368. package/dist/assets/Icons/octagon-x.svg +1 -0
  369. package/dist/assets/Icons/octagon.svg +1 -0
  370. package/dist/assets/Icons/ok-neutral.svg +1 -0
  371. package/dist/assets/Icons/ok-only-sign-neutral.svg +1 -0
  372. package/dist/assets/Icons/ok-only-sign.svg +1 -0
  373. package/dist/assets/Icons/ok.svg +1 -0
  374. package/dist/assets/Icons/onlyfans-neutral.svg +1 -0
  375. package/dist/assets/Icons/onlyfans.svg +1 -0
  376. package/dist/assets/Icons/option.svg +1 -0
  377. package/dist/assets/Icons/outlook365-neutral.svg +1 -0
  378. package/dist/assets/Icons/outlook365.svg +1 -0
  379. package/dist/assets/Icons/paintbrush.svg +1 -0
  380. package/dist/assets/Icons/panel-bottom-open.svg +1 -0
  381. package/dist/assets/Icons/panel-bottom.svg +1 -0
  382. package/dist/assets/Icons/panel-left-close.svg +1 -0
  383. package/dist/assets/Icons/panel-left-dashed.svg +1 -0
  384. package/dist/assets/Icons/panel-left-open.svg +1 -0
  385. package/dist/assets/Icons/panel-left.svg +1 -0
  386. package/dist/assets/Icons/panel-right-animated.svg +1 -0
  387. package/dist/assets/Icons/panel-right-close.svg +1 -0
  388. package/dist/assets/Icons/panel-right-dashed.svg +1 -0
  389. package/dist/assets/Icons/panel-right-open.svg +1 -0
  390. package/dist/assets/Icons/panel-right.svg +1 -0
  391. package/dist/assets/Icons/panel-top-bottom-dashed.svg +1 -0
  392. package/dist/assets/Icons/panel-top-close.svg +1 -0
  393. package/dist/assets/Icons/panel-top-open.svg +1 -0
  394. package/dist/assets/Icons/panel-top.svg +1 -0
  395. package/dist/assets/Icons/panels-left-bottom.svg +1 -0
  396. package/dist/assets/Icons/panels-right-bottom.svg +1 -0
  397. package/dist/assets/Icons/panels-top-left.svg +1 -0
  398. package/dist/assets/Icons/paperclip.svg +1 -0
  399. package/dist/assets/Icons/patreon-neutral.svg +1 -0
  400. package/dist/assets/Icons/patreon.svg +1 -0
  401. package/dist/assets/Icons/paypal-neutral.svg +1 -0
  402. package/dist/assets/Icons/paypal.svg +1 -0
  403. package/dist/assets/Icons/pen-line.svg +1 -0
  404. package/dist/assets/Icons/pen-off.svg +1 -0
  405. package/dist/assets/Icons/pen-tool.svg +1 -0
  406. package/dist/assets/Icons/pen.svg +1 -0
  407. package/dist/assets/Icons/pencil-line.svg +1 -0
  408. package/dist/assets/Icons/pencil-off.svg +1 -0
  409. package/dist/assets/Icons/pencil-ruler.svg +1 -0
  410. package/dist/assets/Icons/pencil.svg +1 -0
  411. package/dist/assets/Icons/pentagon.svg +1 -0
  412. package/dist/assets/Icons/percentage.svg +1 -0
  413. package/dist/assets/Icons/phone.svg +1 -0
  414. package/dist/assets/Icons/pin-off.svg +1 -0
  415. package/dist/assets/Icons/pin.svg +1 -0
  416. package/dist/assets/Icons/pinterest-neutral.svg +1 -0
  417. package/dist/assets/Icons/pinterest.svg +1 -0
  418. package/dist/assets/Icons/play.svg +1 -0
  419. package/dist/assets/Icons/plug.svg +1 -0
  420. package/dist/assets/Icons/plunk-neutral.svg +1 -0
  421. package/dist/assets/Icons/plunk.svg +1 -0
  422. package/dist/assets/Icons/plus.svg +1 -0
  423. package/dist/assets/Icons/postmark-neutral.svg +1 -0
  424. package/dist/assets/Icons/postmark.svg +1 -0
  425. package/dist/assets/Icons/product-hunt-neutral.svg +1 -0
  426. package/dist/assets/Icons/product-hunt.svg +1 -0
  427. package/dist/assets/Icons/quora-neutral.svg +1 -0
  428. package/dist/assets/Icons/quora.svg +1 -0
  429. package/dist/assets/Icons/quote.svg +1 -0
  430. package/dist/assets/Icons/radar.svg +1 -0
  431. package/dist/assets/Icons/reddit-neutral.svg +1 -0
  432. package/dist/assets/Icons/reddit.svg +1 -0
  433. package/dist/assets/Icons/redo-dot.svg +1 -0
  434. package/dist/assets/Icons/redo.svg +1 -0
  435. package/dist/assets/Icons/redo2.svg +1 -0
  436. package/dist/assets/Icons/refresh-ccw.svg +1 -0
  437. package/dist/assets/Icons/refresh-cw-off.svg +1 -0
  438. package/dist/assets/Icons/resend-neutral.svg +1 -0
  439. package/dist/assets/Icons/resend.svg +1 -0
  440. package/dist/assets/Icons/rss.svg +1 -0
  441. package/dist/assets/Icons/rules.svg +1 -0
  442. package/dist/assets/Icons/scan-line.svg +1 -0
  443. package/dist/assets/Icons/scroll-text.svg +1 -0
  444. package/dist/assets/Icons/search-hint.svg +1 -0
  445. package/dist/assets/Icons/search-x.svg +1 -0
  446. package/dist/assets/Icons/search.svg +1 -0
  447. package/dist/assets/Icons/sendgrid-neutral.svg +1 -0
  448. package/dist/assets/Icons/sendgrid.svg +1 -0
  449. package/dist/assets/Icons/server.svg +1 -0
  450. package/dist/assets/Icons/ses-neutral.svg +1 -0
  451. package/dist/assets/Icons/ses.svg +1 -0
  452. package/dist/assets/Icons/settings.svg +1 -0
  453. package/dist/assets/Icons/settings2.svg +1 -0
  454. package/dist/assets/Icons/shapes.svg +1 -0
  455. package/dist/assets/Icons/share.svg +1 -0
  456. package/dist/assets/Icons/sheet.svg +1 -0
  457. package/dist/assets/Icons/shield-alert.svg +1 -0
  458. package/dist/assets/Icons/shield-ban.svg +1 -0
  459. package/dist/assets/Icons/shield-check.svg +1 -0
  460. package/dist/assets/Icons/shield-ellipsis.svg +1 -0
  461. package/dist/assets/Icons/shield-half.svg +1 -0
  462. package/dist/assets/Icons/shield-minus.svg +1 -0
  463. package/dist/assets/Icons/shield-off.svg +1 -0
  464. package/dist/assets/Icons/shield-plus.svg +1 -0
  465. package/dist/assets/Icons/shield-question.svg +1 -0
  466. package/dist/assets/Icons/shield-user.svg +1 -0
  467. package/dist/assets/Icons/shield-x.svg +1 -0
  468. package/dist/assets/Icons/shield.svg +1 -0
  469. package/dist/assets/Icons/shift.svg +1 -0
  470. package/dist/assets/Icons/signal-neutral.svg +1 -0
  471. package/dist/assets/Icons/signal.svg +1 -0
  472. package/dist/assets/Icons/signpost.svg +1 -0
  473. package/dist/assets/Icons/sina-weibo-neutral.svg +1 -0
  474. package/dist/assets/Icons/sina-weibo.svg +1 -0
  475. package/dist/assets/Icons/skull.svg +1 -0
  476. package/dist/assets/Icons/skype-neutral.svg +1 -0
  477. package/dist/assets/Icons/skype.svg +1 -0
  478. package/dist/assets/Icons/slack-neutral.svg +1 -0
  479. package/dist/assets/Icons/slack.svg +1 -0
  480. package/dist/assets/Icons/sliders-horizontal.svg +1 -0
  481. package/dist/assets/Icons/sliders-vertical.svg +1 -0
  482. package/dist/assets/Icons/smtp-neutral.svg +1 -0
  483. package/dist/assets/Icons/smtp.svg +1 -0
  484. package/dist/assets/Icons/snapchat-neutral.svg +1 -0
  485. package/dist/assets/Icons/snapchat.svg +1 -0
  486. package/dist/assets/Icons/soundcloud-neutral.svg +1 -0
  487. package/dist/assets/Icons/soundcloud.svg +1 -0
  488. package/dist/assets/Icons/space.svg +1 -0
  489. package/dist/assets/Icons/sparkpost-neutral.svg +1 -0
  490. package/dist/assets/Icons/sparkpost.svg +1 -0
  491. package/dist/assets/Icons/speed.svg +1 -0
  492. package/dist/assets/Icons/split.svg +1 -0
  493. package/dist/assets/Icons/spotify-neutral.svg +1 -0
  494. package/dist/assets/Icons/spotify.svg +1 -0
  495. package/dist/assets/Icons/square-arrow-out-up-right.svg +1 -0
  496. package/dist/assets/Icons/square-minus.svg +1 -0
  497. package/dist/assets/Icons/square-pen.svg +1 -0
  498. package/dist/assets/Icons/square-plus.svg +1 -0
  499. package/dist/assets/Icons/square-user-round.svg +1 -0
  500. package/dist/assets/Icons/stack-overflow-neutral.svg +1 -0
  501. package/dist/assets/Icons/stack-overflow.svg +1 -0
  502. package/dist/assets/Icons/star.svg +1 -0
  503. package/dist/assets/Icons/sun.svg +1 -0
  504. package/dist/assets/Icons/syringe.svg +1 -0
  505. package/dist/assets/Icons/tag.svg +1 -0
  506. package/dist/assets/Icons/target.svg +1 -0
  507. package/dist/assets/Icons/teams-neutral.svg +1 -0
  508. package/dist/assets/Icons/teams.svg +1 -0
  509. package/dist/assets/Icons/telegram-neutral.svg +1 -0
  510. package/dist/assets/Icons/telegram-only-sign-neutral.svg +1 -0
  511. package/dist/assets/Icons/telegram-only-sign.svg +1 -0
  512. package/dist/assets/Icons/telegram.svg +1 -0
  513. package/dist/assets/Icons/terminal.svg +1 -0
  514. package/dist/assets/Icons/test-policies.svg +1 -0
  515. package/dist/assets/Icons/threads-neutral.svg +1 -0
  516. package/dist/assets/Icons/threads.svg +1 -0
  517. package/dist/assets/Icons/thumbs-down.svg +1 -0
  518. package/dist/assets/Icons/thumbs-up.svg +1 -0
  519. package/dist/assets/Icons/tiktok-neutral.svg +1 -0
  520. package/dist/assets/Icons/tiktok.svg +1 -0
  521. package/dist/assets/Icons/tinder-neutral.svg +1 -0
  522. package/dist/assets/Icons/tinder.svg +1 -0
  523. package/dist/assets/Icons/trash.svg +1 -0
  524. package/dist/assets/Icons/trash2.svg +1 -0
  525. package/dist/assets/Icons/triangle-alert.svg +1 -0
  526. package/dist/assets/Icons/tumblr-neutral.svg +1 -0
  527. package/dist/assets/Icons/tumblr.svg +1 -0
  528. package/dist/assets/Icons/twitch-neutral.svg +1 -0
  529. package/dist/assets/Icons/twitch.svg +1 -0
  530. package/dist/assets/Icons/undo-dot.svg +1 -0
  531. package/dist/assets/Icons/undo.svg +1 -0
  532. package/dist/assets/Icons/undo2.svg +1 -0
  533. package/dist/assets/Icons/unplug.svg +1 -0
  534. package/dist/assets/Icons/user-check.svg +1 -0
  535. package/dist/assets/Icons/user-minus.svg +1 -0
  536. package/dist/assets/Icons/user-plus.svg +1 -0
  537. package/dist/assets/Icons/user-round-x.svg +1 -0
  538. package/dist/assets/Icons/user-round.svg +1 -0
  539. package/dist/assets/Icons/user.svg +1 -0
  540. package/dist/assets/Icons/username.svg +1 -0
  541. package/dist/assets/Icons/users-round.svg +1 -0
  542. package/dist/assets/Icons/users.svg +1 -0
  543. package/dist/assets/Icons/viber-neutral.svg +1 -0
  544. package/dist/assets/Icons/viber.svg +1 -0
  545. package/dist/assets/Icons/video.svg +1 -0
  546. package/dist/assets/Icons/vimeo-neutral.svg +1 -0
  547. package/dist/assets/Icons/vimeo.svg +1 -0
  548. package/dist/assets/Icons/vk-music-neutral.svg +1 -0
  549. package/dist/assets/Icons/vk-music.svg +1 -0
  550. package/dist/assets/Icons/vk-neutral.svg +1 -0
  551. package/dist/assets/Icons/vk-only-sign-neutral.svg +1 -0
  552. package/dist/assets/Icons/vk-only-sign.svg +1 -0
  553. package/dist/assets/Icons/vk.svg +1 -0
  554. package/dist/assets/Icons/webhook-neutral.svg +1 -0
  555. package/dist/assets/Icons/webhook.svg +1 -0
  556. package/dist/assets/Icons/wechat-neutral.svg +1 -0
  557. package/dist/assets/Icons/wechat.svg +1 -0
  558. package/dist/assets/Icons/whatsapp-badge-neutral.svg +1 -0
  559. package/dist/assets/Icons/whatsapp-badge.svg +1 -0
  560. package/dist/assets/Icons/whatsapp-neutral.svg +1 -0
  561. package/dist/assets/Icons/whatsapp.svg +1 -0
  562. package/dist/assets/Icons/workflow.svg +1 -0
  563. package/dist/assets/Icons/wrap-text.svg +1 -0
  564. package/dist/assets/Icons/wrench.svg +1 -0
  565. package/dist/assets/Icons/x-ex-twitter-neutral.svg +1 -0
  566. package/dist/assets/Icons/x-ex-twitter.svg +1 -0
  567. package/dist/assets/Icons/x.svg +1 -0
  568. package/dist/assets/Icons/xing-neutral.svg +1 -0
  569. package/dist/assets/Icons/xing.svg +1 -0
  570. package/dist/assets/Icons/yandex-music-neutral.svg +1 -0
  571. package/dist/assets/Icons/yandex-music.svg +1 -0
  572. package/dist/assets/Icons/yelp-neutral.svg +1 -0
  573. package/dist/assets/Icons/yelp.svg +1 -0
  574. package/dist/assets/Icons/youtube-music-neutral.svg +1 -0
  575. package/dist/assets/Icons/youtube-music.svg +1 -0
  576. package/dist/assets/Icons/youtube-neutral.svg +1 -0
  577. package/dist/assets/Icons/youtube-shorts-neutral.svg +1 -0
  578. package/dist/assets/Icons/youtube-shorts.svg +1 -0
  579. package/dist/assets/Icons/youtube.svg +1 -0
  580. package/dist/assets/Icons/zap.svg +1 -0
  581. package/dist/assets/Icons/zoom-in.svg +1 -0
  582. package/dist/assets/Icons/zoom-neutral.svg +1 -0
  583. package/dist/assets/Icons/zoom-out.svg +1 -0
  584. package/dist/assets/Icons/zoom.svg +1 -0
  585. package/dist/assets/Icons/zulip-neutral.svg +1 -0
  586. package/dist/assets/Icons/zulip.svg +1 -0
  587. package/dist/assets/Logos/wallarm.svg +1 -0
  588. package/dist/assets.json +4666 -0
  589. package/dist/project/README.md +265 -0
  590. package/dist/project/components/Accordion/README.md +354 -0
  591. package/dist/project/components/Accordion/preview.html +2 -0
  592. package/dist/project/components/Accordion/stories.js +12 -0
  593. package/dist/project/components/AgentAvatar/README.md +127 -0
  594. package/dist/project/components/AgentAvatar/preview.html +2 -0
  595. package/dist/project/components/AgentAvatar/stories.js +12 -0
  596. package/dist/project/components/AgentAvatar/stories.md +1805 -0
  597. package/dist/project/components/AgentGlyph/README.md +1182 -0
  598. package/dist/project/components/AgentGlyph/preview.html +2 -0
  599. package/dist/project/components/AgentGlyph/stories.js +12 -0
  600. package/dist/project/components/AgentLookBuilder/README.md +269 -0
  601. package/dist/project/components/AgentLookBuilder/preview.html +2 -0
  602. package/dist/project/components/AgentLookBuilder/stories.js +12 -0
  603. package/dist/project/components/Alert/README.md +492 -0
  604. package/dist/project/components/Alert/preview.html +2 -0
  605. package/dist/project/components/Alert/stories.js +12 -0
  606. package/dist/project/components/AnimatedBackground/README.md +124 -0
  607. package/dist/project/components/AnimatedBackground/preview.html +2 -0
  608. package/dist/project/components/AnimatedBackground/stories.js +12 -0
  609. package/dist/project/components/AppShell/README.md +75 -0
  610. package/dist/project/components/AppShell/preview.html +2 -0
  611. package/dist/project/components/AppShell/stories.js +28 -0
  612. package/dist/project/components/AppShell/stories.md +2878 -0
  613. package/dist/project/components/ApprovalCard/README.md +353 -0
  614. package/dist/project/components/ApprovalCard/preview.html +2 -0
  615. package/dist/project/components/ApprovalCard/stories.js +12 -0
  616. package/dist/project/components/AreaChart/README.md +673 -0
  617. package/dist/project/components/AreaChart/preview.html +2 -0
  618. package/dist/project/components/AreaChart/stories.js +12 -0
  619. package/dist/project/components/AssistantMessage/README.md +724 -0
  620. package/dist/project/components/AssistantMessage/preview.html +2 -0
  621. package/dist/project/components/AssistantMessage/stories.js +55 -0
  622. package/dist/project/components/Attribute/README.md +1174 -0
  623. package/dist/project/components/Attribute/preview.html +2 -0
  624. package/dist/project/components/Attribute/stories.js +12 -0
  625. package/dist/project/components/Avatar/README.md +416 -0
  626. package/dist/project/components/Avatar/preview.html +2 -0
  627. package/dist/project/components/Avatar/stories.js +12 -0
  628. package/dist/project/components/Badge/README.md +853 -0
  629. package/dist/project/components/Badge/preview.html +2 -0
  630. package/dist/project/components/Badge/stories.js +12 -0
  631. package/dist/project/components/Banner/README.md +341 -0
  632. package/dist/project/components/Banner/preview.html +2 -0
  633. package/dist/project/components/Banner/stories.js +12 -0
  634. package/dist/project/components/BarList/README.md +757 -0
  635. package/dist/project/components/BarList/preview.html +2 -0
  636. package/dist/project/components/BarList/stories.js +12 -0
  637. package/dist/project/components/Breadcrumbs/README.md +274 -0
  638. package/dist/project/components/Breadcrumbs/preview.html +2 -0
  639. package/dist/project/components/Breadcrumbs/stories.js +12 -0
  640. package/dist/project/components/BulkBar/README.md +19 -0
  641. package/dist/project/components/Button/README.md +467 -0
  642. package/dist/project/components/Button/preview.html +2 -0
  643. package/dist/project/components/Button/stories.js +12 -0
  644. package/dist/project/components/Calendar/README.md +654 -0
  645. package/dist/project/components/Calendar/preview.html +2 -0
  646. package/dist/project/components/Calendar/stories.js +12 -0
  647. package/dist/project/components/Card/README.md +367 -0
  648. package/dist/project/components/Card/preview.html +2 -0
  649. package/dist/project/components/Card/stories.js +12 -0
  650. package/dist/project/components/ChainOfThought/README.md +412 -0
  651. package/dist/project/components/ChainOfThought/preview.html +2 -0
  652. package/dist/project/components/ChainOfThought/stories.js +12 -0
  653. package/dist/project/components/Chart/README.md +224 -0
  654. package/dist/project/components/Chart/preview.html +2 -0
  655. package/dist/project/components/Chart/stories.js +12 -0
  656. package/dist/project/components/Chat/README.md +1244 -0
  657. package/dist/project/components/Chat/preview.html +2 -0
  658. package/dist/project/components/Chat/stories.js +12 -0
  659. package/dist/project/components/ChatInput/README.md +378 -0
  660. package/dist/project/components/ChatInput/preview.html +2 -0
  661. package/dist/project/components/ChatInput/stories.js +12 -0
  662. package/dist/project/components/ChatPermissions/README.md +218 -0
  663. package/dist/project/components/ChatPermissions/preview.html +2 -0
  664. package/dist/project/components/ChatPermissions/stories.js +12 -0
  665. package/dist/project/components/Checkbox/README.md +367 -0
  666. package/dist/project/components/Checkbox/preview.html +2 -0
  667. package/dist/project/components/Checkbox/stories.js +12 -0
  668. package/dist/project/components/Code/README.md +243 -0
  669. package/dist/project/components/Code/preview.html +2 -0
  670. package/dist/project/components/Code/stories.js +12 -0
  671. package/dist/project/components/CodeEditor/README.md +280 -0
  672. package/dist/project/components/CodeEditor/preview.html +2 -0
  673. package/dist/project/components/CodeEditor/stories.js +162 -0
  674. package/dist/project/components/CodeEditor/stories.md +1533 -0
  675. package/dist/project/components/CodeSnippet/README.md +1352 -0
  676. package/dist/project/components/CodeSnippet/preview.html +2 -0
  677. package/dist/project/components/CodeSnippet/stories.js +156 -0
  678. package/dist/project/components/ColumnChart/README.md +521 -0
  679. package/dist/project/components/ColumnChart/preview.html +2 -0
  680. package/dist/project/components/ColumnChart/stories.js +12 -0
  681. package/dist/project/components/Conversation/README.md +314 -0
  682. package/dist/project/components/Conversation/preview.html +2 -0
  683. package/dist/project/components/Conversation/stories.js +12 -0
  684. package/dist/project/components/Copyable/README.md +406 -0
  685. package/dist/project/components/Copyable/preview.html +2 -0
  686. package/dist/project/components/Copyable/stories.js +12 -0
  687. package/dist/project/components/Country/README.md +226 -0
  688. package/dist/project/components/Country/preview.html +2 -0
  689. package/dist/project/components/Country/stories.js +12 -0
  690. package/dist/project/components/Cover/preview.html +43 -0
  691. package/dist/project/components/DateInput/README.md +458 -0
  692. package/dist/project/components/DateInput/preview.html +2 -0
  693. package/dist/project/components/DateInput/stories.js +12 -0
  694. package/dist/project/components/DateRangeInput/README.md +412 -0
  695. package/dist/project/components/DateRangeInput/preview.html +2 -0
  696. package/dist/project/components/DateRangeInput/stories.js +12 -0
  697. package/dist/project/components/Dialog/README.md +1140 -0
  698. package/dist/project/components/Dialog/preview.html +2 -0
  699. package/dist/project/components/Dialog/stories.js +12 -0
  700. package/dist/project/components/Drawer/README.md +1163 -0
  701. package/dist/project/components/Drawer/preview.html +2 -0
  702. package/dist/project/components/Drawer/stories.js +12 -0
  703. package/dist/project/components/DropdownMenu/README.md +678 -0
  704. package/dist/project/components/DropdownMenu/preview.html +2 -0
  705. package/dist/project/components/DropdownMenu/stories.js +12 -0
  706. package/dist/project/components/EmptyState/README.md +276 -0
  707. package/dist/project/components/EmptyState/preview.html +2 -0
  708. package/dist/project/components/EmptyState/stories.js +12 -0
  709. package/dist/project/components/FeedbackPulse/README.md +125 -0
  710. package/dist/project/components/FeedbackPulse/preview.html +2 -0
  711. package/dist/project/components/FeedbackPulse/stories.js +12 -0
  712. package/dist/project/components/Field/README.md +676 -0
  713. package/dist/project/components/Field/preview.html +2 -0
  714. package/dist/project/components/Field/stories.js +12 -0
  715. package/dist/project/components/FileUpload/README.md +544 -0
  716. package/dist/project/components/FileUpload/preview.html +2 -0
  717. package/dist/project/components/FileUpload/stories.js +12 -0
  718. package/dist/project/components/FilterCascade/README.md +348 -0
  719. package/dist/project/components/FilterCascade/preview.html +2 -0
  720. package/dist/project/components/FilterCascade/stories.js +12 -0
  721. package/dist/project/components/FilterDropdown/README.md +542 -0
  722. package/dist/project/components/FilterDropdown/preview.html +2 -0
  723. package/dist/project/components/FilterDropdown/stories.js +12 -0
  724. package/dist/project/components/FilterInput/README.md +416 -0
  725. package/dist/project/components/FilterInput/preview.html +2 -0
  726. package/dist/project/components/FilterInput/stories.js +12 -0
  727. package/dist/project/components/FilterInput/stories.md +6682 -0
  728. package/dist/project/components/Flex/README.md +225 -0
  729. package/dist/project/components/Flex/preview.html +2 -0
  730. package/dist/project/components/Flex/stories.js +12 -0
  731. package/dist/project/components/FormatDateTime/README.md +315 -0
  732. package/dist/project/components/FormatDateTime/preview.html +2 -0
  733. package/dist/project/components/FormatDateTime/stories.js +12 -0
  734. package/dist/project/components/FormatNumber/README.md +317 -0
  735. package/dist/project/components/FormatNumber/preview.html +2 -0
  736. package/dist/project/components/FormatNumber/stories.js +12 -0
  737. package/dist/project/components/Heading/README.md +240 -0
  738. package/dist/project/components/Heading/preview.html +2 -0
  739. package/dist/project/components/Heading/stories.js +12 -0
  740. package/dist/project/components/HorizontalBarStack/README.md +281 -0
  741. package/dist/project/components/HorizontalBarStack/preview.html +2 -0
  742. package/dist/project/components/HorizontalBarStack/stories.js +12 -0
  743. package/dist/project/components/HttpMethod/README.md +137 -0
  744. package/dist/project/components/HttpMethod/preview.html +2 -0
  745. package/dist/project/components/HttpMethod/stories.js +12 -0
  746. package/dist/project/components/ImageCropper/README.md +332 -0
  747. package/dist/project/components/ImageCropper/preview.html +2 -0
  748. package/dist/project/components/ImageCropper/stories.js +33 -0
  749. package/dist/project/components/Indicator/README.md +110 -0
  750. package/dist/project/components/Indicator/preview.html +2 -0
  751. package/dist/project/components/Indicator/stories.js +12 -0
  752. package/dist/project/components/InlineEdit/README.md +977 -0
  753. package/dist/project/components/InlineEdit/preview.html +2 -0
  754. package/dist/project/components/InlineEdit/stories.js +12 -0
  755. package/dist/project/components/Input/README.md +187 -0
  756. package/dist/project/components/Input/preview.html +2 -0
  757. package/dist/project/components/Input/stories.js +12 -0
  758. package/dist/project/components/InputGroup/README.md +423 -0
  759. package/dist/project/components/InputGroup/preview.html +2 -0
  760. package/dist/project/components/InputGroup/stories.js +12 -0
  761. package/dist/project/components/InputOTP/README.md +201 -0
  762. package/dist/project/components/InputOTP/preview.html +2 -0
  763. package/dist/project/components/InputOTP/stories.js +12 -0
  764. package/dist/project/components/Ip/README.md +226 -0
  765. package/dist/project/components/Ip/preview.html +2 -0
  766. package/dist/project/components/Ip/stories.js +12 -0
  767. package/dist/project/components/Kbd/README.md +126 -0
  768. package/dist/project/components/Kbd/preview.html +2 -0
  769. package/dist/project/components/Kbd/stories.js +12 -0
  770. package/dist/project/components/LineChart/README.md +1479 -0
  771. package/dist/project/components/LineChart/preview.html +2 -0
  772. package/dist/project/components/LineChart/stories.js +12 -0
  773. package/dist/project/components/Link/README.md +160 -0
  774. package/dist/project/components/Link/preview.html +2 -0
  775. package/dist/project/components/Link/stories.js +12 -0
  776. package/dist/project/components/List/README.md +181 -0
  777. package/dist/project/components/List/preview.html +2 -0
  778. package/dist/project/components/List/stories.js +12 -0
  779. package/dist/project/components/Loader/README.md +145 -0
  780. package/dist/project/components/Loader/preview.html +2 -0
  781. package/dist/project/components/Loader/stories.js +12 -0
  782. package/dist/project/components/Logo/README.md +183 -0
  783. package/dist/project/components/Logo/preview.html +2 -0
  784. package/dist/project/components/Logo/stories.js +12 -0
  785. package/dist/project/components/MessageActions/README.md +255 -0
  786. package/dist/project/components/MessageActions/preview.html +2 -0
  787. package/dist/project/components/MessageActions/stories.js +12 -0
  788. package/dist/project/components/Metric/README.md +324 -0
  789. package/dist/project/components/Metric/preview.html +2 -0
  790. package/dist/project/components/Metric/stories.js +12 -0
  791. package/dist/project/components/NavRail/README.md +25 -0
  792. package/dist/project/components/NotificationCenter/README.md +91 -0
  793. package/dist/project/components/NotificationCenter/preview.html +2 -0
  794. package/dist/project/components/NotificationCenter/stories.js +12 -0
  795. package/dist/project/components/NotificationCenter/stories.md +2170 -0
  796. package/dist/project/components/NumberInput/README.md +177 -0
  797. package/dist/project/components/NumberInput/preview.html +2 -0
  798. package/dist/project/components/NumberInput/stories.js +12 -0
  799. package/dist/project/components/NumericBadge/README.md +252 -0
  800. package/dist/project/components/NumericBadge/preview.html +2 -0
  801. package/dist/project/components/NumericBadge/stories.js +12 -0
  802. package/dist/project/components/OverflowList/README.md +324 -0
  803. package/dist/project/components/OverflowList/preview.html +2 -0
  804. package/dist/project/components/OverflowList/stories.js +12 -0
  805. package/dist/project/components/OverflowTooltip/README.md +421 -0
  806. package/dist/project/components/OverflowTooltip/preview.html +2 -0
  807. package/dist/project/components/OverflowTooltip/stories.js +12 -0
  808. package/dist/project/components/Overlay/README.md +95 -0
  809. package/dist/project/components/Overlay/preview.html +2 -0
  810. package/dist/project/components/Overlay/stories.js +12 -0
  811. package/dist/project/components/Overview/README.md +445 -0
  812. package/dist/project/components/Overview/preview.html +2 -0
  813. package/dist/project/components/Overview/stories.js +12 -0
  814. package/dist/project/components/Page/README.md +215 -0
  815. package/dist/project/components/Page/preview.html +2 -0
  816. package/dist/project/components/Page/stories.js +12 -0
  817. package/dist/project/components/Pagination/README.md +302 -0
  818. package/dist/project/components/Pagination/preview.html +2 -0
  819. package/dist/project/components/Pagination/stories.js +12 -0
  820. package/dist/project/components/ParameterPath/README.md +284 -0
  821. package/dist/project/components/ParameterPath/preview.html +2 -0
  822. package/dist/project/components/ParameterPath/stories.js +12 -0
  823. package/dist/project/components/PasswordComplexity/README.md +171 -0
  824. package/dist/project/components/PasswordComplexity/preview.html +2 -0
  825. package/dist/project/components/PasswordComplexity/stories.js +12 -0
  826. package/dist/project/components/PasswordInput/README.md +254 -0
  827. package/dist/project/components/PasswordInput/preview.html +2 -0
  828. package/dist/project/components/PasswordInput/stories.js +12 -0
  829. package/dist/project/components/PieChart/README.md +895 -0
  830. package/dist/project/components/PieChart/preview.html +2 -0
  831. package/dist/project/components/PieChart/stories.js +12 -0
  832. package/dist/project/components/Popover/README.md +282 -0
  833. package/dist/project/components/Popover/preview.html +2 -0
  834. package/dist/project/components/Popover/stories.js +12 -0
  835. package/dist/project/components/Progress/README.md +174 -0
  836. package/dist/project/components/Progress/preview.html +2 -0
  837. package/dist/project/components/Progress/stories.js +12 -0
  838. package/dist/project/components/Radio/README.md +264 -0
  839. package/dist/project/components/Radio/preview.html +2 -0
  840. package/dist/project/components/Radio/stories.js +12 -0
  841. package/dist/project/components/RemoteShell/README.md +1758 -0
  842. package/dist/project/components/RemoteShell/preview.html +2 -0
  843. package/dist/project/components/RemoteShell/stories.js +28 -0
  844. package/dist/project/components/Resizable/README.md +290 -0
  845. package/dist/project/components/Resizable/preview.html +2 -0
  846. package/dist/project/components/Resizable/stories.js +12 -0
  847. package/dist/project/components/ResponseCode/README.md +205 -0
  848. package/dist/project/components/ResponseCode/preview.html +2 -0
  849. package/dist/project/components/ResponseCode/stories.js +12 -0
  850. package/dist/project/components/ScrollArea/README.md +234 -0
  851. package/dist/project/components/ScrollArea/preview.html +2 -0
  852. package/dist/project/components/ScrollArea/stories.js +12 -0
  853. package/dist/project/components/SearchInput/README.md +158 -0
  854. package/dist/project/components/SearchInput/preview.html +2 -0
  855. package/dist/project/components/SearchInput/stories.js +12 -0
  856. package/dist/project/components/SearchModal/README.md +836 -0
  857. package/dist/project/components/SearchModal/preview.html +2 -0
  858. package/dist/project/components/SearchModal/stories.js +12 -0
  859. package/dist/project/components/SegmentedControl/README.md +382 -0
  860. package/dist/project/components/SegmentedControl/preview.html +2 -0
  861. package/dist/project/components/SegmentedControl/stories.js +12 -0
  862. package/dist/project/components/SegmentedTabs/README.md +390 -0
  863. package/dist/project/components/SegmentedTabs/preview.html +2 -0
  864. package/dist/project/components/SegmentedTabs/stories.js +12 -0
  865. package/dist/project/components/Select/README.md +1311 -0
  866. package/dist/project/components/Select/preview.html +2 -0
  867. package/dist/project/components/Select/stories.js +12 -0
  868. package/dist/project/components/Selection/README.md +648 -0
  869. package/dist/project/components/Selection/preview.html +2 -0
  870. package/dist/project/components/Selection/stories.js +12 -0
  871. package/dist/project/components/Separator/README.md +77 -0
  872. package/dist/project/components/Separator/preview.html +2 -0
  873. package/dist/project/components/Separator/stories.js +12 -0
  874. package/dist/project/components/Skeleton/README.md +358 -0
  875. package/dist/project/components/Skeleton/preview.html +2 -0
  876. package/dist/project/components/Skeleton/stories.js +12 -0
  877. package/dist/project/components/Slider/README.md +433 -0
  878. package/dist/project/components/Slider/preview.html +2 -0
  879. package/dist/project/components/Slider/stories.js +12 -0
  880. package/dist/project/components/SplashScreen/README.md +192 -0
  881. package/dist/project/components/SplashScreen/preview.html +2 -0
  882. package/dist/project/components/SplashScreen/stories.js +12 -0
  883. package/dist/project/components/SplitButton/README.md +402 -0
  884. package/dist/project/components/SplitButton/preview.html +2 -0
  885. package/dist/project/components/SplitButton/stories.js +12 -0
  886. package/dist/project/components/Stack/README.md +269 -0
  887. package/dist/project/components/Stack/preview.html +2 -0
  888. package/dist/project/components/Stack/stories.js +12 -0
  889. package/dist/project/components/Stepper/README.md +682 -0
  890. package/dist/project/components/Stepper/preview.html +2 -0
  891. package/dist/project/components/Stepper/stories.js +12 -0
  892. package/dist/project/components/Switch/README.md +236 -0
  893. package/dist/project/components/Switch/preview.html +2 -0
  894. package/dist/project/components/Switch/stories.js +12 -0
  895. package/dist/project/components/Table/README.md +471 -0
  896. package/dist/project/components/Table/preview.html +2 -0
  897. package/dist/project/components/Table/stories.js +12 -0
  898. package/dist/project/components/Table/stories.md +3697 -0
  899. package/dist/project/components/Tabs/README.md +443 -0
  900. package/dist/project/components/Tabs/preview.html +2 -0
  901. package/dist/project/components/Tabs/stories.js +12 -0
  902. package/dist/project/components/Tag/README.md +324 -0
  903. package/dist/project/components/Tag/preview.html +2 -0
  904. package/dist/project/components/Tag/stories.js +12 -0
  905. package/dist/project/components/Text/README.md +200 -0
  906. package/dist/project/components/Text/preview.html +2 -0
  907. package/dist/project/components/Text/stories.js +12 -0
  908. package/dist/project/components/Textarea/README.md +273 -0
  909. package/dist/project/components/Textarea/preview.html +2 -0
  910. package/dist/project/components/Textarea/stories.js +12 -0
  911. package/dist/project/components/TimeInput/README.md +359 -0
  912. package/dist/project/components/TimeInput/preview.html +2 -0
  913. package/dist/project/components/TimeInput/stories.js +12 -0
  914. package/dist/project/components/Timeline/README.md +204 -0
  915. package/dist/project/components/Timeline/preview.html +2 -0
  916. package/dist/project/components/Timeline/stories.js +12 -0
  917. package/dist/project/components/Toast/README.md +718 -0
  918. package/dist/project/components/Toast/preview.html +2 -0
  919. package/dist/project/components/Toast/stories.js +12 -0
  920. package/dist/project/components/ToggleButton/README.md +292 -0
  921. package/dist/project/components/ToggleButton/preview.html +2 -0
  922. package/dist/project/components/ToggleButton/stories.js +12 -0
  923. package/dist/project/components/Tooltip/README.md +199 -0
  924. package/dist/project/components/Tooltip/preview.html +2 -0
  925. package/dist/project/components/Tooltip/stories.js +12 -0
  926. package/dist/project/components/TopHeader/README.md +21 -0
  927. package/dist/project/components/Tour/README.md +532 -0
  928. package/dist/project/components/Tour/preview.html +2 -0
  929. package/dist/project/components/Tour/stories.js +12 -0
  930. package/dist/project/components/Tree/README.md +450 -0
  931. package/dist/project/components/Tree/preview.html +2 -0
  932. package/dist/project/components/Tree/stories.js +12 -0
  933. package/dist/project/components/TreeView/README.md +531 -0
  934. package/dist/project/components/TreeView/preview.html +2 -0
  935. package/dist/project/components/TreeView/stories.js +12 -0
  936. package/dist/project/components/UTILITIES.md +647 -0
  937. package/dist/project/components/UserMessage/README.md +166 -0
  938. package/dist/project/components/UserMessage/preview.html +2 -0
  939. package/dist/project/components/UserMessage/stories.js +12 -0
  940. package/dist/project/components/UtilityPage/README.md +147 -0
  941. package/dist/project/components/UtilityPage/preview.html +2 -0
  942. package/dist/project/components/UtilityPage/stories.js +12 -0
  943. package/dist/project/components/WallyAssistant/README.md +1380 -0
  944. package/dist/project/components/WallyAssistant/preview.html +2 -0
  945. package/dist/project/components/WallyAssistant/stories.js +12 -0
  946. package/dist/project/components/WallyIcon/README.md +130 -0
  947. package/dist/project/components/WallyIcon/preview.html +2 -0
  948. package/dist/project/components/WallyIcon/stories.js +12 -0
  949. package/dist/project/components/WallySkeleton/README.md +96 -0
  950. package/dist/project/components/WallySkeleton/preview.html +2 -0
  951. package/dist/project/components/WallySkeleton/stories.js +12 -0
  952. package/dist/project/components/bundle.css +18734 -0
  953. package/dist/project/components/bundle.js +167 -0
  954. package/dist/project/components/index.d.ts +27226 -0
  955. package/dist/project/design-system.json +35 -0
  956. package/dist/project/fonts/Geist-Variable.woff2 +0 -0
  957. package/dist/project/fonts/GeistMono-Variable.woff2 +0 -0
  958. package/dist/project/fonts/GeistPixel-Square.woff2 +0 -0
  959. package/dist/project/sampler.dc.html +238 -0
  960. package/dist/project/tokens.css +1395 -0
  961. package/dist/project/tokens.json +5235 -0
  962. package/dist/wallarm-sampler.dc.html +239 -0
  963. package/package.json +67 -3
@@ -0,0 +1,2878 @@
1
+ # AppShell story sources
2
+
3
+ ### AppShell/AppShell.stories.tsx
4
+
5
+ ```tsx
6
+ import { type FC, useEffect, useRef, useState } from 'react';
7
+ import {
8
+ createMemoryHistory,
9
+ createRootRoute,
10
+ createRoute,
11
+ createRouter,
12
+ Outlet,
13
+ Link as RouterLink,
14
+ RouterProvider,
15
+ useNavigate,
16
+ useParams,
17
+ useRouterState,
18
+ } from '@tanstack/react-router';
19
+ import type { Meta, StoryFn } from 'storybook-react-rsbuild';
20
+ import { Home, X } from '../../icons';
21
+ import { AnimatedBackground } from '../AnimatedBackground';
22
+ import { Button } from '../Button';
23
+ import { ChatTrigger } from '../Chat';
24
+ import { Input } from '../Input';
25
+ import { Link } from '../Link';
26
+ import { NavRail, NavRailBody, NavRailItem, NavRailSeparator, NavRailSkeleton } from '../NavRail';
27
+ import { Page, PageContent, PageHeader, PageTitle } from '../Page';
28
+ import {
29
+ RemoteShell,
30
+ RemoteShellBreadcrumb,
31
+ RemoteShellContent,
32
+ RemoteShellPanel,
33
+ useRemoteShellContext,
34
+ } from '../RemoteShell';
35
+ import { Skeleton } from '../Skeleton';
36
+ import { SplashScreen } from '../SplashScreen';
37
+ import { Text } from '../Text';
38
+ import { useTheme } from '../ThemeProvider';
39
+ import { TopHeader, TopHeaderActions, TopHeaderLogo, TopHeaderSeparator } from '../TopHeader';
40
+ import {
41
+ type ChatAssistantMessage,
42
+ type ChatFeedback,
43
+ type ChatToolPermission,
44
+ WallyAssistant,
45
+ } from '../WallyAssistant';
46
+ import { CONVERSATION, useMockWally } from '../WallyAssistant/mocks';
47
+ import { AppShell, type AppShellProps } from './AppShell';
48
+ import { AppShellAside } from './AppShellAside';
49
+ import { AppShellHeader } from './AppShellHeader';
50
+ import { AppShellRail } from './AppShellRail';
51
+ import { AppShellRemote } from './AppShellRemote';
52
+ import {
53
+ deriveProduct,
54
+ HeaderActions,
55
+ HomeContent,
56
+ NavRailFooterContent,
57
+ NotificationsInbox,
58
+ PRODUCT_CONFIGS,
59
+ type Product,
60
+ ProductNavItems,
61
+ productLandingPath,
62
+ QuickHelpDropdown,
63
+ RecentDropdown,
64
+ RemoteForProduct,
65
+ railModeFor,
66
+ type SidebarMode,
67
+ StoryRouter,
68
+ useStoryProductNavigation,
69
+ WallarmLogo,
70
+ } from './story-content';
71
+
72
+ const DESCRIPTION = [
73
+ 'The platform’s outermost frame — global top bar, product rail, and the content surface products mount into, with an optional `AppShellAside` panel docked to its right. There is exactly one, at the app root, and it persists while the content swaps.',
74
+ 'Do not wrap a page in it: a single product screen renders `RemoteShell` and its own content, blind to the shell. In-product navigation is `RemoteShell`, configured with a `NavConfig`, and laying out a page’s insides is `Stack` and `Flex`.',
75
+ 'The gray surround — header bar plus rail, everything that is not the canvas — is the **Frame**. It carries **Ambient**, a faint still layer of soft blooms, grain and a dot cluster pooled bottom-left, painted under everything. Ambient is on by default; `ambient={false}` leaves the flat Frame fill.',
76
+ 'The Frame comes in two styles, **Neutral** and **Branded**: a user preference next to light/dark, set with `useTheme().setFrameStyle` (from `ThemeProvider`) and written on `<html>` so every micro-frontend follows it. Branded warms the Frame and turns active navigation to the brand colour; try it from the account item → Appearance.',
77
+ ].join(' ');
78
+
79
+ const meta = {
80
+ title: 'Navigation/AppShell',
81
+ component: AppShell,
82
+ subcomponents: {
83
+ AppShellHeader,
84
+ AppShellRail,
85
+ AppShellRemote,
86
+ AppShellAside,
87
+ },
88
+ parameters: {
89
+ layout: 'fullscreen',
90
+ docs: {
91
+ description: {
92
+ component: DESCRIPTION,
93
+ },
94
+ },
95
+ },
96
+ args: {
97
+ ambient: true,
98
+ },
99
+ argTypes: {
100
+ children: { control: false },
101
+ ref: { control: false },
102
+ ambient: {
103
+ control: 'boolean',
104
+ description: 'The Frame’s ambient layer. Turn off to see the flat Frame token fill.',
105
+ },
106
+ },
107
+ } satisfies Meta<typeof AppShell>;
108
+
109
+ export default meta;
110
+
111
+ interface ShellProps {
112
+ ambient?: boolean;
113
+ /** Header actions and product rail show skeletons in place of their items. */
114
+ loading?: boolean;
115
+ /** Each product fakes a 2s load when opened, showing its panel and content skeletons. */
116
+ simulateProductLoading?: boolean;
117
+ /** Docks the real `WallyAssistant` (driven by `useMockWally`) in `AppShellAside`, open at first and toggled from the header's `ChatTrigger`. */
118
+ aside?: boolean;
119
+ }
120
+
121
+ const Shell = ({
122
+ ambient,
123
+ loading = false,
124
+ simulateProductLoading = false,
125
+ aside = false,
126
+ }: ShellProps) => {
127
+ const { activeProduct, goToProduct } = useStoryProductNavigation();
128
+
129
+ const [sidebarMode, setSidebarMode] = useState<SidebarMode>('adaptive');
130
+ const [asideOpen, setAsideOpen] = useState(true);
131
+ const toggleRef = useRef<HTMLButtonElement>(null);
132
+ const { theme, setTheme } = useTheme();
133
+ const railMode = railModeFor(sidebarMode, activeProduct === 'home');
134
+ const wally = useMockWally({ initial: CONVERSATION });
135
+ const [feedback, setFeedback] = useState<Record<string, ChatFeedback | null>>({});
136
+ const [permissions, setPermissions] = useState(CONVERSATION.permissions);
137
+
138
+ const handleHomeClick = () => goToProduct('home');
139
+ const handleAssistantToggleClick = () => setAsideOpen(open => !open);
140
+ // Fixed (UTC) clock text instead of "N hours ago", so screenshots don't drift.
141
+ const formatTimestamp = (iso: string) => new Date(iso).toISOString().slice(11, 16);
142
+ const handleFeedback = (message: ChatAssistantMessage, value: ChatFeedback | null) => {
143
+ setFeedback(f => ({ ...f, [message.id]: value }));
144
+ };
145
+ const handlePermissionChange = (permission: ChatToolPermission, allowed: boolean) => {
146
+ setPermissions(list => list?.map(x => (x.id === permission.id ? { ...x, allowed } : x)));
147
+ };
148
+
149
+ const messages = wally.snapshot.messages.map(m =>
150
+ m.role === 'assistant' && m.id in feedback ? { ...m, feedback: feedback[m.id] ?? null } : m,
151
+ );
152
+
153
+ const assistantToggle = aside && (
154
+ <ChatTrigger ref={toggleRef} aria-pressed={asideOpen} onClick={handleAssistantToggleClick} />
155
+ );
156
+ const handleCloseClick = () => {
157
+ // Focus inside the panel would drop to <body> once it hides: hand it back to the toggle.
158
+ toggleRef.current?.focus();
159
+ setAsideOpen(false);
160
+ };
161
+
162
+ return (
163
+ <AppShell ambient={ambient} data-testid='app-shell'>
164
+ <AppShellHeader>
165
+ <TopHeader>
166
+ <TopHeaderLogo href='/'>
167
+ <WallarmLogo />
168
+ </TopHeaderLogo>
169
+
170
+ <TopHeaderActions>
171
+ {loading ? (
172
+ <>
173
+ <Skeleton width='150px' height='20px' rounded={6} />
174
+ <TopHeaderSeparator />
175
+ <Skeleton width='150px' height='20px' rounded={6} />
176
+
177
+ <NotificationsInbox loading />
178
+
179
+ <QuickHelpDropdown />
180
+ </>
181
+ ) : (
182
+ <HeaderActions assistantToggle={assistantToggle} />
183
+ )}
184
+ </TopHeaderActions>
185
+ </TopHeader>
186
+ </AppShellHeader>
187
+
188
+ <AppShellRail>
189
+ <NavRail mode={railMode}>
190
+ <NavRailBody>
191
+ <NavRailItem
192
+ icon={Home}
193
+ label='Home'
194
+ shortcut={['G', 'H']}
195
+ active={activeProduct === 'home'}
196
+ onClick={handleHomeClick}
197
+ />
198
+ <RecentDropdown />
199
+
200
+ <NavRailSeparator />
201
+
202
+ {loading ? (
203
+ <NavRailSkeleton />
204
+ ) : (
205
+ <ProductNavItems activeProduct={activeProduct} onSelectProduct={goToProduct} />
206
+ )}
207
+ </NavRailBody>
208
+
209
+ <NavRailFooterContent
210
+ onSelectProduct={goToProduct}
211
+ activeProduct={activeProduct}
212
+ sidebarMode={sidebarMode}
213
+ onSidebarModeChange={setSidebarMode}
214
+ theme={theme}
215
+ onThemeChange={setTheme}
216
+ />
217
+ </NavRail>
218
+ </AppShellRail>
219
+
220
+ <AppShellRemote>
221
+ <RemoteForProduct product={activeProduct} simulateLoading={simulateProductLoading} />
222
+ </AppShellRemote>
223
+
224
+ {aside && (
225
+ <AppShellAside open={asideOpen} aria-label='Wally'>
226
+ <WallyAssistant
227
+ className='min-h-0 flex-1'
228
+ {...wally.snapshot}
229
+ messages={messages}
230
+ permissions={permissions}
231
+ formatTimestamp={formatTimestamp}
232
+ onSubmit={wally.send}
233
+ onStop={wally.stop}
234
+ onApprovalRespond={wally.respond}
235
+ onFeedback={handleFeedback}
236
+ onPermissionChange={handlePermissionChange}
237
+ headerActions={
238
+ <Button
239
+ variant='ghost'
240
+ color='neutral'
241
+ size='small'
242
+ aria-label='Close'
243
+ onClick={handleCloseClick}
244
+ >
245
+ <X />
246
+ </Button>
247
+ }
248
+ />
249
+ </AppShellAside>
250
+ )}
251
+ </AppShell>
252
+ );
253
+ };
254
+
255
+ /** The three regions composed once — `AppShellHeader`, `AppShellRail`, `AppShellRemote` — with the content surface being the only part that scrolls. Pick a product in the rail to drill into it, and the account item at the bottom for appearance and sidebar mode. */
256
+ export const Basic: StoryFn<AppShellProps> = ({ ambient }) => (
257
+ <StoryRouter>
258
+ <Shell ambient={ambient} />
259
+ </StoryRouter>
260
+ );
261
+
262
+ /** How the shell looks while the platform loads. The header actions and the product rail hold skeletons that sit exactly where the real items land, so nothing jumps when they arrive. Turn `loading` off in Controls to watch the swap. Opening a product also shows its own panel and content loading. */
263
+ export const Loading: StoryFn<AppShellProps & { loading: boolean }> = ({ ambient, loading }) => (
264
+ <StoryRouter>
265
+ <Shell ambient={ambient} loading={loading} simulateProductLoading />
266
+ </StoryRouter>
267
+ );
268
+ Loading.args = { loading: true };
269
+ Loading.argTypes = {
270
+ loading: {
271
+ control: 'boolean',
272
+ description: 'Show the header and rail skeletons instead of their items.',
273
+ },
274
+ };
275
+
276
+ /** `AppShellAside` docks a panel right of the content surface — the real `WallyAssistant` here, driven by the `useMockWally` streaming mock and toggled from the header's `ChatTrigger` — that slides in as the canvas narrows, while closing widens the canvas back and hides the region without unmounting what is inside, so the conversation and text typed into the panel survive a close and reopen. */
277
+ export const WithAside: StoryFn<AppShellProps> = ({ ambient }) => (
278
+ <StoryRouter>
279
+ <Shell ambient={ambient} aside />
280
+ </StoryRouter>
281
+ );
282
+
283
+ const RevealFlowShell = () => {
284
+ const { activeProduct, goToProduct } = useStoryProductNavigation();
285
+
286
+ const [splashDone, setSplashDone] = useState(false);
287
+ const [sidebarMode, setSidebarMode] = useState<SidebarMode>('adaptive');
288
+ const [revealKey, setRevealKey] = useState(0);
289
+ const { theme, setTheme } = useTheme();
290
+ const railMode = railModeFor(sidebarMode, activeProduct === 'home');
291
+
292
+ const handleHomeClick = () => goToProduct('home');
293
+
294
+ useEffect(() => {
295
+ if (splashDone) return;
296
+ const timer = setTimeout(() => setSplashDone(true), 2000);
297
+ return () => clearTimeout(timer);
298
+ }, [splashDone]);
299
+
300
+ const handleReplay = () => {
301
+ setSplashDone(false);
302
+ setRevealKey(k => k + 1);
303
+ };
304
+
305
+ if (!splashDone) {
306
+ return (
307
+ <div key={revealKey} className='h-screen w-screen bg-bg-page-bg'>
308
+ <SplashScreen />
309
+ </div>
310
+ );
311
+ }
312
+
313
+ return (
314
+ <AppShell key={revealKey} reveal>
315
+ <AppShellHeader>
316
+ <TopHeader>
317
+ <TopHeaderLogo href='/'>
318
+ <WallarmLogo />
319
+ </TopHeaderLogo>
320
+
321
+ <TopHeaderActions>
322
+ <HeaderActions />
323
+ </TopHeaderActions>
324
+ </TopHeader>
325
+ </AppShellHeader>
326
+
327
+ <AppShellRail>
328
+ <NavRail mode={railMode}>
329
+ <NavRailBody>
330
+ <NavRailItem
331
+ icon={Home}
332
+ label='Home'
333
+ shortcut={['G', 'H']}
334
+ active={activeProduct === 'home'}
335
+ onClick={handleHomeClick}
336
+ />
337
+ <RecentDropdown />
338
+
339
+ <NavRailSeparator />
340
+
341
+ <ProductNavItems activeProduct={activeProduct} onSelectProduct={goToProduct} />
342
+ </NavRailBody>
343
+
344
+ <NavRailFooterContent
345
+ onSelectProduct={goToProduct}
346
+ activeProduct={activeProduct}
347
+ sidebarMode={sidebarMode}
348
+ onSidebarModeChange={setSidebarMode}
349
+ theme={theme}
350
+ onThemeChange={setTheme}
351
+ />
352
+ </NavRail>
353
+ </AppShellRail>
354
+
355
+ <AppShellRemote>
356
+ <div className='flex gap-8 absolute top-4 right-4 z-10'>
357
+ <Button variant='ghost' size='small' color='neutral' onClick={handleReplay}>
358
+ Replay animation
359
+ </Button>
360
+ </div>
361
+
362
+ <RemoteForProduct product={activeProduct} />
363
+ </AppShellRemote>
364
+ </AppShell>
365
+ );
366
+ };
367
+
368
+ /** `reveal` animates the chrome in on the first application load only. Leave it unset for an ordinary screen; this exists for prototyping the boot moment itself. */
369
+ export const RevealFlow: StoryFn<AppShellProps> = () => (
370
+ <StoryRouter>
371
+ <RevealFlowShell />
372
+ </StoryRouter>
373
+ );
374
+
375
+ const CARD_DIMENSIONS = { width: 480, height: 600, borderRadius: 12 };
376
+
377
+ const LoginFlowShell = () => {
378
+ const { activeProduct, goToProduct } = useStoryProductNavigation();
379
+
380
+ const [splashVisible, setSplashVisible] = useState(true);
381
+ const [showShell, setShowShell] = useState(false);
382
+ const [revealed, setRevealed] = useState(false);
383
+ const [flowKey, setFlowKey] = useState(0);
384
+ const [sidebarMode, setSidebarMode] = useState<SidebarMode>('adaptive');
385
+ const { theme, setTheme } = useTheme();
386
+ const railMode = railModeFor(sidebarMode, activeProduct === 'home');
387
+
388
+ useEffect(() => {
389
+ if (!splashVisible) return;
390
+ const timer = setTimeout(() => setSplashVisible(false), 2000);
391
+ return () => clearTimeout(timer);
392
+ }, [splashVisible]);
393
+
394
+ const handleHomeClick = () => goToProduct('home');
395
+ const handleRevealed = () => setRevealed(true);
396
+
397
+ const handleSignIn = () => {
398
+ setShowShell(true);
399
+ };
400
+
401
+ const handleReplay = () => {
402
+ setShowShell(false);
403
+ setRevealed(false);
404
+ setSplashVisible(true);
405
+ setFlowKey(k => k + 1);
406
+ };
407
+
408
+ return (
409
+ <div
410
+ key={flowKey}
411
+ className='relative h-screen w-screen overflow-hidden bg-component-app-shell-bg'
412
+ >
413
+ {!revealed && (
414
+ <AnimatedBackground
415
+ className='absolute inset-0'
416
+ style={{
417
+ opacity: showShell ? 0 : 1,
418
+ transition: showShell ? 'opacity 400ms cubic-bezier(0.4, 0, 0.2, 1)' : undefined,
419
+ }}
420
+ />
421
+ )}
422
+
423
+ {!showShell && (
424
+ <div className='absolute inset-0 flex items-center justify-center z-10'>
425
+ <SplashScreen
426
+ visible={splashVisible}
427
+ shrinkTarget={CARD_DIMENSIONS}
428
+ className='bg-bg-page-bg shadow-lg'
429
+ >
430
+ <div className='flex h-full w-full flex-col items-center justify-center gap-16 p-24'>
431
+ <Text size='xl'>Sign In</Text>
432
+ <div className='flex w-full flex-col gap-12'>
433
+ <Input placeholder='Email' />
434
+ <Input placeholder='Password' type='password' />
435
+ </div>
436
+ <Button variant='primary' color='brand' className='w-full' onClick={handleSignIn}>
437
+ Sign In
438
+ </Button>
439
+ </div>
440
+ </SplashScreen>
441
+ </div>
442
+ )}
443
+
444
+ {showShell && (
445
+ <div className='absolute inset-0'>
446
+ <AppShell expandFrom={CARD_DIMENSIONS} onRevealed={handleRevealed}>
447
+ <AppShellHeader>
448
+ <TopHeader>
449
+ <TopHeaderLogo href='/'>
450
+ <WallarmLogo />
451
+ </TopHeaderLogo>
452
+
453
+ <TopHeaderActions>
454
+ <HeaderActions />
455
+ </TopHeaderActions>
456
+ </TopHeader>
457
+ </AppShellHeader>
458
+
459
+ <AppShellRail>
460
+ <NavRail mode={railMode}>
461
+ <NavRailBody>
462
+ <NavRailItem
463
+ icon={Home}
464
+ label='Home'
465
+ active={activeProduct === 'home'}
466
+ onClick={handleHomeClick}
467
+ />
468
+
469
+ <NavRailSeparator />
470
+
471
+ <ProductNavItems activeProduct={activeProduct} onSelectProduct={goToProduct} />
472
+ </NavRailBody>
473
+ <NavRailFooterContent
474
+ onSelectProduct={goToProduct}
475
+ activeProduct={activeProduct}
476
+ sidebarMode={sidebarMode}
477
+ onSidebarModeChange={setSidebarMode}
478
+ theme={theme}
479
+ onThemeChange={setTheme}
480
+ />
481
+ </NavRail>
482
+ </AppShellRail>
483
+
484
+ <AppShellRemote>
485
+ <div className='flex gap-8 absolute top-4 right-4 z-10'>
486
+ <Button variant='ghost' size='small' color='neutral' onClick={handleReplay}>
487
+ Replay animation
488
+ </Button>
489
+ </div>
490
+ <RemoteForProduct product={activeProduct} />
491
+ </AppShellRemote>
492
+ </AppShell>
493
+ </div>
494
+ )}
495
+ </div>
496
+ );
497
+ };
498
+
499
+ /** The whole entrance: the splash hands off, the content surface expands from the sign-in card into the full shell. `onRevealed` fires when it has settled. */
500
+ export const LoginFlow: StoryFn<AppShellProps> = () => (
501
+ <StoryRouter>
502
+ <LoginFlowShell />
503
+ </StoryRouter>
504
+ );
505
+
506
+ // --- TanStack Router ---------------------------------------------------------------
507
+
508
+ /** Rail → router: the host shell owns product switching. */
509
+ const RouterRootLayout: FC = () => {
510
+ const navigate = useNavigate();
511
+ const pathname = useRouterState({ select: state => state.location.pathname });
512
+ const activeProduct = deriveProduct(pathname);
513
+ const goToProduct = (product: Product) => navigate({ to: productLandingPath(product) });
514
+ const handleHomeClick = () => goToProduct('home');
515
+
516
+ const [sidebarMode, setSidebarMode] = useState<SidebarMode>('adaptive');
517
+ const { theme, setTheme } = useTheme();
518
+ const railMode = railModeFor(sidebarMode, activeProduct === 'home');
519
+
520
+ return (
521
+ <AppShell>
522
+ <AppShellHeader>
523
+ <TopHeader>
524
+ <TopHeaderLogo href='/'>
525
+ <WallarmLogo />
526
+ </TopHeaderLogo>
527
+ <TopHeaderActions>
528
+ <HeaderActions />
529
+ </TopHeaderActions>
530
+ </TopHeader>
531
+ </AppShellHeader>
532
+
533
+ <AppShellRail>
534
+ <NavRail mode={railMode}>
535
+ <NavRailBody>
536
+ <NavRailItem
537
+ icon={Home}
538
+ label='Home'
539
+ shortcut={['G', 'H']}
540
+ active={activeProduct === 'home'}
541
+ onClick={handleHomeClick}
542
+ />
543
+ <NavRailSeparator />
544
+ <ProductNavItems activeProduct={activeProduct} onSelectProduct={goToProduct} />
545
+ </NavRailBody>
546
+ <NavRailFooterContent
547
+ activeProduct={activeProduct}
548
+ onSelectProduct={goToProduct}
549
+ sidebarMode={sidebarMode}
550
+ onSidebarModeChange={setSidebarMode}
551
+ theme={theme}
552
+ onThemeChange={setTheme}
553
+ />
554
+ </NavRail>
555
+ </AppShellRail>
556
+
557
+ <AppShellRemote>
558
+ <Outlet />
559
+ </AppShellRemote>
560
+ </AppShell>
561
+ );
562
+ };
563
+
564
+ /**
565
+ * One product remote. The router owns the URL: `RemoteShell` gets the router's pathname
566
+ * through `pathname` and hands its own navigations back through `onNavigate`. The router
567
+ * has no `basepath`, so both sides use the full pathname (`/edge/...`).
568
+ */
569
+ const RouterProductRemote: FC = () => {
570
+ const navigate = useNavigate();
571
+ const pathname = useRouterState({ select: state => state.location.pathname });
572
+ const { product } = useParams({ strict: false });
573
+ const entry = PRODUCT_CONFIGS[product as Exclude<Product, 'home'>];
574
+ const handleNavigate = (to: string) => navigate({ to });
575
+
576
+ if (!entry) return <HomeContent />;
577
+
578
+ return (
579
+ <RemoteShell
580
+ // A product is its own remote: remount so no drill state carries over.
581
+ key={product}
582
+ config={entry.config}
583
+ basePath={`/${product}`}
584
+ pathname={pathname}
585
+ onNavigate={handleNavigate}
586
+ >
587
+ <RemoteShellPanel resizable />
588
+ <RemoteShellBreadcrumb />
589
+ <RemoteShellContent>
590
+ <Outlet />
591
+ </RemoteShellContent>
592
+ </RemoteShell>
593
+ );
594
+ };
595
+
596
+ const ROUTER_PAGE_LINKS = [
597
+ { to: '/edge/data-planes/production/services', label: 'Edge → Production → Services' },
598
+ {
599
+ to: '/edge/data-planes/staging/nodes/node-2/metrics',
600
+ label: 'Edge → Staging → Node 2 → Metrics',
601
+ },
602
+ { to: '/settings', label: 'Settings (another product)' },
603
+ ];
604
+
605
+ /** A routed page. Its router links bypass the shell — the panel and breadcrumbs still follow. */
606
+ const RouterProductPage: FC = () => {
607
+ const { breadcrumbSegments } = useRemoteShellContext();
608
+ const pathname = useRouterState({ select: state => state.location.pathname });
609
+ const title = breadcrumbSegments[breadcrumbSegments.length - 1]?.label ?? '';
610
+
611
+ return (
612
+ <Page title={title} fixedHeight>
613
+ <PageHeader>
614
+ <PageTitle>{title}</PageTitle>
615
+ </PageHeader>
616
+ <PageContent>
617
+ <div className='flex flex-col gap-12'>
618
+ <Text size='sm' color='secondary'>
619
+ Router pathname: <code>{pathname}</code>
620
+ </Text>
621
+ <ul className='flex flex-col gap-4'>
622
+ {ROUTER_PAGE_LINKS.map(link => (
623
+ <li key={link.to}>
624
+ <Link asChild>
625
+ <RouterLink to={link.to}>{link.label}</RouterLink>
626
+ </Link>
627
+ </li>
628
+ ))}
629
+ </ul>
630
+ </div>
631
+ </PageContent>
632
+ </Page>
633
+ );
634
+ };
635
+
636
+ const createShellRouter = () => {
637
+ const rootRoute = createRootRoute({ component: RouterRootLayout });
638
+ const homeRoute = createRoute({
639
+ getParentRoute: () => rootRoute,
640
+ path: 'home',
641
+ component: HomeContent,
642
+ });
643
+ const productRoute = createRoute({
644
+ getParentRoute: () => rootRoute,
645
+ path: '$product',
646
+ component: RouterProductRemote,
647
+ });
648
+ const productIndexRoute = createRoute({
649
+ getParentRoute: () => productRoute,
650
+ path: '/',
651
+ component: RouterProductPage,
652
+ });
653
+ const productPageRoute = createRoute({
654
+ getParentRoute: () => productRoute,
655
+ path: '$',
656
+ component: RouterProductPage,
657
+ });
658
+
659
+ return createRouter({
660
+ routeTree: rootRoute.addChildren([
661
+ homeRoute,
662
+ productRoute.addChildren([productIndexRoute, productPageRoute]),
663
+ ]),
664
+ history: createMemoryHistory({ initialEntries: ['/edge/overview'] }),
665
+ });
666
+ };
667
+
668
+ /**
669
+ * `AppShell` and a `RemoteShell` per product on one TanStack Router: the root route is
670
+ * the shell (rail → `navigate`, `AppShellRemote` → `<Outlet />`), `/$product` is the
671
+ * remote (`RemoteShell` with `pathname` + `onNavigate`, content → `<Outlet />`), and
672
+ * `/$product/$` is the page. Every way of moving — the rail, the nav panel, the
673
+ * breadcrumbs, links in the page — goes through the router, and the active items follow.
674
+ */
675
+ export const WithTanStackRouter: StoryFn<AppShellProps> = () => {
676
+ const [router] = useState(createShellRouter);
677
+
678
+ return <RouterProvider router={router} />;
679
+ };
680
+ ```
681
+
682
+ ### WallyAssistant/mocks/index.ts
683
+
684
+ ````tsx
685
+ /**
686
+ * Story and test fixtures for WallyAssistant, plus the custom SSE/REST mapper example from spec §10.2.
687
+ * Excluded from the build and the d.ts (`**\/mocks.{ts,tsx}`), like `Table/mocks.tsx`.
688
+ */
689
+ import { useEffect, useReducer, useRef } from 'react';
690
+ import { isRunningStatus } from '../model/deriveChatState';
691
+ import type {
692
+ ChatApprovalPart,
693
+ ChatApprovalResponse,
694
+ ChatAssistantMessage,
695
+ ChatAssistantPart,
696
+ ChatChainOfThoughtPart,
697
+ ChatMessage,
698
+ ChatSnapshot,
699
+ ChatStatus,
700
+ ChatStep,
701
+ ChatStepStatus,
702
+ } from '../model/model';
703
+
704
+ export type WallyEvent =
705
+ | { t: 'user'; id: string; text: string; at?: string }
706
+ /** Insert, or replace by id anywhere in the reply. */
707
+ | { t: 'step'; step: ChatStep; parentId?: string }
708
+ /** Deltas aren't idempotent: real streams carry offsets or snapshots. */
709
+ | { t: 'text'; delta: string }
710
+ /** Insert, or replace by id (resolving it too). */
711
+ | { t: 'approval'; approval: ChatApprovalPart }
712
+ /** "Worked for 12s", stamped on the first block. */
713
+ | { t: 'summary'; text: string }
714
+ | { t: 'error'; text: string }
715
+ | { t: 'end'; stopped?: boolean };
716
+
717
+ const LIVE = new Set<ChatStepStatus>(['pending', 'running', 'awaiting-approval']);
718
+
719
+ const upsert = <T extends { id: string }>(list: readonly T[], item: T): readonly T[] => {
720
+ const i = list.findIndex(x => x.id === item.id);
721
+ return i < 0 ? [...list, item] : list.map((x, j) => (j === i ? item : x));
722
+ };
723
+
724
+ const isCot = (p: ChatAssistantPart): p is ChatChainOfThoughtPart => p.type === 'chain-of-thought';
725
+
726
+ const hasStep = (steps: readonly ChatStep[], id: string): boolean =>
727
+ steps.some(s => s.id === id || hasStep(s.steps ?? [], id));
728
+
729
+ /** Depth-first map; untouched branches keep their identity. */
730
+ const mapSteps = (
731
+ steps: readonly ChatStep[],
732
+ f: (s: ChatStep) => ChatStep,
733
+ ): readonly ChatStep[] => {
734
+ const next = steps.map(s => {
735
+ const kids = s.steps && mapSteps(s.steps, f);
736
+ return f(kids === s.steps ? s : { ...s, steps: kids });
737
+ });
738
+ return next.every((s, i) => s === steps[i]) ? steps : next;
739
+ };
740
+
741
+ const mapBlocks = (
742
+ m: ChatAssistantMessage,
743
+ f: (steps: readonly ChatStep[]) => readonly ChatStep[],
744
+ ): ChatAssistantMessage => {
745
+ const parts = m.parts.map(p => {
746
+ if (!isCot(p)) return p;
747
+ const steps = f(p.steps);
748
+ return steps === p.steps ? p : { ...p, steps };
749
+ });
750
+ return parts.every((p, i) => p === m.parts[i]) ? m : { ...m, parts };
751
+ };
752
+
753
+ const addStep = (
754
+ m: ChatAssistantMessage,
755
+ step: ChatStep,
756
+ parentId?: string,
757
+ ): ChatAssistantMessage => {
758
+ const has = (id: string) => m.parts.some(p => isCot(p) && hasStep(p.steps, id));
759
+ if (has(step.id)) {
760
+ return mapBlocks(m, s =>
761
+ mapSteps(s, x => (x.id === step.id ? { ...step, steps: step.steps ?? x.steps } : x)),
762
+ );
763
+ }
764
+ if (parentId && has(parentId)) {
765
+ return mapBlocks(m, s =>
766
+ mapSteps(s, x => (x.id === parentId ? { ...x, steps: [...(x.steps ?? []), step] } : x)),
767
+ );
768
+ }
769
+ const tail = [...m.parts].reverse().find(p => p.type !== 'approval'); // approvals don't close a block
770
+ if (tail && isCot(tail)) {
771
+ return {
772
+ ...m,
773
+ parts: m.parts.map(p => (p === tail ? { ...tail, steps: [...tail.steps, step] } : p)),
774
+ };
775
+ }
776
+ // After text (or at the start): a new block.
777
+ return {
778
+ ...m,
779
+ parts: [
780
+ ...m.parts,
781
+ { type: 'chain-of-thought', id: `${m.id}:cot:${m.parts.length}`, steps: [step] },
782
+ ],
783
+ };
784
+ };
785
+
786
+ const settle = (m: ChatAssistantMessage, to: ChatStepStatus) =>
787
+ mapBlocks(m, s => mapSteps(s, x => (LIVE.has(x.status) ? { ...x, status: to } : x)));
788
+
789
+ const cancelApprovals = (m: ChatAssistantMessage): ChatAssistantMessage => ({
790
+ ...m,
791
+ parts: m.parts.map(p =>
792
+ p.type === 'approval' && p.status === 'pending'
793
+ ? { ...p, status: 'rejected' as const, resolvedLabel: 'Cancelled' }
794
+ : p,
795
+ ),
796
+ });
797
+
798
+ const lastUserId = (s: ChatSnapshot) => {
799
+ for (let i = s.messages.length - 1; i >= 0; i--) {
800
+ if (s.messages[i]?.role === 'user') return s.messages[i]?.id;
801
+ }
802
+ return undefined;
803
+ };
804
+
805
+ /** The reply to the last user message, created on demand; its id is the one WallyAssistant's loading row uses. */
806
+ const withReply = (
807
+ s: ChatSnapshot,
808
+ f: (m: ChatAssistantMessage) => ChatAssistantMessage,
809
+ status: ChatStatus = 'streaming',
810
+ ): ChatSnapshot => {
811
+ const id = `${lastUserId(s) ?? 'boot'}:reply`;
812
+ const prev = s.messages.find(m => m.id === id);
813
+ const reply: ChatAssistantMessage =
814
+ prev?.role === 'assistant' ? prev : { role: 'assistant', id, parts: [] };
815
+ return { ...s, status, messages: upsert(s.messages, f(reply)) }; // untouched messages keep their identity
816
+ };
817
+
818
+ /** Folds one backend event into the next immutable snapshot (spec §10.2). */
819
+ export function reduceWally(s: ChatSnapshot, e: WallyEvent): ChatSnapshot {
820
+ switch (e.t) {
821
+ case 'user':
822
+ return {
823
+ ...s,
824
+ status: 'submitted',
825
+ messages: upsert(s.messages, {
826
+ role: 'user',
827
+ id: e.id,
828
+ createdAt: e.at,
829
+ parts: [{ type: 'text', id: `${e.id}:text`, text: e.text }],
830
+ }),
831
+ };
832
+ case 'step':
833
+ return withReply(s, m => addStep(m, e.step, e.parentId));
834
+ case 'text':
835
+ return withReply(s, m => {
836
+ const last = m.parts[m.parts.length - 1];
837
+ return last?.type === 'text'
838
+ ? { ...m, parts: [...m.parts.slice(0, -1), { ...last, text: last.text + e.delta }] }
839
+ : {
840
+ ...m,
841
+ parts: [
842
+ ...m.parts,
843
+ { type: 'text', id: `${m.id}:text:${m.parts.length}`, text: e.delta },
844
+ ],
845
+ };
846
+ });
847
+ case 'approval':
848
+ return withReply(s, m => {
849
+ const n = { ...m, parts: upsert(m.parts, e.approval) };
850
+ if (e.approval.status === 'pending') return n;
851
+ // ponytail: approvals don't name their step, so a resolution settles every HITL step in the reply
852
+ const to = e.approval.status === 'approved' ? 'done' : 'denied';
853
+ return mapBlocks(n, st =>
854
+ mapSteps(st, x => (x.status === 'awaiting-approval' ? { ...x, status: to } : x)),
855
+ );
856
+ });
857
+ case 'summary':
858
+ return withReply(s, m => {
859
+ const first = m.parts.find(isCot);
860
+ return first
861
+ ? { ...m, parts: m.parts.map(p => (p === first ? { ...first, summary: e.text } : p)) }
862
+ : m;
863
+ });
864
+ case 'error':
865
+ return withReply(
866
+ s,
867
+ m => {
868
+ const n = settle(m, 'error');
869
+ return {
870
+ ...n,
871
+ parts: upsert(n.parts, {
872
+ type: 'error',
873
+ id: `${m.id}:error`,
874
+ text: e.text,
875
+ retryable: true,
876
+ }),
877
+ };
878
+ },
879
+ 'error',
880
+ );
881
+ case 'end':
882
+ return e.stopped
883
+ ? withReply(s, m => cancelApprovals(settle(m, 'cancelled')), 'ready')
884
+ : { ...s, status: 'ready' };
885
+ default:
886
+ return s; // unknown events are ignored: forward compatible
887
+ }
888
+ }
889
+
890
+ // ---------------------------------------------------------------------------------------------------------------
891
+ // Script
892
+ // ---------------------------------------------------------------------------------------------------------------
893
+
894
+ /** About 30ms per token at `speed: 1`. */
895
+ const TOKEN_MS = 30;
896
+ const tokens = (text: string) => text.match(/\S+\s*/g) ?? [];
897
+
898
+ const REASONING =
899
+ 'The user wants the noisiest attack sources. I will query the last 24 hours of attack events, ' +
900
+ 'check which applications they hit, then read the policy to see what is already blocked.';
901
+ const ANSWER =
902
+ 'Over the last 24 hours `api.example.com` received 1,204 attacks, mostly SQL injection from 3 IPs.\n\n' +
903
+ 'Two of those IPs are already on the denylist. I can propose a rule for the third one, `203.0.113.7`.';
904
+
905
+ const reasoningStep = (status: ChatStepStatus, label: string, content?: string): ChatStep => ({
906
+ id: 'reasoning',
907
+ kind: 'reasoning',
908
+ status,
909
+ label,
910
+ content,
911
+ });
912
+
913
+ const tool = (id: string, icon: string, running: string, done: string, metric?: string) =>
914
+ [
915
+ [500, { t: 'step', step: { id, kind: 'tool', icon, status: 'running', label: running } }],
916
+ [900, { t: 'step', step: { id, kind: 'tool', icon, status: 'done', label: done, metric } }],
917
+ ] satisfies [number, WallyEvent][];
918
+
919
+ /** `[delayMs, event]` pairs replayed by `useMockWally` after the user event (Streaming Simulation story). */
920
+ export const WALLY_SCRIPT: readonly (readonly [number, WallyEvent])[] = [
921
+ [400, { t: 'step', step: reasoningStep('running', 'Thinking...') }],
922
+ ...tokens(REASONING).map(
923
+ (_, i, all) =>
924
+ [
925
+ TOKEN_MS,
926
+ { t: 'step', step: reasoningStep('running', 'Thinking...', all.slice(0, i + 1).join('')) },
927
+ ] as const,
928
+ ),
929
+ [200, { t: 'step', step: reasoningStep('done', 'Thought for 3s', REASONING) }],
930
+ ...tool('query', 'query', 'Querying attack events...', 'Queried attack events', '1,204'),
931
+ ...tool('apps', 'scan', 'Checking applications...', 'Checked 3 applications'),
932
+ ...tool(
933
+ 'policy',
934
+ 'read',
935
+ 'Reading the policy for api.example.com...',
936
+ 'Read the policy for api.example.com',
937
+ ),
938
+ ...tokens(ANSWER).map((delta, i) => [i === 0 ? 400 : TOKEN_MS, { t: 'text', delta }] as const),
939
+ [300, { t: 'summary', text: 'Worked for 12s' }],
940
+ [100, { t: 'end' }],
941
+ ];
942
+
943
+ // ---------------------------------------------------------------------------------------------------------------
944
+ // Fixtures
945
+ // ---------------------------------------------------------------------------------------------------------------
946
+
947
+ const userText = (id: string, text: string, createdAt?: string): ChatMessage => ({
948
+ role: 'user',
949
+ id,
950
+ createdAt,
951
+ parts: [{ type: 'text', id: `${id}:text`, text }],
952
+ });
953
+
954
+ const SUGGESTIONS: ChatSnapshot['suggestions'] = [
955
+ { id: 'top-attacks', label: 'Show top attacks today' },
956
+ {
957
+ id: 'blocked',
958
+ label: 'Which requests were blocked?',
959
+ prompt: 'Which requests were blocked in the last hour?',
960
+ },
961
+ { id: 'rule', label: 'Create a mitigation rule' },
962
+ ];
963
+
964
+ const FOLLOW_UPS: ChatSnapshot['suggestions'] = [
965
+ { id: 'deny', label: 'Add 203.0.113.7 to the denylist' },
966
+ { id: 'details', label: 'Show attack details' },
967
+ ];
968
+
969
+ /** No messages: the greeting with suggestion pills. */
970
+ export const EMPTY: ChatSnapshot = { messages: [], status: 'ready', suggestions: SUGGESTIONS };
971
+
972
+ /** Two finished turns plus follow-up suggestions and tool permissions. Timestamps are fixed: pass a fixed
973
+ * `formatTimestamp` in screenshots. */
974
+ export const CONVERSATION: ChatSnapshot = {
975
+ status: 'ready',
976
+ messages: [
977
+ userText('u1', 'Show me the top attacks from the last 24 hours', '2026-10-21T09:00:00.000Z'),
978
+ {
979
+ role: 'assistant',
980
+ id: 'u1:reply',
981
+ createdAt: '2026-10-21T09:00:12.000Z',
982
+ parts: [
983
+ {
984
+ type: 'chain-of-thought',
985
+ id: 'u1:reply:cot:0',
986
+ summary: 'Worked for 12s',
987
+ steps: [
988
+ reasoningStep('done', 'Thought for 3s', REASONING),
989
+ {
990
+ id: 'query',
991
+ kind: 'tool',
992
+ icon: 'query',
993
+ status: 'done',
994
+ label: 'Queried attack events',
995
+ metric: '1,204',
996
+ },
997
+ {
998
+ id: 'apps',
999
+ kind: 'tool',
1000
+ icon: 'scan',
1001
+ status: 'done',
1002
+ label: 'Checked 3 applications',
1003
+ },
1004
+ ],
1005
+ },
1006
+ { type: 'text', id: 'u1:reply:text:1', text: ANSWER },
1007
+ ],
1008
+ },
1009
+ userText('u2', 'Which of them were blocked?', '2026-10-21T09:01:00.000Z'),
1010
+ {
1011
+ role: 'assistant',
1012
+ id: 'u2:reply',
1013
+ createdAt: '2026-10-21T09:01:05.000Z',
1014
+ feedback: 'positive',
1015
+ parts: [
1016
+ {
1017
+ type: 'chain-of-thought',
1018
+ id: 'u2:reply:cot:0',
1019
+ steps: [{ id: 'reasoning', kind: 'reasoning', status: 'done', label: 'Thought for 2s' }],
1020
+ },
1021
+ {
1022
+ type: 'text',
1023
+ id: 'u2:reply:text:1',
1024
+ text: '1,019 of the 1,204 attacks were blocked by the `sqli-default` rule. The other 185 came from `203.0.113.7`, which is not on the denylist yet.',
1025
+ },
1026
+ ],
1027
+ },
1028
+ ],
1029
+ suggestions: FOLLOW_UPS,
1030
+ permissions: [
1031
+ {
1032
+ id: 'deny',
1033
+ title: 'propose_add_to_denylist',
1034
+ description: 'Propose a denylist entry for an IP',
1035
+ allowed: true,
1036
+ },
1037
+ { id: 'rules', title: 'create_mitigation_rule', allowed: false },
1038
+ ],
1039
+ };
1040
+
1041
+ /** A reply waiting on a pending approval. `status` stays 'ready', as with CopilotKit HITL tools (§7.5). */
1042
+ export const APPROVAL_PENDING: ChatSnapshot = {
1043
+ status: 'ready',
1044
+ messages: [
1045
+ userText('u1', 'Block 203.0.113.7 for a day'),
1046
+ {
1047
+ role: 'assistant',
1048
+ id: 'u1:reply',
1049
+ parts: [
1050
+ {
1051
+ type: 'chain-of-thought',
1052
+ id: 'u1:reply:cot:0',
1053
+ steps: [
1054
+ {
1055
+ id: 'policy',
1056
+ kind: 'tool',
1057
+ icon: 'read',
1058
+ status: 'done',
1059
+ label: 'Read the policy for api.example.com',
1060
+ },
1061
+ {
1062
+ id: 'deny',
1063
+ kind: 'tool',
1064
+ name: 'propose_add_to_denylist',
1065
+ icon: 'create',
1066
+ status: 'awaiting-approval',
1067
+ label: 'Proposing a denylist entry...',
1068
+ },
1069
+ ],
1070
+ },
1071
+ {
1072
+ type: 'approval',
1073
+ id: 'u1:reply:approval',
1074
+ status: 'pending',
1075
+ content:
1076
+ 'Wally wants to run `propose_add_to_denylist`:\n\n```json\n{ "ip": "203.0.113.7", "duration": "24h" }\n```',
1077
+ },
1078
+ ],
1079
+ },
1080
+ ],
1081
+ };
1082
+
1083
+ /** A run that failed before any output: an assistant message with only a retryable error part. */
1084
+ export const ERROR_TURN: ChatSnapshot = {
1085
+ status: 'error',
1086
+ messages: [
1087
+ userText('u1', 'Show me the top attacks from the last 24 hours'),
1088
+ {
1089
+ role: 'assistant',
1090
+ id: 'u1:reply',
1091
+ parts: [
1092
+ {
1093
+ type: 'error',
1094
+ id: 'u1:reply:error',
1095
+ text: "Wally couldn't reach the attack store.",
1096
+ retryable: true,
1097
+ },
1098
+ ],
1099
+ },
1100
+ ],
1101
+ };
1102
+
1103
+ /** `n` finished messages, alternating user and assistant, for the perf test. */
1104
+ export const longThread = (n: number): ChatSnapshot => ({
1105
+ status: 'ready',
1106
+ messages: Array.from({ length: n }, (_, i): ChatMessage => {
1107
+ const turn = Math.floor(i / 2) + 1;
1108
+ return i % 2 === 0
1109
+ ? userText(`u${turn}`, `Question ${turn}`)
1110
+ : {
1111
+ role: 'assistant',
1112
+ id: `u${turn}:reply`,
1113
+ parts: [{ type: 'text', id: `u${turn}:reply:text:0`, text: `Answer ${turn}` }],
1114
+ };
1115
+ }),
1116
+ });
1117
+
1118
+ // ---------------------------------------------------------------------------------------------------------------
1119
+ // Hook
1120
+ // ---------------------------------------------------------------------------------------------------------------
1121
+
1122
+ export interface UseMockWallyOptions {
1123
+ /** Default `EMPTY`. */
1124
+ initial?: ChatSnapshot;
1125
+ /** Default `WALLY_SCRIPT`. */
1126
+ script?: readonly (readonly [number, WallyEvent])[];
1127
+ /** Delay multiplier: 1 = about 30ms per token, 0 = as fast as timers allow. Default 1. */
1128
+ speed?: number;
1129
+ }
1130
+
1131
+ export interface MockWally {
1132
+ snapshot: ChatSnapshot;
1133
+ /** Appends a user message (`u1`, `u2`, …) and replays the script. */
1134
+ send: (text: string) => void;
1135
+ /** Clears the timers, cancels live steps and pending approvals. */
1136
+ stop: () => void;
1137
+ /** Same signature as `onApprovalRespond`: resolves the approval in the next snapshot. */
1138
+ respond: (response: ChatApprovalResponse, approval: ChatApprovalPart) => void;
1139
+ }
1140
+
1141
+ /** Replays `script` against `reduceWally` with a `setTimeout` chain. Deterministic ids for stable screenshots. */
1142
+ export function useMockWally({
1143
+ initial = EMPTY,
1144
+ script = WALLY_SCRIPT,
1145
+ speed = 1,
1146
+ }: UseMockWallyOptions = {}): MockWally {
1147
+ const [snapshot, dispatch] = useReducer(reduceWally, initial);
1148
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
1149
+
1150
+ useEffect(() => () => clearTimeout(timer.current), []);
1151
+
1152
+ const play = (i: number) => {
1153
+ const entry = script[i];
1154
+ if (!entry) return;
1155
+ const [delay, event] = entry;
1156
+ timer.current = setTimeout(() => {
1157
+ dispatch(event);
1158
+ play(i + 1);
1159
+ }, delay * speed);
1160
+ };
1161
+
1162
+ const send = (text: string) => {
1163
+ clearTimeout(timer.current);
1164
+ const n = snapshot.messages.filter(m => m.role === 'user').length + 1;
1165
+ dispatch({ t: 'user', id: `u${n}`, text });
1166
+ play(0);
1167
+ };
1168
+
1169
+ const stop = () => {
1170
+ clearTimeout(timer.current);
1171
+ dispatch({ t: 'end', stopped: true });
1172
+ };
1173
+
1174
+ const respond = (response: ChatApprovalResponse, approval: ChatApprovalPart) => {
1175
+ dispatch({
1176
+ t: 'approval',
1177
+ approval: { ...approval, status: response.approved ? 'approved' : 'rejected' },
1178
+ });
1179
+ // The reducer marks the reply as streaming; outside a run, put the status back to 'ready'.
1180
+ if (!isRunningStatus(snapshot.status)) dispatch({ t: 'end' });
1181
+ };
1182
+
1183
+ return { snapshot, send, stop, respond };
1184
+ }
1185
+ ````
1186
+
1187
+ ### AppShell/story-content/index.ts
1188
+
1189
+ ```tsx
1190
+ export { AccountDropdown, railModeFor, type SidebarMode } from './_storyAccountDropdown';
1191
+ export { ConfigRemote, type ConfigRemoteProps, RemoteForProduct } from './_storyConfigRenderer';
1192
+ export { HeaderActions } from './_storyHeaderActions';
1193
+ export { HomeContent } from './_storyHomeContent';
1194
+ export { deriveProduct, PRODUCT_CONFIGS, type Product, productLandingPath } from './_storyLib';
1195
+ export {
1196
+ aiHypervisorNavConfig,
1197
+ edgeNavConfig,
1198
+ infraDiscoveryNavConfig,
1199
+ securityTestingNavConfig,
1200
+ settingsNavConfig,
1201
+ } from './_storyNavConfigs';
1202
+ export { NavRailFooterContent } from './_storyNavRailFooter';
1203
+ export { NotificationsInbox } from './_storyNotificationsInbox';
1204
+ export { ProductNavItems } from './_storyProductNavItems';
1205
+ export { QuickHelpDropdown } from './_storyQuickHelpDropdown';
1206
+ export { RecentDropdown } from './_storyRecentDropdown';
1207
+ export { StoryRouter, useStoryPathname, useStoryProductNavigation } from './_storyRouter';
1208
+ export { WallarmLogo } from './_storyWallarmLogo';
1209
+ ```
1210
+
1211
+ ### WallyAssistant/model/deriveChatState.ts
1212
+
1213
+ ```tsx
1214
+ import type {
1215
+ ChatApprovalPart,
1216
+ ChatId,
1217
+ ChatMessage,
1218
+ ChatStatus,
1219
+ ChatStep,
1220
+ ChatStepStatus,
1221
+ } from './model';
1222
+
1223
+ export interface ChatViewState {
1224
+ /** `messages`, plus an empty reply `{ role: 'assistant', id: `${lastUserMessageId}:reply`, parts: [] }` while
1225
+ * running and the last message is the user's (the loading row; the same id as the recommended mapper reply). */
1226
+ rows: readonly ChatMessage[];
1227
+ /** `messages.length === 0`. */
1228
+ empty: boolean;
1229
+ /** `status` is 'submitted' or 'streaming'. */
1230
+ running: boolean;
1231
+ /** The last row has a pending approval part. */
1232
+ awaitingApproval: boolean;
1233
+ /** `running && !awaitingApproval` → `aria-busy`. */
1234
+ busy: boolean;
1235
+ /** → Conversation `autoScrollKey`. */
1236
+ lastUserMessageId?: ChatId;
1237
+ /** Placement 'docked' only (§7.5). Render each with
1238
+ * `<ChatPartView part message={rows.at(-1)} isLast placement='docked' />`. */
1239
+ dockedApprovals: readonly ChatApprovalPart[];
1240
+ }
1241
+
1242
+ const LIVE_STEP_STATUSES: ReadonlySet<string> = new Set<ChatStepStatus>([
1243
+ 'pending',
1244
+ 'running',
1245
+ 'awaiting-approval',
1246
+ ]);
1247
+ const NO_APPROVALS: readonly ChatApprovalPart[] = [];
1248
+
1249
+ /** 'submitted' | 'streaming'. Unknown statuses behave like 'ready'. */
1250
+ export const isRunningStatus = (status: ChatStatus | undefined): boolean =>
1251
+ status === 'submitted' || status === 'streaming';
1252
+
1253
+ /** The step's own status is pending, running or awaiting-approval. */
1254
+ export const isLiveStepStatus = (status: ChatStepStatus): boolean => LIVE_STEP_STATUSES.has(status);
1255
+
1256
+ /** The step or any nested step is live (§7.3). */
1257
+ export const isLiveStep = (step: ChatStep): boolean =>
1258
+ isLiveStepStatus(step.status) || (step.steps?.some(isLiveStep) ?? false);
1259
+
1260
+ const approvalsOf = (message: ChatMessage | undefined): ChatApprovalPart[] =>
1261
+ message?.role === 'assistant'
1262
+ ? message.parts.filter((p): p is ChatApprovalPart => p.type === 'approval')
1263
+ : [];
1264
+
1265
+ export const hasPendingApproval = (message: ChatMessage | undefined): boolean =>
1266
+ approvalsOf(message).some(p => p.status === 'pending');
1267
+
1268
+ /** The last row's pending approvals; when none is pending, its last approval (decision 11). */
1269
+ export const getDockedApprovals = (
1270
+ lastRow: ChatMessage | undefined,
1271
+ ): readonly ChatApprovalPart[] => {
1272
+ const approvals = approvalsOf(lastRow);
1273
+ const pending = approvals.filter(p => p.status === 'pending');
1274
+ if (pending.length) return pending;
1275
+ const last = approvals[approvals.length - 1];
1276
+ return last ? [last] : NO_APPROVALS;
1277
+ };
1278
+
1279
+ /** What Copy copies: `copyText`, or the non-empty text parts joined by a blank line. */
1280
+ export const copyTextOf = (message: ChatMessage): string =>
1281
+ (message.role === 'assistant' && message.copyText) ||
1282
+ message.parts.flatMap(p => (p.type === 'text' && p.text ? [p.text] : [])).join('\n\n');
1283
+
1284
+ const lastUserIdOf = (messages: readonly ChatMessage[]): ChatId | undefined => {
1285
+ for (let i = messages.length - 1; i >= 0; i--) {
1286
+ const m = messages[i];
1287
+ if (m?.role === 'user') return m.id;
1288
+ }
1289
+ return undefined;
1290
+ };
1291
+
1292
+ /** Every derived rule of the chat, in one pure function shared by WallyAssistant and custom layouts (§7.1).
1293
+ * `approvalPlacement` must match the `ChatViewProvider`'s, or a docked approval renders twice or never. */
1294
+ export function deriveChatState(
1295
+ messages: readonly ChatMessage[],
1296
+ status: ChatStatus = 'ready',
1297
+ approvalPlacement: 'docked' | 'inline' = 'inline',
1298
+ ): ChatViewState {
1299
+ const running = isRunningStatus(status);
1300
+ const lastUserMessageId = lastUserIdOf(messages);
1301
+ const last = messages[messages.length - 1];
1302
+ const rows: readonly ChatMessage[] =
1303
+ running && last?.role === 'user'
1304
+ ? [...messages, { role: 'assistant', id: `${last.id}:reply`, parts: [] }]
1305
+ : messages;
1306
+ const lastRow = rows[rows.length - 1];
1307
+ const awaitingApproval = hasPendingApproval(lastRow);
1308
+
1309
+ return {
1310
+ rows,
1311
+ empty: messages.length === 0,
1312
+ running,
1313
+ awaitingApproval,
1314
+ busy: running && !awaitingApproval,
1315
+ lastUserMessageId,
1316
+ dockedApprovals: approvalPlacement === 'docked' ? getDockedApprovals(lastRow) : NO_APPROVALS,
1317
+ };
1318
+ }
1319
+ ```
1320
+
1321
+ ### AppShell/story-content/_storyAccountDropdown.tsx
1322
+
1323
+ ```tsx
1324
+ import { Fragment } from 'react';
1325
+ import { LogOut, PanelLeftDashed, PencilRuler, Settings, UserRound } from '../../../icons';
1326
+ import { cn } from '../../../utils/cn';
1327
+ import {
1328
+ Avatar,
1329
+ AvatarContent,
1330
+ AvatarDescription,
1331
+ AvatarFallback,
1332
+ AvatarImage,
1333
+ AvatarName,
1334
+ } from '../../Avatar';
1335
+ import {
1336
+ DropdownMenu,
1337
+ DropdownMenuContent,
1338
+ DropdownMenuItem,
1339
+ DropdownMenuItemContent,
1340
+ DropdownMenuItemIcon,
1341
+ DropdownMenuItemText,
1342
+ DropdownMenuRadioGroup,
1343
+ DropdownMenuRadioItem,
1344
+ DropdownMenuSeparator,
1345
+ DropdownMenuTrigger,
1346
+ DropdownMenuTriggerItem,
1347
+ } from '../../DropdownMenu';
1348
+ import { NavRailItem, type NavRailMode } from '../../NavRail';
1349
+ import { Text } from '../../Text';
1350
+ import { type FrameStyle, type Theme, useTheme } from '../../ThemeProvider';
1351
+
1352
+ export type SidebarMode = 'adaptive' | 'expanded' | 'collapsed' | 'compact';
1353
+
1354
+ /** Adaptive collapses the rail inside a product and expands it on Home; the other modes pin it. */
1355
+ export const railModeFor = (mode: SidebarMode, onHome: boolean): NavRailMode => {
1356
+ if (mode === 'adaptive') return onHome ? 'expanded' : 'collapsed';
1357
+ return mode;
1358
+ };
1359
+
1360
+ const USER_NAME = 'Meow Meow';
1361
+ const USER_EMAIL = 'meow@meow.com';
1362
+
1363
+ const THEMES: Theme[] = ['light', 'dark'];
1364
+ // Adaptive on its own, then the two pinned widths, then compact.
1365
+ const SIDEBAR_MODE_GROUPS: SidebarMode[][] = [['adaptive'], ['expanded', 'collapsed'], ['compact']];
1366
+ const SIDEBAR_MODES = SIDEBAR_MODE_GROUPS.flat();
1367
+
1368
+ const THEME_LABELS: Record<Theme, string> = {
1369
+ light: 'Light',
1370
+ dark: 'Dark',
1371
+ };
1372
+
1373
+ const FRAME_STYLES: FrameStyle[] = ['neutral', 'branded'];
1374
+
1375
+ const FRAME_STYLE_LABELS: Record<FrameStyle, string> = {
1376
+ neutral: 'Neutral',
1377
+ branded: 'Branded',
1378
+ };
1379
+
1380
+ // The swatch in front of each Frame style option, as in Figma.
1381
+ const FRAME_STYLE_SWATCH: Record<FrameStyle, string> = {
1382
+ neutral: 'bg-icon-secondary',
1383
+ branded: 'bg-icon-brand',
1384
+ };
1385
+
1386
+ const SIDEBAR_MODE_LABELS: Record<SidebarMode, string> = {
1387
+ adaptive: 'Adaptive',
1388
+ expanded: 'Always expanded',
1389
+ collapsed: 'Always collapsed',
1390
+ compact: 'Compact',
1391
+ };
1392
+
1393
+ export const AccountDropdown = ({
1394
+ sidebarMode,
1395
+ onSidebarModeChange,
1396
+ theme,
1397
+ onThemeChange,
1398
+ }: {
1399
+ sidebarMode: SidebarMode;
1400
+ onSidebarModeChange: (mode: SidebarMode) => void;
1401
+ theme: Theme;
1402
+ onThemeChange: (theme: Theme) => void;
1403
+ }) => {
1404
+ const { frameStyle, setFrameStyle } = useTheme();
1405
+
1406
+ return (
1407
+ <DropdownMenu positioning={{ placement: 'right-end', gutter: 6, offset: { crossAxis: 12 } }}>
1408
+ <DropdownMenuTrigger>
1409
+ <NavRailItem icon={UserRound} label={USER_NAME} avatar />
1410
+ </DropdownMenuTrigger>
1411
+ <DropdownMenuContent className='w-256'>
1412
+ <div className='rounded-6 border border-border-primary-light bg-states-primary-hover px-8 py-8 mb-4'>
1413
+ <Avatar size='md'>
1414
+ <AvatarImage />
1415
+ <AvatarFallback name={USER_NAME} />
1416
+ <AvatarContent>
1417
+ <AvatarName>{USER_NAME}</AvatarName>
1418
+ <AvatarDescription>{USER_EMAIL}</AvatarDescription>
1419
+ </AvatarContent>
1420
+ </Avatar>
1421
+ </div>
1422
+ <DropdownMenuItem>
1423
+ <DropdownMenuItemIcon>
1424
+ <Settings />
1425
+ </DropdownMenuItemIcon>
1426
+ <DropdownMenuItemText>Profile settings</DropdownMenuItemText>
1427
+ </DropdownMenuItem>
1428
+ <DropdownMenu>
1429
+ <DropdownMenuTriggerItem>
1430
+ <DropdownMenuItemIcon>
1431
+ <PencilRuler />
1432
+ </DropdownMenuItemIcon>
1433
+ <DropdownMenuItemContent>
1434
+ <DropdownMenuItemText>Appearance</DropdownMenuItemText>
1435
+ <Text size='xs' color='secondary'>
1436
+ {THEME_LABELS[theme]} and {FRAME_STYLE_LABELS[frameStyle].toLowerCase()}
1437
+ </Text>
1438
+ </DropdownMenuItemContent>
1439
+ </DropdownMenuTriggerItem>
1440
+ <DropdownMenuContent className='w-132'>
1441
+ <DropdownMenuRadioGroup
1442
+ value={theme}
1443
+ onValueChange={({ value }) => {
1444
+ const next = THEMES.find(t => t === value);
1445
+ if (next) onThemeChange(next);
1446
+ }}
1447
+ >
1448
+ {THEMES.map(t => (
1449
+ <DropdownMenuRadioItem key={t} value={t}>
1450
+ <DropdownMenuItemText>{THEME_LABELS[t]}</DropdownMenuItemText>
1451
+ </DropdownMenuRadioItem>
1452
+ ))}
1453
+ </DropdownMenuRadioGroup>
1454
+ <DropdownMenuSeparator />
1455
+ <DropdownMenuRadioGroup
1456
+ value={frameStyle}
1457
+ onValueChange={({ value }) => {
1458
+ const next = FRAME_STYLES.find(s => s === value);
1459
+ if (next) setFrameStyle(next);
1460
+ }}
1461
+ >
1462
+ {FRAME_STYLES.map(s => (
1463
+ <DropdownMenuRadioItem key={s} value={s}>
1464
+ <DropdownMenuItemIcon>
1465
+ <span className='flex size-16 items-center justify-center'>
1466
+ <span className={cn('size-8 rounded-full', FRAME_STYLE_SWATCH[s])} />
1467
+ </span>
1468
+ </DropdownMenuItemIcon>
1469
+ <DropdownMenuItemText>{FRAME_STYLE_LABELS[s]}</DropdownMenuItemText>
1470
+ </DropdownMenuRadioItem>
1471
+ ))}
1472
+ </DropdownMenuRadioGroup>
1473
+ </DropdownMenuContent>
1474
+ </DropdownMenu>
1475
+ <DropdownMenu>
1476
+ <DropdownMenuTriggerItem>
1477
+ <DropdownMenuItemIcon>
1478
+ <PanelLeftDashed />
1479
+ </DropdownMenuItemIcon>
1480
+ <DropdownMenuItemContent>
1481
+ <DropdownMenuItemText>Sidebar mode</DropdownMenuItemText>
1482
+ <Text size='xs' color='secondary'>
1483
+ {SIDEBAR_MODE_LABELS[sidebarMode]}
1484
+ </Text>
1485
+ </DropdownMenuItemContent>
1486
+ </DropdownMenuTriggerItem>
1487
+ <DropdownMenuContent className='w-200'>
1488
+ <DropdownMenuRadioGroup
1489
+ value={sidebarMode}
1490
+ onValueChange={({ value }) => {
1491
+ const next = SIDEBAR_MODES.find(m => m === value);
1492
+ if (next) onSidebarModeChange(next);
1493
+ }}
1494
+ >
1495
+ {SIDEBAR_MODE_GROUPS.map((group, index) => (
1496
+ <Fragment key={group.join('-')}>
1497
+ {index > 0 && <DropdownMenuSeparator />}
1498
+ {group.map(m => (
1499
+ <DropdownMenuRadioItem key={m} value={m}>
1500
+ <DropdownMenuItemText>{SIDEBAR_MODE_LABELS[m]}</DropdownMenuItemText>
1501
+ </DropdownMenuRadioItem>
1502
+ ))}
1503
+ </Fragment>
1504
+ ))}
1505
+ </DropdownMenuRadioGroup>
1506
+ </DropdownMenuContent>
1507
+ </DropdownMenu>
1508
+ <DropdownMenuSeparator />
1509
+ <DropdownMenuItem>
1510
+ <DropdownMenuItemIcon>
1511
+ <LogOut />
1512
+ </DropdownMenuItemIcon>
1513
+ <DropdownMenuItemText>Sign out</DropdownMenuItemText>
1514
+ </DropdownMenuItem>
1515
+ </DropdownMenuContent>
1516
+ </DropdownMenu>
1517
+ );
1518
+ };
1519
+ ```
1520
+
1521
+ ### AppShell/story-content/_storyConfigRenderer.tsx
1522
+
1523
+ ```tsx
1524
+ import { type FC, type ReactNode, useEffect, useState } from 'react';
1525
+ import { useNavigate } from '@tanstack/react-router';
1526
+ import { Page, PageContent, PageHeader, PageTitle } from '../../Page';
1527
+ import {
1528
+ type NavConfig,
1529
+ RemoteShell,
1530
+ RemoteShellBreadcrumb,
1531
+ RemoteShellContent,
1532
+ RemoteShellPanel,
1533
+ useRemoteShellContext,
1534
+ } from '../../RemoteShell';
1535
+ import { HomeContent } from './_storyHomeContent';
1536
+ import { PRODUCT_CONFIGS, type Product } from './_storyLib';
1537
+ import { useStoryPathname } from './_storyRouter';
1538
+
1539
+ export interface ConfigRemoteProps {
1540
+ config: NavConfig;
1541
+ basePath?: string;
1542
+ /** Fake a 2s load when the product opens, to show the panel and content skeletons. */
1543
+ simulateLoading?: boolean;
1544
+ /** Page content; defaults to a placeholder page titled after the breadcrumbs. */
1545
+ children?: ReactNode;
1546
+ }
1547
+
1548
+ const RemotePageContent: FC = () => {
1549
+ const { breadcrumbSegments } = useRemoteShellContext();
1550
+
1551
+ const lastSegment = breadcrumbSegments[breadcrumbSegments.length - 1];
1552
+ const pageTitle = lastSegment?.label ?? '';
1553
+ const fullPath = breadcrumbSegments.map(s => s.label).join(' / ');
1554
+
1555
+ return (
1556
+ <Page title={pageTitle} fixedHeight>
1557
+ <PageHeader>
1558
+ <PageTitle>{pageTitle}</PageTitle>
1559
+ </PageHeader>
1560
+ <PageContent>
1561
+ <p className='text-sm text-text-secondary'>Placeholder page for {fullPath}.</p>
1562
+ </PageContent>
1563
+ </Page>
1564
+ );
1565
+ };
1566
+
1567
+ /**
1568
+ * A product remote wired to TanStack Router the recommended way: the router owns the
1569
+ * URL, `RemoteShell` gets the router's pathname (`pathname`) and hands navigations
1570
+ * back to it (`onNavigate`). Must render inside a router (`StoryRouter`).
1571
+ */
1572
+ export const ConfigRemote: FC<ConfigRemoteProps> = ({
1573
+ config,
1574
+ basePath,
1575
+ simulateLoading = true,
1576
+ children,
1577
+ }) => {
1578
+ const navigate = useNavigate();
1579
+ const pathname = useStoryPathname();
1580
+ const [loading, setLoading] = useState(simulateLoading);
1581
+
1582
+ // Remounted per product (see `RemoteForProduct`), so this runs once per product.
1583
+ useEffect(() => {
1584
+ if (!simulateLoading) return;
1585
+ const timer = setTimeout(() => setLoading(false), 2000);
1586
+ return () => clearTimeout(timer);
1587
+ }, [simulateLoading]);
1588
+
1589
+ return (
1590
+ <RemoteShell
1591
+ config={config}
1592
+ basePath={basePath}
1593
+ pathname={pathname}
1594
+ onNavigate={to => navigate({ to })}
1595
+ >
1596
+ {loading ? (
1597
+ <>
1598
+ <RemoteShellPanel isLoading />
1599
+ <RemoteShellContent isLoading />
1600
+ </>
1601
+ ) : (
1602
+ <>
1603
+ <RemoteShellPanel resizable />
1604
+ <RemoteShellBreadcrumb />
1605
+ <RemoteShellContent>{children ?? <RemotePageContent />}</RemoteShellContent>
1606
+ </>
1607
+ )}
1608
+ </RemoteShell>
1609
+ );
1610
+ };
1611
+
1612
+ export const RemoteForProduct = ({
1613
+ product,
1614
+ simulateLoading = true,
1615
+ }: {
1616
+ product: Product;
1617
+ simulateLoading?: boolean;
1618
+ }) => {
1619
+ if (product === 'home') return <HomeContent />;
1620
+ const { config } = PRODUCT_CONFIGS[product];
1621
+ // Keyed by product: each product is its own remote, so its drill-transition and
1622
+ // loading state never carry over to the next one.
1623
+ return (
1624
+ <ConfigRemote
1625
+ key={product}
1626
+ config={config}
1627
+ basePath={`/${product}`}
1628
+ simulateLoading={simulateLoading}
1629
+ />
1630
+ );
1631
+ };
1632
+ ```
1633
+
1634
+ ### AppShell/story-content/_storyHeaderActions.tsx
1635
+
1636
+ ```tsx
1637
+ import type { ReactNode } from 'react';
1638
+ import { ChevronUpDown } from '../../../icons';
1639
+ import { Button } from '../../Button';
1640
+ import { Code } from '../../Code';
1641
+ import { Text } from '../../Text';
1642
+ import { TopHeaderSeparator } from '../../TopHeader';
1643
+ import { NotificationsInbox } from './_storyNotificationsInbox';
1644
+ import { QuickHelpDropdown } from './_storyQuickHelpDropdown';
1645
+ import { StorySearchModal } from './_storySearchModal';
1646
+
1647
+ interface HeaderActionsProps {
1648
+ /** Rendered before the notifications bell — where the host puts its AI Assistant toggle. */
1649
+ assistantToggle?: ReactNode;
1650
+ }
1651
+
1652
+ export const HeaderActions = ({ assistantToggle }: HeaderActionsProps) => (
1653
+ <>
1654
+ <StorySearchModal />
1655
+
1656
+ <TopHeaderSeparator />
1657
+
1658
+ <Button variant='ghost' size='small' color='neutral' className='py-4 rounded-6'>
1659
+ <Text size='xs' weight='medium'>
1660
+ Tenant Name
1661
+ </Text>
1662
+ <span className='text-text-tertiary mx-[-2px]'>•</span>
1663
+ <Code size='s' color='secondary'>
1664
+ 12345
1665
+ </Code>
1666
+ <ChevronUpDown className='!icon-sm' />
1667
+ </Button>
1668
+
1669
+ {assistantToggle}
1670
+
1671
+ <NotificationsInbox />
1672
+
1673
+ <QuickHelpDropdown />
1674
+ </>
1675
+ );
1676
+ ```
1677
+
1678
+ ### AppShell/story-content/_storyHomeContent.tsx
1679
+
1680
+ ```tsx
1681
+ import type { FC } from 'react';
1682
+
1683
+ export const HomeContent: FC = () => (
1684
+ <div className='p-24'>
1685
+ <h1 className='text-xl font-semibold text-text-primary'>Home</h1>
1686
+ <p className='mt-4 text-sm text-text-secondary'>
1687
+ Cross-product summary. Click any product in the rail to drill into it.
1688
+ </p>
1689
+ </div>
1690
+ );
1691
+ ```
1692
+
1693
+ ### AppShell/story-content/_storyLib.ts
1694
+
1695
+ ```tsx
1696
+ import { findFirstLinkPath } from '../../RemoteShell';
1697
+ import {
1698
+ aiHypervisorNavConfig,
1699
+ edgeNavConfig,
1700
+ infraDiscoveryNavConfig,
1701
+ securityTestingNavConfig,
1702
+ settingsNavConfig,
1703
+ } from './_storyNavConfigs';
1704
+
1705
+ const KNOWN_PRODUCTS = [
1706
+ 'home',
1707
+ 'edge',
1708
+ 'ai-hypervisor',
1709
+ 'infra-discovery',
1710
+ 'security-testing',
1711
+ 'settings',
1712
+ ] as const;
1713
+
1714
+ export type Product = (typeof KNOWN_PRODUCTS)[number];
1715
+
1716
+ export const PRODUCT_CONFIGS: Record<Exclude<Product, 'home'>, { config: typeof edgeNavConfig }> = {
1717
+ edge: { config: edgeNavConfig },
1718
+ 'ai-hypervisor': { config: aiHypervisorNavConfig },
1719
+ 'infra-discovery': { config: infraDiscoveryNavConfig },
1720
+ 'security-testing': { config: securityTestingNavConfig },
1721
+ settings: { config: settingsNavConfig },
1722
+ };
1723
+
1724
+ export function deriveProduct(pathname: string): Product {
1725
+ const segment = pathname.split('/').filter(Boolean)[0];
1726
+ if (segment && (KNOWN_PRODUCTS as readonly string[]).includes(segment)) {
1727
+ return segment as Product;
1728
+ }
1729
+ return 'home';
1730
+ }
1731
+
1732
+ /** Landing URL of a product: its first link, or `/home`. */
1733
+ export function productLandingPath(product: Product): string {
1734
+ if (product === 'home') return '/home';
1735
+ const { config } = PRODUCT_CONFIGS[product];
1736
+ const firstPath = findFirstLinkPath(config.items) ?? '';
1737
+ return `/${product}/${firstPath}`;
1738
+ }
1739
+ ```
1740
+
1741
+ ### AppShell/story-content/_storyNavConfigs.ts
1742
+
1743
+ ```tsx
1744
+ import { CircleDashed, Filter, Plus } from '../../../icons';
1745
+ import type { NavConfig } from '../../RemoteShell';
1746
+
1747
+ export const edgeNavConfig: NavConfig = {
1748
+ productLabel: 'Edge',
1749
+ productPath: '/edge',
1750
+ headerActions: [
1751
+ { icon: Filter, label: 'Filter', onClick: () => alert('Filter clicked') },
1752
+ { icon: Plus, label: 'Add', onClick: () => alert('Add clicked') },
1753
+ ],
1754
+ items: [
1755
+ { type: 'link', id: 'overview', label: 'Overview', path: 'overview', icon: CircleDashed },
1756
+ {
1757
+ type: 'drill',
1758
+ id: 'data-planes',
1759
+ label: 'Data planes',
1760
+ path: 'data-planes',
1761
+ icon: CircleDashed,
1762
+ param: 'dataPlaneId',
1763
+ entities: [
1764
+ { id: 'production', label: 'Production', description: 'Main production environment' },
1765
+ { id: 'staging', label: 'Staging', description: 'Pre-production testing' },
1766
+ { id: 'edge-eu', label: 'Edge EU', description: 'European edge nodes' },
1767
+ { id: 'edge-us', label: 'Edge US', description: 'US edge nodes' },
1768
+ ],
1769
+ children: [
1770
+ {
1771
+ type: 'link',
1772
+ id: 'dp-overview',
1773
+ label: 'Overview',
1774
+ path: 'overview',
1775
+ icon: CircleDashed,
1776
+ },
1777
+ {
1778
+ type: 'drill',
1779
+ id: 'dp-nodes',
1780
+ label: 'Nodes',
1781
+ path: 'nodes',
1782
+ icon: CircleDashed,
1783
+ param: 'nodeId',
1784
+ entities: [
1785
+ { id: 'node-1', label: 'Node 1', description: 'Primary node' },
1786
+ { id: 'node-2', label: 'Node 2', description: 'Secondary node' },
1787
+ { id: 'node-3', label: 'Node 3', description: 'Tertiary node' },
1788
+ ],
1789
+ children: [
1790
+ {
1791
+ type: 'link',
1792
+ id: 'node-overview',
1793
+ label: 'Overview',
1794
+ path: 'overview',
1795
+ icon: CircleDashed,
1796
+ },
1797
+ {
1798
+ type: 'link',
1799
+ id: 'node-metrics',
1800
+ label: 'Metrics',
1801
+ path: 'metrics',
1802
+ icon: CircleDashed,
1803
+ },
1804
+ { type: 'link', id: 'node-logs', label: 'Logs', path: 'logs', icon: CircleDashed },
1805
+ {
1806
+ type: 'link',
1807
+ id: 'node-config',
1808
+ label: 'Configuration',
1809
+ path: 'config',
1810
+ icon: CircleDashed,
1811
+ },
1812
+ ],
1813
+ },
1814
+ {
1815
+ type: 'link',
1816
+ id: 'dp-services',
1817
+ label: 'Services',
1818
+ path: 'services',
1819
+ icon: CircleDashed,
1820
+ dividerAfter: true,
1821
+ },
1822
+ { type: 'link', id: 'dp-govern', label: 'Govern', path: 'govern', icon: CircleDashed },
1823
+ {
1824
+ type: 'group',
1825
+ id: 'dp-operations',
1826
+ label: 'Operations',
1827
+ icon: CircleDashed,
1828
+ children: [
1829
+ {
1830
+ type: 'link',
1831
+ id: 'dp-routing-rules',
1832
+ label: 'Routing rules',
1833
+ path: 'routing-rules',
1834
+ },
1835
+ { type: 'link', id: 'dp-settings', label: 'Settings', path: 'settings' },
1836
+ ],
1837
+ },
1838
+ ],
1839
+ },
1840
+ {
1841
+ type: 'link',
1842
+ id: 'dashboards',
1843
+ label: 'Dashboards',
1844
+ path: 'dashboards',
1845
+ icon: CircleDashed,
1846
+ dividerAfter: true,
1847
+ },
1848
+ { type: 'section-header', id: 'section-detect', label: 'Detect & Respond' },
1849
+ {
1850
+ type: 'group',
1851
+ id: 'events',
1852
+ label: 'Events',
1853
+ icon: CircleDashed,
1854
+ defaultExpanded: true,
1855
+ children: [
1856
+ { type: 'link', id: 'attacks', label: 'Attacks', path: 'attacks' },
1857
+ { type: 'link', id: 'incidents', label: 'Incidents', path: 'incidents' },
1858
+ { type: 'link', id: 'security-issues', label: 'Security Issues', path: 'security-issues' },
1859
+ { type: 'link', id: 'api-sessions', label: 'API Sessions', path: 'api-sessions' },
1860
+ ],
1861
+ },
1862
+ {
1863
+ type: 'group',
1864
+ id: 'api-security',
1865
+ label: 'API Security',
1866
+ icon: CircleDashed,
1867
+ children: [
1868
+ {
1869
+ type: 'link',
1870
+ id: 'api-attack-surface',
1871
+ label: 'API Attack Surface',
1872
+ path: 'api-attack-surface',
1873
+ },
1874
+ { type: 'link', id: 'api-discovery', label: 'API Discovery', path: 'api-discovery' },
1875
+ {
1876
+ type: 'link',
1877
+ id: 'api-abuse-prevention',
1878
+ label: 'API Abuse Prevention',
1879
+ path: 'api-abuse-prevention',
1880
+ },
1881
+ {
1882
+ type: 'link',
1883
+ id: 'api-specifications',
1884
+ label: 'API Specifications',
1885
+ path: 'api-specifications',
1886
+ },
1887
+ ],
1888
+ },
1889
+ {
1890
+ type: 'group',
1891
+ id: 'security-controls',
1892
+ label: 'Security controls',
1893
+ icon: CircleDashed,
1894
+ children: [
1895
+ {
1896
+ type: 'link',
1897
+ id: 'ip-session-lists',
1898
+ label: 'IP & Session Lists',
1899
+ path: 'ip-session-lists',
1900
+ },
1901
+ { type: 'link', id: 'triggers', label: 'Triggers', path: 'triggers' },
1902
+ { type: 'link', id: 'rules', label: 'Rules', path: 'rules' },
1903
+ {
1904
+ type: 'link',
1905
+ id: 'mitigation-controls',
1906
+ label: 'Mitigation Controls',
1907
+ path: 'mitigation-controls',
1908
+ },
1909
+ {
1910
+ type: 'link',
1911
+ id: 'credential-stuffing',
1912
+ label: 'Credential Stuffing',
1913
+ path: 'credential-stuffing',
1914
+ },
1915
+ ],
1916
+ },
1917
+ { type: 'section-header', id: 'section-protect', label: 'Protect' },
1918
+ {
1919
+ type: 'group',
1920
+ id: 'security-testing',
1921
+ label: 'Security Testing',
1922
+ icon: CircleDashed,
1923
+ children: [
1924
+ { type: 'link', id: 'threat-replay', label: 'Threat Replay', path: 'threat-replay' },
1925
+ { type: 'link', id: 'schema-based', label: 'Schema-Based', path: 'schema-based' },
1926
+ ],
1927
+ },
1928
+ {
1929
+ type: 'group',
1930
+ id: 'configuration',
1931
+ label: 'Configuration',
1932
+ icon: CircleDashed,
1933
+ children: [
1934
+ {
1935
+ type: 'group',
1936
+ id: 'nodes',
1937
+ label: 'Nodes',
1938
+ children: [
1939
+ {
1940
+ type: 'link',
1941
+ id: 'nodes-overview',
1942
+ label: 'Overview',
1943
+ path: 'nodes-overview',
1944
+ },
1945
+ { type: 'link', id: 'nodes-pools', label: 'Pools', path: 'nodes-pools' },
1946
+ {
1947
+ type: 'link',
1948
+ id: 'nodes-health-checks',
1949
+ label: 'Health checks',
1950
+ path: 'nodes-health-checks',
1951
+ },
1952
+ ],
1953
+ },
1954
+ { type: 'link', id: 'security-edge', label: 'Security Edge', path: 'security-edge' },
1955
+ { type: 'link', id: 'integrations', label: 'Integrations', path: 'integrations' },
1956
+ ],
1957
+ },
1958
+ ],
1959
+ };
1960
+
1961
+ export const aiHypervisorNavConfig: NavConfig = {
1962
+ productLabel: 'AI Hypervisor',
1963
+ productPath: '/ai-hypervisor',
1964
+ items: [
1965
+ { type: 'link', id: 'heatmap', label: 'Heatmap', path: 'heatmap', icon: CircleDashed },
1966
+ { type: 'link', id: 'registry', label: 'Registry', path: 'registry', icon: CircleDashed },
1967
+ {
1968
+ type: 'group',
1969
+ id: 'topology',
1970
+ label: 'Topology',
1971
+ icon: CircleDashed,
1972
+ defaultExpanded: true,
1973
+ children: [
1974
+ {
1975
+ type: 'link',
1976
+ id: 'topology-overview',
1977
+ label: 'Overview',
1978
+ path: 'topology-overview',
1979
+ },
1980
+ {
1981
+ type: 'link',
1982
+ id: 'topology-services',
1983
+ label: 'Services',
1984
+ path: 'topology-services',
1985
+ },
1986
+ {
1987
+ type: 'group',
1988
+ id: 'graphs',
1989
+ label: 'Graphs',
1990
+ children: [
1991
+ { type: 'link', id: 'flow-graph', label: 'Flow graph', path: 'flow-graph' },
1992
+ {
1993
+ type: 'link',
1994
+ id: 'dependency-graph',
1995
+ label: 'Dependency graph',
1996
+ path: 'dependency-graph',
1997
+ },
1998
+ {
1999
+ type: 'link',
2000
+ id: 'traffic-graph',
2001
+ label: 'Traffic graph',
2002
+ path: 'traffic-graph',
2003
+ },
2004
+ ],
2005
+ },
2006
+ ],
2007
+ },
2008
+ {
2009
+ type: 'link',
2010
+ id: 'data-tracks',
2011
+ label: 'Data Tracks',
2012
+ path: 'data-tracks',
2013
+ icon: CircleDashed,
2014
+ },
2015
+ {
2016
+ type: 'link',
2017
+ id: 'user-tracks',
2018
+ label: 'User Tracks',
2019
+ path: 'user-tracks',
2020
+ icon: CircleDashed,
2021
+ },
2022
+ {
2023
+ type: 'link',
2024
+ id: 'supply-chain',
2025
+ label: 'Supply Chain',
2026
+ path: 'supply-chain',
2027
+ icon: CircleDashed,
2028
+ },
2029
+ {
2030
+ type: 'link',
2031
+ id: 'enforcement',
2032
+ label: 'Enforcement',
2033
+ path: 'enforcement',
2034
+ icon: CircleDashed,
2035
+ },
2036
+ {
2037
+ type: 'link',
2038
+ id: 'ai-integrations',
2039
+ label: 'Integrations',
2040
+ path: 'ai-integrations',
2041
+ icon: CircleDashed,
2042
+ },
2043
+ { type: 'link', id: 'red-team', label: 'Red Team', path: 'red-team', icon: CircleDashed },
2044
+ { type: 'link', id: 'debugger', label: 'Debugger', path: 'debugger', icon: CircleDashed },
2045
+ ],
2046
+ };
2047
+
2048
+ export const infraDiscoveryNavConfig: NavConfig = {
2049
+ productLabel: 'Infra Discovery',
2050
+ productPath: '/infra-discovery',
2051
+ headerActions: [{ icon: Filter, label: 'Filter', onClick: () => alert('Filter clicked') }],
2052
+ items: [
2053
+ { type: 'link', id: 'overview', label: 'Overview', path: 'overview', icon: CircleDashed },
2054
+ {
2055
+ type: 'group',
2056
+ id: 'inventory',
2057
+ label: 'Inventory',
2058
+ icon: CircleDashed,
2059
+ defaultExpanded: true,
2060
+ children: [
2061
+ { type: 'link', id: 'all-assets', label: 'All assets', path: 'all-assets' },
2062
+ { type: 'link', id: 'untagged', label: 'Untagged', path: 'untagged' },
2063
+ {
2064
+ type: 'group',
2065
+ id: 'by-source',
2066
+ label: 'By source',
2067
+ children: [
2068
+ {
2069
+ type: 'link',
2070
+ id: 'cloud-accounts',
2071
+ label: 'Cloud accounts',
2072
+ path: 'cloud-accounts',
2073
+ },
2074
+ { type: 'link', id: 'kubernetes', label: 'Kubernetes', path: 'kubernetes' },
2075
+ {
2076
+ type: 'link',
2077
+ id: 'source-control',
2078
+ label: 'Source control',
2079
+ path: 'source-control',
2080
+ },
2081
+ ],
2082
+ },
2083
+ ],
2084
+ },
2085
+ { type: 'link', id: 'topology', label: 'Topology', path: 'topology', icon: CircleDashed },
2086
+ { type: 'link', id: 'findings', label: 'Findings', path: 'findings', icon: CircleDashed },
2087
+ { type: 'link', id: 'risks', label: 'Risks', path: 'risks', icon: CircleDashed },
2088
+ {
2089
+ type: 'link',
2090
+ id: 'infra-integrations',
2091
+ label: 'Integrations',
2092
+ path: 'infra-integrations',
2093
+ icon: CircleDashed,
2094
+ },
2095
+ {
2096
+ type: 'link',
2097
+ id: 'infra-settings',
2098
+ label: 'Settings',
2099
+ path: 'infra-settings',
2100
+ icon: CircleDashed,
2101
+ },
2102
+ ],
2103
+ };
2104
+
2105
+ export const securityTestingNavConfig: NavConfig = {
2106
+ productLabel: 'Security Testing',
2107
+ productPath: '/security-testing',
2108
+ items: [
2109
+ { type: 'link', id: 'overview', label: 'Overview', path: 'overview', icon: CircleDashed },
2110
+ {
2111
+ type: 'link',
2112
+ id: 'test-suites',
2113
+ label: 'Test Suites',
2114
+ path: 'test-suites',
2115
+ icon: CircleDashed,
2116
+ },
2117
+ {
2118
+ type: 'group',
2119
+ id: 'templates',
2120
+ label: 'Templates',
2121
+ icon: CircleDashed,
2122
+ children: [
2123
+ { type: 'link', id: 'owasp', label: 'OWASP', path: 'owasp' },
2124
+ { type: 'link', id: 'pci-dss', label: 'PCI DSS', path: 'pci-dss' },
2125
+ { type: 'link', id: 'internal', label: 'Internal', path: 'internal' },
2126
+ ],
2127
+ },
2128
+ { type: 'link', id: 'schedules', label: 'Schedules', path: 'schedules', icon: CircleDashed },
2129
+ { type: 'link', id: 'results', label: 'Results', path: 'results', icon: CircleDashed },
2130
+ { type: 'link', id: 'coverage', label: 'Coverage', path: 'coverage', icon: CircleDashed },
2131
+ {
2132
+ type: 'link',
2133
+ id: 'testing-settings',
2134
+ label: 'Settings',
2135
+ path: 'testing-settings',
2136
+ icon: CircleDashed,
2137
+ },
2138
+ ],
2139
+ };
2140
+
2141
+ export const settingsNavConfig: NavConfig = {
2142
+ productLabel: 'Settings',
2143
+ productPath: '/settings',
2144
+ items: [
2145
+ { type: 'link', id: 'profile', label: 'Profile', path: 'profile', icon: CircleDashed },
2146
+ { type: 'link', id: 'general', label: 'General', path: 'general', icon: CircleDashed },
2147
+ {
2148
+ type: 'link',
2149
+ id: 'subscriptions',
2150
+ label: 'Subscriptions',
2151
+ path: 'subscriptions',
2152
+ icon: CircleDashed,
2153
+ },
2154
+ {
2155
+ type: 'link',
2156
+ id: 'applications',
2157
+ label: 'Applications',
2158
+ path: 'applications',
2159
+ icon: CircleDashed,
2160
+ },
2161
+ {
2162
+ type: 'group',
2163
+ id: 'users',
2164
+ label: 'Users',
2165
+ icon: CircleDashed,
2166
+ defaultExpanded: true,
2167
+ children: [
2168
+ { type: 'link', id: 'users-all', label: 'All users', path: 'users-all' },
2169
+ {
2170
+ type: 'link',
2171
+ id: 'users-invites',
2172
+ label: 'Pending invites',
2173
+ path: 'users-invites',
2174
+ },
2175
+ { type: 'link', id: 'users-roles', label: 'Roles', path: 'users-roles' },
2176
+ ],
2177
+ },
2178
+ { type: 'section-header', id: 'section-access', label: 'Access Management' },
2179
+ { type: 'link', id: 'groups', label: 'Groups', path: 'groups', icon: CircleDashed },
2180
+ { type: 'link', id: 'api-tokens', label: 'API Tokens', path: 'api-tokens', icon: CircleDashed },
2181
+ {
2182
+ type: 'link',
2183
+ id: 'activity-log',
2184
+ label: 'Activity Log',
2185
+ path: 'activity-log',
2186
+ icon: CircleDashed,
2187
+ dividerAfter: true,
2188
+ },
2189
+ {
2190
+ type: 'group',
2191
+ id: 'admin-zone',
2192
+ label: 'Admin Zone',
2193
+ icon: CircleDashed,
2194
+ defaultExpanded: true,
2195
+ children: [
2196
+ {
2197
+ type: 'link',
2198
+ id: 'customer-settings',
2199
+ label: 'Customer Settings',
2200
+ path: 'customer-settings',
2201
+ },
2202
+ {
2203
+ type: 'group',
2204
+ id: 'system-configuration',
2205
+ label: 'System Configuration',
2206
+ children: [
2207
+ { type: 'link', id: 'sys-general', label: 'General', path: 'sys-general' },
2208
+ { type: 'link', id: 'sys-storage', label: 'Storage', path: 'sys-storage' },
2209
+ {
2210
+ type: 'link',
2211
+ id: 'sys-networking',
2212
+ label: 'Networking',
2213
+ path: 'sys-networking',
2214
+ },
2215
+ ],
2216
+ },
2217
+ {
2218
+ type: 'link',
2219
+ id: 'bola-triggers',
2220
+ label: 'BOLA Triggers',
2221
+ path: 'bola-triggers',
2222
+ },
2223
+ { type: 'link', id: 'experiments', label: 'Experiments', path: 'experiments' },
2224
+ ],
2225
+ },
2226
+ ],
2227
+ };
2228
+ ```
2229
+
2230
+ ### AppShell/story-content/_storyNavRailFooter.tsx
2231
+
2232
+ ```tsx
2233
+ import type { FC } from 'react';
2234
+ import { Settings } from '../../../icons';
2235
+ import { NavRailFooter, NavRailItem } from '../../NavRail';
2236
+ import type { Theme } from '../../ThemeProvider';
2237
+ import { AccountDropdown, type SidebarMode } from './_storyAccountDropdown';
2238
+ import type { Product } from './_storyLib';
2239
+
2240
+ interface NavRailFooterContentProps {
2241
+ activeProduct: Product;
2242
+ onSelectProduct: (product: Product) => void;
2243
+ sidebarMode: SidebarMode;
2244
+ onSidebarModeChange: (mode: SidebarMode) => void;
2245
+ theme: Theme;
2246
+ onThemeChange: (theme: Theme) => void;
2247
+ }
2248
+
2249
+ export const NavRailFooterContent: FC<NavRailFooterContentProps> = ({
2250
+ activeProduct,
2251
+ onSelectProduct,
2252
+ sidebarMode,
2253
+ onSidebarModeChange,
2254
+ theme,
2255
+ onThemeChange,
2256
+ }) => (
2257
+ <NavRailFooter>
2258
+ <NavRailItem
2259
+ icon={Settings}
2260
+ label='Settings'
2261
+ shortcut={['G', 'S']}
2262
+ active={activeProduct === 'settings'}
2263
+ onClick={() => onSelectProduct('settings')}
2264
+ />
2265
+ <AccountDropdown
2266
+ sidebarMode={sidebarMode}
2267
+ onSidebarModeChange={onSidebarModeChange}
2268
+ theme={theme}
2269
+ onThemeChange={onThemeChange}
2270
+ />
2271
+ </NavRailFooter>
2272
+ );
2273
+ ```
2274
+
2275
+ ### AppShell/story-content/_storyNotificationsInbox.tsx
2276
+
2277
+ ```tsx
2278
+ import { Settings } from '../../../icons';
2279
+ import {
2280
+ NotificationCenter,
2281
+ NotificationCenterBody,
2282
+ NotificationCenterContent,
2283
+ NotificationCenterHeader,
2284
+ NotificationCenterHeaderAction,
2285
+ NotificationCenterHeaderActions,
2286
+ NotificationCenterItem,
2287
+ NotificationCenterItemDescription,
2288
+ NotificationCenterItemTime,
2289
+ NotificationCenterItemTitle,
2290
+ NotificationCenterList,
2291
+ NotificationCenterSkeleton,
2292
+ NotificationCenterTabs,
2293
+ NotificationCenterTabsList,
2294
+ NotificationCenterTitle,
2295
+ NotificationCenterTrigger,
2296
+ } from '../../NotificationCenter';
2297
+ import { NumericBadge } from '../../NumericBadge';
2298
+ import { TabsContent, TabsTrigger } from '../../Tabs';
2299
+
2300
+ const MINUTE = 60_000;
2301
+ const minutesAgo = (minutes: number) => new Date(Date.now() - minutes * MINUTE);
2302
+
2303
+ const NEW = [
2304
+ {
2305
+ id: 'attack',
2306
+ title: 'New attack detected on api.example.com',
2307
+ description: '214 SQL injection attempts from 12 IPs in the last 10 minutes.',
2308
+ createdAt: minutesAgo(3),
2309
+ },
2310
+ {
2311
+ id: 'node',
2312
+ title: 'Node wallarm-node-3 is offline',
2313
+ description: 'No heartbeat for 15 minutes. Traffic fails over to wallarm-node-1.',
2314
+ createdAt: minutesAgo(22),
2315
+ },
2316
+ ];
2317
+
2318
+ const READ = [
2319
+ {
2320
+ id: 'report',
2321
+ title: 'Weekly API discovery report is ready',
2322
+ description: '38 new endpoints this week, 5 of them handling sensitive data.',
2323
+ createdAt: minutesAgo(60 * 24 * 7),
2324
+ },
2325
+ ];
2326
+
2327
+ /** The host's inbox, as core-frontend's NotificationsInbox: bell with the unread count, New/Read tabs. */
2328
+ export const NotificationsInbox = ({ loading = false }: { loading?: boolean }) => (
2329
+ <NotificationCenter data-testid='notifications'>
2330
+ <NotificationCenterTrigger count={loading ? 0 : NEW.length} />
2331
+ <NotificationCenterContent>
2332
+ <NotificationCenterHeader>
2333
+ <NotificationCenterTitle />
2334
+ <NotificationCenterHeaderActions>
2335
+ <NotificationCenterHeaderAction disabled={loading}>
2336
+ Mark all as read
2337
+ </NotificationCenterHeaderAction>
2338
+ <NotificationCenterHeaderAction asChild aria-label='Notification settings'>
2339
+ <a href='#notification-settings'>
2340
+ <Settings />
2341
+ </a>
2342
+ </NotificationCenterHeaderAction>
2343
+ </NotificationCenterHeaderActions>
2344
+ </NotificationCenterHeader>
2345
+ {loading ? (
2346
+ <NotificationCenterBody>
2347
+ <NotificationCenterSkeleton />
2348
+ </NotificationCenterBody>
2349
+ ) : (
2350
+ <NotificationCenterTabs defaultValue='new'>
2351
+ <NotificationCenterTabsList>
2352
+ <TabsTrigger value='new'>
2353
+ New <NumericBadge size='small'>{NEW.length}</NumericBadge>
2354
+ </TabsTrigger>
2355
+ <TabsTrigger value='read'>Read</TabsTrigger>
2356
+ </NotificationCenterTabsList>
2357
+ {[
2358
+ { value: 'new', items: NEW },
2359
+ { value: 'read', items: READ },
2360
+ ].map(tab => (
2361
+ <TabsContent key={tab.value} value={tab.value}>
2362
+ <NotificationCenterBody>
2363
+ <NotificationCenterList>
2364
+ {tab.items.map(item => (
2365
+ <NotificationCenterItem key={item.id} unread={tab.value === 'new'}>
2366
+ <NotificationCenterItemTitle>{item.title}</NotificationCenterItemTitle>
2367
+ <NotificationCenterItemTime value={item.createdAt} />
2368
+ <NotificationCenterItemDescription>
2369
+ {item.description}
2370
+ </NotificationCenterItemDescription>
2371
+ </NotificationCenterItem>
2372
+ ))}
2373
+ </NotificationCenterList>
2374
+ </NotificationCenterBody>
2375
+ </TabsContent>
2376
+ ))}
2377
+ </NotificationCenterTabs>
2378
+ )}
2379
+ </NotificationCenterContent>
2380
+ </NotificationCenter>
2381
+ );
2382
+ ```
2383
+
2384
+ ### AppShell/story-content/_storyProductNavItems.tsx
2385
+
2386
+ ```tsx
2387
+ import type { FC } from 'react';
2388
+ import { CircleDashed } from '../../../icons';
2389
+ import { NavRailItem } from '../../NavRail';
2390
+ import type { Product } from './_storyLib';
2391
+
2392
+ interface ProductNavItemsProps {
2393
+ activeProduct: Product;
2394
+ onSelectProduct: (product: Product) => void;
2395
+ }
2396
+
2397
+ export const ProductNavItems: FC<ProductNavItemsProps> = ({ activeProduct, onSelectProduct }) => (
2398
+ <>
2399
+ <NavRailItem
2400
+ icon={CircleDashed}
2401
+ label='Edge'
2402
+ shortcut={['G', 'E']}
2403
+ active={activeProduct === 'edge'}
2404
+ onClick={() => onSelectProduct('edge')}
2405
+ />
2406
+ <NavRailItem
2407
+ icon={CircleDashed}
2408
+ label='AI Hypervisor'
2409
+ shortcut={['G', 'A']}
2410
+ active={activeProduct === 'ai-hypervisor'}
2411
+ onClick={() => onSelectProduct('ai-hypervisor')}
2412
+ />
2413
+ <NavRailItem
2414
+ icon={CircleDashed}
2415
+ label='Infra Discovery'
2416
+ shortcut={['G', 'I']}
2417
+ active={activeProduct === 'infra-discovery'}
2418
+ onClick={() => onSelectProduct('infra-discovery')}
2419
+ />
2420
+ <NavRailItem
2421
+ icon={CircleDashed}
2422
+ label='Security Testing'
2423
+ shortcut={['G', 'T']}
2424
+ active={activeProduct === 'security-testing'}
2425
+ onClick={() => onSelectProduct('security-testing')}
2426
+ />
2427
+ </>
2428
+ );
2429
+ ```
2430
+
2431
+ ### AppShell/story-content/_storyQuickHelpDropdown.tsx
2432
+
2433
+ ```tsx
2434
+ import {
2435
+ Activity,
2436
+ Book,
2437
+ CircleHelp,
2438
+ LifeBuoy,
2439
+ NotebookText,
2440
+ SquareArrowOutUpRight,
2441
+ } from '../../../icons';
2442
+ import { Button } from '../../Button';
2443
+ import {
2444
+ DropdownMenu,
2445
+ DropdownMenuContent,
2446
+ DropdownMenuItem,
2447
+ DropdownMenuItemContent,
2448
+ DropdownMenuItemIcon,
2449
+ DropdownMenuItemText,
2450
+ DropdownMenuSeparator,
2451
+ DropdownMenuTrigger,
2452
+ } from '../../DropdownMenu';
2453
+ import { Text } from '../../Text';
2454
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../../Tooltip';
2455
+
2456
+ export const QuickHelpDropdown = () => (
2457
+ <DropdownMenu positioning={{ placement: 'bottom-end', gutter: 4 }}>
2458
+ <Tooltip>
2459
+ <TooltipTrigger asChild>
2460
+ <span className='inline-flex'>
2461
+ <DropdownMenuTrigger asChild>
2462
+ <Button variant='ghost' size='small' color='neutral' aria-label='Quick Help'>
2463
+ <CircleHelp />
2464
+ </Button>
2465
+ </DropdownMenuTrigger>
2466
+ </span>
2467
+ </TooltipTrigger>
2468
+ <TooltipContent>Quick help</TooltipContent>
2469
+ </Tooltip>
2470
+
2471
+ <DropdownMenuContent className='w-320'>
2472
+ <DropdownMenuItem onSelect={() => window.open('https://docs.wallarm.com/', '_blank')}>
2473
+ <DropdownMenuItemIcon>
2474
+ <Book />
2475
+ </DropdownMenuItemIcon>
2476
+
2477
+ <DropdownMenuItemContent>
2478
+ <DropdownMenuItemText>Documentation</DropdownMenuItemText>
2479
+ <Text size='xs' color='secondary'>
2480
+ Setup, deployment, and how-tos
2481
+ </Text>
2482
+ </DropdownMenuItemContent>
2483
+ </DropdownMenuItem>
2484
+
2485
+ <DropdownMenuItem onSelect={() => window.open('https://status.wallarm.com/', '_blank')}>
2486
+ <DropdownMenuItemIcon>
2487
+ <Activity />
2488
+ </DropdownMenuItemIcon>
2489
+
2490
+ <DropdownMenuItemContent>
2491
+ <DropdownMenuItemText>Service status</DropdownMenuItemText>
2492
+ <Text size='xs' color='secondary'>
2493
+ Live uptime and alerts
2494
+ </Text>
2495
+ </DropdownMenuItemContent>
2496
+ </DropdownMenuItem>
2497
+
2498
+ <DropdownMenuItem onSelect={() => window.open('mailto:support@wallarm.com')}>
2499
+ <DropdownMenuItemIcon>
2500
+ <LifeBuoy />
2501
+ </DropdownMenuItemIcon>
2502
+
2503
+ <DropdownMenuItemContent>
2504
+ <DropdownMenuItemText>Support</DropdownMenuItemText>
2505
+ <Text size='xs' color='secondary'>
2506
+ Talk to our team
2507
+ </Text>
2508
+ </DropdownMenuItemContent>
2509
+ </DropdownMenuItem>
2510
+
2511
+ <DropdownMenuItem onSelect={() => window.open('https://lab.wallarm.com', '_blank')}>
2512
+ <DropdownMenuItemIcon>
2513
+ <NotebookText />
2514
+ </DropdownMenuItemIcon>
2515
+
2516
+ <DropdownMenuItemContent>
2517
+ <DropdownMenuItemText>Blog</DropdownMenuItemText>
2518
+ <Text size='xs' color='secondary'>
2519
+ Security news and updates
2520
+ </Text>
2521
+ </DropdownMenuItemContent>
2522
+ </DropdownMenuItem>
2523
+
2524
+ <DropdownMenuSeparator />
2525
+
2526
+ <DropdownMenuItem onSelect={() => window.open('https://playground.wallarm.com', '_blank')}>
2527
+ <DropdownMenuItemIcon>
2528
+ <SquareArrowOutUpRight />
2529
+ </DropdownMenuItemIcon>
2530
+
2531
+ <DropdownMenuItemContent>
2532
+ <DropdownMenuItemText>Wallarm Playground</DropdownMenuItemText>
2533
+ <Text size='xs' color='secondary'>
2534
+ Try the platform without any setup
2535
+ </Text>
2536
+ </DropdownMenuItemContent>
2537
+ </DropdownMenuItem>
2538
+ </DropdownMenuContent>
2539
+ </DropdownMenu>
2540
+ );
2541
+ ```
2542
+
2543
+ ### AppShell/story-content/_storyRecentDropdown.tsx
2544
+
2545
+ ```tsx
2546
+ import { History } from '../../../icons';
2547
+ import {
2548
+ DropdownMenu,
2549
+ DropdownMenuContent,
2550
+ DropdownMenuItem,
2551
+ DropdownMenuItemContent,
2552
+ DropdownMenuItemText,
2553
+ DropdownMenuLabel,
2554
+ DropdownMenuTrigger,
2555
+ } from '../../DropdownMenu';
2556
+ import { NavRailItem } from '../../NavRail';
2557
+ import { Text } from '../../Text';
2558
+
2559
+ const RECENT_ITEMS = [
2560
+ { pageName: 'WAF Rules', productName: 'Edge' },
2561
+ { pageName: 'API Sessions', dataPlane: 'US Cloud', productName: 'Edge' },
2562
+ { pageName: 'Prompt Inspector', productName: 'AI Hypervisor' },
2563
+ { pageName: 'Scanner Results', dataPlane: 'EU Cloud', productName: 'Security Testing' },
2564
+ { pageName: 'Asset Inventory', productName: 'Infra Discovery' },
2565
+ { pageName: 'Pupu', productName: 'Edge' },
2566
+ { pageName: 'Pupupu', dataPlane: 'US Cloud', productName: 'Edge' },
2567
+ { pageName: 'PuPuPuuu', productName: 'AI Hypervisor' },
2568
+ { pageName: 'MeowMeow', dataPlane: 'EU Cloud', productName: 'Security Testing' },
2569
+ { pageName: '...', productName: '....' },
2570
+ ];
2571
+
2572
+ export const RecentDropdown = () => (
2573
+ <DropdownMenu positioning={{ placement: 'right-start', gutter: 8 }}>
2574
+ <DropdownMenuTrigger>
2575
+ <NavRailItem icon={History} label='Recent' />
2576
+ </DropdownMenuTrigger>
2577
+
2578
+ <DropdownMenuContent className='w-304 h-290 overscroll-none'>
2579
+ <DropdownMenuLabel className='sticky top-0 z-10 bg-bg-surface-2'>Recent</DropdownMenuLabel>
2580
+
2581
+ {RECENT_ITEMS.map(item => (
2582
+ <DropdownMenuItem key={`${item.pageName}-${item.productName}`}>
2583
+ <DropdownMenuItemContent>
2584
+ <DropdownMenuItemText>{item.pageName}</DropdownMenuItemText>
2585
+ <Text size='xs' color='secondary'>
2586
+ {item.dataPlane ? `${item.dataPlane} \u00b7 ${item.productName}` : item.productName}
2587
+ </Text>
2588
+ </DropdownMenuItemContent>
2589
+ </DropdownMenuItem>
2590
+ ))}
2591
+ </DropdownMenuContent>
2592
+ </DropdownMenu>
2593
+ );
2594
+ ```
2595
+
2596
+ ### AppShell/story-content/_storyRouter.tsx
2597
+
2598
+ ```tsx
2599
+ import { createContext, type FC, type ReactNode, useCallback, useContext, useState } from 'react';
2600
+ import {
2601
+ createMemoryHistory,
2602
+ createRootRoute,
2603
+ createRoute,
2604
+ createRouter,
2605
+ RouterProvider,
2606
+ useNavigate,
2607
+ useRouterState,
2608
+ } from '@tanstack/react-router';
2609
+ import { deriveProduct, type Product, productLandingPath } from './_storyLib';
2610
+
2611
+ // Story-only harness: every story gets its own TanStack Router on a memory history,
2612
+ // so navigation never touches Storybook's iframe URL and stories don't share history.
2613
+ // `story-content/**` is excluded from the published build (see rslib.config.ts).
2614
+
2615
+ const StoryContentContext = createContext<ReactNode>(null);
2616
+
2617
+ const StoryContent: FC = () => <>{useContext(StoryContentContext)}</>;
2618
+
2619
+ const createStoryRouter = (initialPath: string) => {
2620
+ const rootRoute = createRootRoute({ component: StoryContent });
2621
+ // Catch-all so every product URL matches; the shell reads the pathname itself.
2622
+ const splatRoute = createRoute({ getParentRoute: () => rootRoute, path: '$' });
2623
+
2624
+ return createRouter({
2625
+ routeTree: rootRoute.addChildren([splatRoute]),
2626
+ history: createMemoryHistory({ initialEntries: [initialPath] }),
2627
+ });
2628
+ };
2629
+
2630
+ interface StoryRouterProps {
2631
+ /** Starting URL of the memory history. */
2632
+ initialPath?: string;
2633
+ children: ReactNode;
2634
+ }
2635
+
2636
+ /** Renders `children` inside a per-render TanStack Router with memory history. */
2637
+ export const StoryRouter: FC<StoryRouterProps> = ({ initialPath = '/home', children }) => {
2638
+ const [router] = useState(() => createStoryRouter(initialPath));
2639
+
2640
+ return (
2641
+ <StoryContentContext.Provider value={children}>
2642
+ <RouterProvider router={router} />
2643
+ </StoryContentContext.Provider>
2644
+ );
2645
+ };
2646
+
2647
+ StoryRouter.displayName = 'StoryRouter';
2648
+
2649
+ /** Current router pathname — what a host passes to `RemoteShell`'s `pathname` prop. */
2650
+ export const useStoryPathname = (): string =>
2651
+ useRouterState({ select: state => state.location.pathname });
2652
+
2653
+ /** Router-driven product navigation for the rail. */
2654
+ export const useStoryProductNavigation = () => {
2655
+ const navigate = useNavigate();
2656
+ const pathname = useStoryPathname();
2657
+
2658
+ const goToProduct = useCallback(
2659
+ (product: Product) => {
2660
+ navigate({ to: productLandingPath(product) });
2661
+ },
2662
+ [navigate],
2663
+ );
2664
+
2665
+ return { pathname, activeProduct: deriveProduct(pathname), goToProduct };
2666
+ };
2667
+ ```
2668
+
2669
+ ### AppShell/story-content/_storyWallarmLogo.tsx
2670
+
2671
+ ```tsx
2672
+ export const WallarmLogo = () => (
2673
+ <svg width='89' height='16' viewBox='0 0 89 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
2674
+ <path
2675
+ fillRule='evenodd'
2676
+ clipRule='evenodd'
2677
+ d='M33.9026 4.87835H35.5876L37.698 11.2096L39.383 4.87835H41.4934L38.5405 14.1625H36.8555L34.7451 7.83127L32.6346 14.1625H30.9496L27.9884 4.87835H30.0988L31.7838 11.2096L33.8942 4.87835H33.9026ZM44.9886 12.7862C46.3733 12.7862 47.4076 12.0104 47.4076 10.8676V10.292C46.8738 10.0835 46.1314 9.92502 45.3139 9.92502C44.0042 9.92502 43.2201 10.4589 43.2201 11.3931V11.4265C43.2201 12.294 43.9959 12.7778 44.9802 12.7778L44.9886 12.7862ZM42.7613 5.35382C43.5371 5.07855 44.388 4.90337 45.5057 4.90337C48.2001 4.90337 49.518 6.24637 49.518 8.68212V14.1709H47.4076V13.0531C46.782 13.7955 45.8728 14.3627 44.4881 14.3627C42.7614 14.3627 41.2432 13.3784 41.2432 11.5433V11.5099C41.2432 9.5413 42.8197 8.57368 44.9385 8.57368C46.0479 8.57368 46.6986 8.73217 47.4076 8.95739V8.73217C47.4076 7.47258 46.6652 6.78023 45.2138 6.78023C44.2712 6.78023 43.5204 6.98043 42.7613 7.28073V5.3705V5.35382ZM50.786 14.1625V1.5H52.8964V14.1625H50.786ZM54.1643 14.1625V1.5H56.2747L56.3164 14.1625H54.1643ZM61.0378 12.7862C62.4225 12.7862 63.4569 12.0104 63.4569 10.8676V10.292C62.923 10.0835 62.1806 9.92502 61.3631 9.92502C60.0535 9.92502 59.2694 10.4589 59.2694 11.3931V11.4265C59.2694 12.294 60.0451 12.7778 61.0295 12.7778L61.0378 12.7862ZM58.8106 5.35382C59.578 5.07855 60.4372 4.90337 61.555 4.90337C64.2493 4.90337 65.5589 6.24637 65.5589 8.68212V14.1709H63.4485V13.0531C62.8229 13.7955 61.9137 14.3627 60.529 14.3627C58.8022 14.3627 57.2841 13.3784 57.2841 11.5433V11.5099C57.2841 9.5413 58.8606 8.57368 60.9794 8.57368C62.0888 8.57368 62.7395 8.73217 63.4485 8.95739V8.73217C63.4485 7.47258 62.7061 6.78023 61.2547 6.78023C60.3121 6.78023 59.5613 6.98043 58.8022 7.28073V5.3705L58.8106 5.35382ZM66.8269 14.1625V4.87835H68.9373V6.93872C69.5045 5.5707 70.5389 4.63644 72.1405 4.71152V6.92204H72.0237C70.1885 6.92204 68.9373 8.11489 68.9373 10.534V14.1709H66.8269V14.1625ZM87.0969 8.18162V14.1709H84.9865V8.94071C84.9865 7.48927 84.3359 6.58838 83.1597 6.58838C81.9835 6.58838 81.1911 7.47258 81.1911 8.94071V14.1709H79.0807V8.80724C79.0807 7.38917 78.43 6.58838 77.2705 6.58838C76.1111 6.58838 75.2769 7.52263 75.2769 8.94071V14.1709H73.1665V4.87835H75.2769V6.26305C75.8692 5.45392 76.695 4.68649 78.0964 4.68649C79.406 4.68649 80.3236 5.32879 80.8074 6.29642C81.5498 5.32879 82.5174 4.68649 83.9021 4.68649C85.8874 4.68649 87.0969 5.94607 87.0969 8.18162Z'
2678
+ fill='currentColor'
2679
+ />
2680
+ <path
2681
+ d='M0.849998 6.83954L9.8954 3V8.4134L16.4618 5.62599V9.59236L21.2757 7.54864V10.4542L16.4618 12.498V10.754L9.8954 13.5414V11.1662L0.849998 15V6.83954Z'
2682
+ fill='#FF441C'
2683
+ />
2684
+ </svg>
2685
+ );
2686
+ ```
2687
+
2688
+ ### ../utils/cn.ts
2689
+
2690
+ ```tsx
2691
+ import { type ClassArray, clsx } from 'clsx';
2692
+ import { twMerge } from 'tailwind-merge';
2693
+
2694
+ export const cn = (...inputs: ClassArray): string => {
2695
+ return twMerge(clsx(inputs));
2696
+ };
2697
+ ```
2698
+
2699
+ ### AppShell/story-content/_storySearchModal.tsx
2700
+
2701
+ ```tsx
2702
+ import { useState } from 'react';
2703
+ import { Building, CircleDashed, Plus } from '../../../icons';
2704
+ import { Button } from '../../Button';
2705
+ import { Code } from '../../Code';
2706
+ import { EmptyState, EmptyStateDescription, EmptyStateMessage } from '../../EmptyState';
2707
+ import { Kbd } from '../../Kbd';
2708
+ import {
2709
+ SearchModal,
2710
+ SearchModalBody,
2711
+ SearchModalContent,
2712
+ SearchModalEmpty,
2713
+ SearchModalFooter,
2714
+ SearchModalGroup,
2715
+ SearchModalGroupLabel,
2716
+ SearchModalInput,
2717
+ SearchModalItem,
2718
+ SearchModalTrigger,
2719
+ } from '../../SearchModal';
2720
+ import { Text } from '../../Text';
2721
+ import { WallyIcon } from '../../WallyIcon';
2722
+
2723
+ const recentItems = [
2724
+ { page: 'Attacks', product: 'Cloud WAF', date: '2 hours ago', href: '/attacks' },
2725
+ { page: 'Rules', product: 'Cloud WAF', date: '5 hours ago', href: '/rules' },
2726
+ {
2727
+ page: 'Endpoints',
2728
+ product: 'US East',
2729
+ dataPlane: true,
2730
+ productExtra: 'API Discovery',
2731
+ date: 'Yesterday',
2732
+ href: '/endpoints',
2733
+ },
2734
+ { page: 'Dashboards', product: 'Cloud WAF', date: '2 days ago', href: '/dashboards' },
2735
+ { page: 'Triggers', product: 'Cloud WAF', date: '3 days ago', href: '/triggers' },
2736
+ ];
2737
+
2738
+ const jumpToItems = [
2739
+ { name: 'Cloud WAF', href: '/cloud-waf' },
2740
+ { name: 'API Discovery', href: '/api-discovery' },
2741
+ ];
2742
+
2743
+ const filterRecent = (query: string) => {
2744
+ if (!query) return recentItems;
2745
+ return recentItems.filter(item => {
2746
+ const text = `${item.page} ${item.product} ${item.productExtra ?? ''}`.toLowerCase();
2747
+ return text.includes(query);
2748
+ });
2749
+ };
2750
+
2751
+ const filterJumpTo = (query: string) => {
2752
+ if (!query) return jumpToItems;
2753
+ return jumpToItems.filter(item => item.name.toLowerCase().includes(query));
2754
+ };
2755
+
2756
+ export const StorySearchModal = () => {
2757
+ const [open, setOpen] = useState(false);
2758
+ const [query, setQuery] = useState('');
2759
+ const lowerQuery = query.toLowerCase();
2760
+
2761
+ const filteredRecent = filterRecent(lowerQuery);
2762
+ const filteredJumpTo = filterJumpTo(lowerQuery);
2763
+ const hasResults = filteredRecent.length > 0 || filteredJumpTo.length > 0 || !lowerQuery;
2764
+
2765
+ const handleOpenChange = (nextOpen: boolean) => {
2766
+ setOpen(nextOpen);
2767
+ if (!nextOpen) {
2768
+ setQuery('');
2769
+ }
2770
+ };
2771
+
2772
+ return (
2773
+ <SearchModal open={open} onOpenChange={handleOpenChange} onQueryChange={setQuery}>
2774
+ <SearchModalTrigger asChild>
2775
+ <Button
2776
+ variant='ghost'
2777
+ size='small'
2778
+ color='neutral'
2779
+ className='p-4 gap-6 rounded-6'
2780
+ data-slot='top-header-search'
2781
+ >
2782
+ <Code size='s' color='secondary'>
2783
+ Search Wallarm
2784
+ </Code>
2785
+ <Kbd size='xsmall'>⌘ K</Kbd>
2786
+ </Button>
2787
+ </SearchModalTrigger>
2788
+
2789
+ <SearchModalContent>
2790
+ <SearchModalInput placeholder='Search products, features, and more...' />
2791
+
2792
+ {lowerQuery && !hasResults && (
2793
+ <SearchModalEmpty>
2794
+ <EmptyState type='no-results' className='pb-8'>
2795
+ <EmptyStateMessage>
2796
+ <EmptyStateDescription>No results</EmptyStateDescription>
2797
+ </EmptyStateMessage>
2798
+ </EmptyState>
2799
+ </SearchModalEmpty>
2800
+ )}
2801
+
2802
+ <SearchModalBody>
2803
+ <SearchModalGroup>
2804
+ <SearchModalGroupLabel>
2805
+ <Text size='xs' color='secondary' weight='medium'>
2806
+ Quick actions
2807
+ </Text>
2808
+ </SearchModalGroupLabel>
2809
+ <SearchModalItem onSelect={() => {}}>
2810
+ <WallyIcon variant='circle' size='sm' />
2811
+ <Text size='sm'>Ask Wally</Text>
2812
+ </SearchModalItem>
2813
+
2814
+ {!lowerQuery && (
2815
+ <>
2816
+ <SearchModalItem onSelect={() => {}}>
2817
+ <Building className='text-text-secondary !icon-md' />
2818
+ <Text size='sm'>Switch tenant</Text>
2819
+ </SearchModalItem>
2820
+
2821
+ <SearchModalItem onSelect={() => {}}>
2822
+ <Plus className='text-text-secondary !icon-md' />
2823
+ <Text size='sm'>Create...</Text>
2824
+ </SearchModalItem>
2825
+ </>
2826
+ )}
2827
+ </SearchModalGroup>
2828
+
2829
+ {filteredRecent.length > 0 && (
2830
+ <SearchModalGroup>
2831
+ <SearchModalGroupLabel>
2832
+ <Text size='xs' color='secondary' weight='medium'>
2833
+ Recent
2834
+ </Text>
2835
+ </SearchModalGroupLabel>
2836
+ {filteredRecent.map(item => (
2837
+ <SearchModalItem key={`${item.page}-${item.product}`} onSelect={() => {}}>
2838
+ <CircleDashed className='text-text-primary shrink-0 !icon-md' />
2839
+ <Text size='sm' grow truncate>
2840
+ {item.page}
2841
+ <Text size='sm' color='secondary' inline>
2842
+ {' '}
2843
+ — {item.dataPlane ? `${item.productExtra} · ${item.product}` : item.product}
2844
+ </Text>
2845
+ </Text>
2846
+ <span className='shrink-0'>
2847
+ <Text size='xs' color='secondary'>
2848
+ {item.date}
2849
+ </Text>
2850
+ </span>
2851
+ </SearchModalItem>
2852
+ ))}
2853
+ </SearchModalGroup>
2854
+ )}
2855
+
2856
+ {filteredJumpTo.length > 0 && (
2857
+ <SearchModalGroup>
2858
+ <SearchModalGroupLabel>
2859
+ <Text size='xs' color='secondary' weight='medium'>
2860
+ Jump to
2861
+ </Text>
2862
+ </SearchModalGroupLabel>
2863
+ {filteredJumpTo.map(item => (
2864
+ <SearchModalItem key={item.href} onSelect={() => {}}>
2865
+ <CircleDashed className='text-text-primary shrink-0 !icon-md' />
2866
+ <Text size='sm'>{item.name}</Text>
2867
+ </SearchModalItem>
2868
+ ))}
2869
+ </SearchModalGroup>
2870
+ )}
2871
+ </SearchModalBody>
2872
+
2873
+ <SearchModalFooter />
2874
+ </SearchModalContent>
2875
+ </SearchModal>
2876
+ );
2877
+ };
2878
+ ```