@evinvest/uikit 0.30.0 → 0.32.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 (403) hide show
  1. package/README.md +267 -73
  2. package/dist/{chunk-3IPB4QQJ.js → chunk-35SPIU4U.js} +10 -6
  3. package/dist/chunk-35SPIU4U.js.map +1 -0
  4. package/dist/components/{accordion.js → atomic/accordion.js} +3 -3
  5. package/dist/components/atomic/accordion.js.map +1 -0
  6. package/dist/components/{alert-dialog.js → atomic/alert-dialog.js} +11 -10
  7. package/dist/components/atomic/alert-dialog.js.map +1 -0
  8. package/dist/components/{alert.js → atomic/alert.js} +2 -2
  9. package/dist/components/atomic/alert.js.map +1 -0
  10. package/dist/components/atomic/animated-number-columns.js +105 -0
  11. package/dist/components/atomic/animated-number-columns.js.map +1 -0
  12. package/dist/components/{avatar.js → atomic/avatar.js} +2 -2
  13. package/dist/components/atomic/avatar.js.map +1 -0
  14. package/dist/components/{badge.js → atomic/badge.js} +4 -4
  15. package/dist/components/atomic/badge.js.map +1 -0
  16. package/dist/components/{band.js → atomic/band.js} +2 -2
  17. package/dist/components/atomic/band.js.map +1 -0
  18. package/dist/components/{breadcrumb.js → atomic/breadcrumb.js} +3 -3
  19. package/dist/components/atomic/breadcrumb.js.map +1 -0
  20. package/dist/components/{button-group.js → atomic/button-group.js} +3 -3
  21. package/dist/components/atomic/button-group.js.map +1 -0
  22. package/dist/components/{button.js → atomic/button.js} +7 -15
  23. package/dist/components/atomic/button.js.map +1 -0
  24. package/dist/components/{calendar.js → atomic/calendar.js} +2 -2
  25. package/dist/components/atomic/calendar.js.map +1 -0
  26. package/dist/components/{card.js → atomic/card.js} +2 -2
  27. package/dist/components/atomic/card.js.map +1 -0
  28. package/dist/components/{checkbox.js → atomic/checkbox.js} +4 -4
  29. package/dist/components/atomic/checkbox.js.map +1 -0
  30. package/dist/components/{collapsible.js → atomic/collapsible.js} +1 -1
  31. package/dist/components/atomic/collapsible.js.map +1 -0
  32. package/dist/components/{command.js → atomic/command.js} +9 -7
  33. package/dist/components/atomic/command.js.map +1 -0
  34. package/dist/components/{container.js → atomic/container.js} +2 -2
  35. package/dist/components/atomic/container.js.map +1 -0
  36. package/dist/components/{context-menu.js → atomic/context-menu.js} +62 -14
  37. package/dist/components/atomic/context-menu.js.map +1 -0
  38. package/dist/components/{dialog.js → atomic/dialog.js} +11 -10
  39. package/dist/components/atomic/dialog.js.map +1 -0
  40. package/dist/components/{drawer.js → atomic/drawer.js} +10 -8
  41. package/dist/components/atomic/drawer.js.map +1 -0
  42. package/dist/components/{dropdown-menu.js → atomic/dropdown-menu.js} +74 -15
  43. package/dist/components/atomic/dropdown-menu.js.map +1 -0
  44. package/dist/components/{empty.js → atomic/empty.js} +2 -2
  45. package/dist/components/atomic/empty.js.map +1 -0
  46. package/dist/components/atomic/field-context.js.map +1 -0
  47. package/dist/components/atomic/form-row.js +51 -0
  48. package/dist/components/atomic/form-row.js.map +1 -0
  49. package/dist/components/{hover-card.js → atomic/hover-card.js} +8 -8
  50. package/dist/components/atomic/hover-card.js.map +1 -0
  51. package/dist/components/{info-tip.js → atomic/info-tip.js} +10 -10
  52. package/dist/components/atomic/info-tip.js.map +1 -0
  53. package/dist/components/{input-otp.js → atomic/input-otp.js} +3 -3
  54. package/dist/components/atomic/input-otp.js.map +1 -0
  55. package/dist/components/{input.js → atomic/input.js} +3 -3
  56. package/dist/components/atomic/input.js.map +1 -0
  57. package/dist/components/{kbd.js → atomic/kbd.js} +2 -2
  58. package/dist/components/atomic/kbd.js.map +1 -0
  59. package/dist/components/atomic/key-value.js +68 -0
  60. package/dist/components/atomic/key-value.js.map +1 -0
  61. package/dist/components/{label.js → atomic/label.js} +2 -2
  62. package/dist/components/atomic/label.js.map +1 -0
  63. package/dist/components/{menubar.js → atomic/menubar.js} +48 -14
  64. package/dist/components/atomic/menubar.js.map +1 -0
  65. package/dist/components/{native-select.js → atomic/native-select.js} +6 -7
  66. package/dist/components/atomic/native-select.js.map +1 -0
  67. package/dist/components/{navigation-menu.js → atomic/navigation-menu.js} +11 -12
  68. package/dist/components/atomic/navigation-menu.js.map +1 -0
  69. package/dist/components/{pagination.js → atomic/pagination.js} +1 -1
  70. package/dist/components/atomic/pagination.js.map +1 -0
  71. package/dist/components/{popover.js → atomic/popover.js} +10 -10
  72. package/dist/components/atomic/popover.js.map +1 -0
  73. package/dist/components/{progress.js → atomic/progress.js} +2 -2
  74. package/dist/components/atomic/progress.js.map +1 -0
  75. package/dist/components/{radio-group.js → atomic/radio-group.js} +4 -4
  76. package/dist/components/atomic/radio-group.js.map +1 -0
  77. package/dist/components/{scroll-area.js → atomic/scroll-area.js} +2 -2
  78. package/dist/components/atomic/scroll-area.js.map +1 -0
  79. package/dist/components/{section-descriptor.js → atomic/section-descriptor.js} +2 -2
  80. package/dist/components/atomic/section-descriptor.js.map +1 -0
  81. package/dist/components/{select-listbox.js → atomic/select-listbox.js} +1 -1
  82. package/dist/components/atomic/select-listbox.js.map +1 -0
  83. package/dist/components/{select.js → atomic/select.js} +26 -16
  84. package/dist/components/atomic/select.js.map +1 -0
  85. package/dist/components/{separator.js → atomic/separator.js} +2 -2
  86. package/dist/components/atomic/separator.js.map +1 -0
  87. package/dist/components/{sheet.js → atomic/sheet.js} +11 -10
  88. package/dist/components/atomic/sheet.js.map +1 -0
  89. package/dist/components/{sidebar.js → atomic/sidebar.js} +5 -5
  90. package/dist/components/atomic/sidebar.js.map +1 -0
  91. package/dist/components/{skeleton.js → atomic/skeleton.js} +2 -2
  92. package/dist/components/atomic/skeleton.js.map +1 -0
  93. package/dist/components/{slider.js → atomic/slider.js} +3 -3
  94. package/dist/components/atomic/slider.js.map +1 -0
  95. package/dist/components/{sonner.js → atomic/sonner.js} +2 -2
  96. package/dist/components/atomic/sonner.js.map +1 -0
  97. package/dist/components/{spinner.js → atomic/spinner.js} +1 -1
  98. package/dist/components/atomic/spinner.js.map +1 -0
  99. package/dist/components/atomic/status-item.js +42 -0
  100. package/dist/components/atomic/status-item.js.map +1 -0
  101. package/dist/components/{switch.js → atomic/switch.js} +4 -4
  102. package/dist/components/atomic/switch.js.map +1 -0
  103. package/dist/components/{table.js → atomic/table.js} +2 -2
  104. package/dist/components/atomic/table.js.map +1 -0
  105. package/dist/components/{tabs.js → atomic/tabs.js} +4 -4
  106. package/dist/components/atomic/tabs.js.map +1 -0
  107. package/dist/components/{textarea.js → atomic/textarea.js} +3 -3
  108. package/dist/components/atomic/textarea.js.map +1 -0
  109. package/dist/components/{toggle-group.js → atomic/toggle-group.js} +2 -2
  110. package/dist/components/atomic/toggle-group.js.map +1 -0
  111. package/dist/components/{toggle.js → atomic/toggle.js} +3 -3
  112. package/dist/components/atomic/toggle.js.map +1 -0
  113. package/dist/components/{tooltip.js → atomic/tooltip.js} +8 -8
  114. package/dist/components/atomic/tooltip.js.map +1 -0
  115. package/dist/components/atomic/use-count-tween.js +42 -0
  116. package/dist/components/atomic/use-count-tween.js.map +1 -0
  117. package/dist/components/derived/animated-number.js +157 -0
  118. package/dist/components/derived/animated-number.js.map +1 -0
  119. package/dist/components/{carousel.js → derived/carousel.js} +4 -4
  120. package/dist/components/derived/carousel.js.map +1 -0
  121. package/dist/components/derived/command-palette.js +119 -0
  122. package/dist/components/derived/command-palette.js.map +1 -0
  123. package/dist/components/derived/confirm-dialog.js +123 -0
  124. package/dist/components/derived/confirm-dialog.js.map +1 -0
  125. package/dist/components/{date-time-picker.js → derived/date-time-picker.js} +8 -8
  126. package/dist/components/derived/date-time-picker.js.map +1 -0
  127. package/dist/components/{field.js → derived/field.js} +6 -6
  128. package/dist/components/derived/field.js.map +1 -0
  129. package/dist/components/{form.js → derived/form.js} +4 -4
  130. package/dist/components/derived/form.js.map +1 -0
  131. package/dist/components/derived/icon-button.js +35 -0
  132. package/dist/components/derived/icon-button.js.map +1 -0
  133. package/dist/components/{input-group.js → derived/input-group.js} +5 -5
  134. package/dist/components/derived/input-group.js.map +1 -0
  135. package/dist/components/{item.js → derived/item.js} +4 -4
  136. package/dist/components/derived/item.js.map +1 -0
  137. package/dist/components/derived/popover-form.js +60 -0
  138. package/dist/components/derived/popover-form.js.map +1 -0
  139. package/dist/components/derived/settings.js +94 -0
  140. package/dist/components/derived/settings.js.map +1 -0
  141. package/dist/components/shell/account-list.js +128 -0
  142. package/dist/components/shell/account-list.js.map +1 -0
  143. package/dist/components/shell/account-menu-profile.js +114 -0
  144. package/dist/components/shell/account-menu-profile.js.map +1 -0
  145. package/dist/components/shell/account-menu.js +97 -0
  146. package/dist/components/shell/account-menu.js.map +1 -0
  147. package/dist/components/shell/account-settings-dialog.js +84 -0
  148. package/dist/components/shell/account-settings-dialog.js.map +1 -0
  149. package/dist/components/{app-shell.js → shell/app-shell.js} +10 -1
  150. package/dist/components/shell/app-shell.js.map +1 -0
  151. package/dist/components/shell/aside.js +140 -0
  152. package/dist/components/shell/aside.js.map +1 -0
  153. package/dist/components/{auth-wall.js → shell/auth-wall.js} +1 -1
  154. package/dist/components/shell/auth-wall.js.map +1 -0
  155. package/dist/components/{bottom-tab-bar.js → shell/bottom-tab-bar.js} +1 -1
  156. package/dist/components/shell/bottom-tab-bar.js.map +1 -0
  157. package/dist/components/shell/chat-composer.js +141 -0
  158. package/dist/components/shell/chat-composer.js.map +1 -0
  159. package/dist/components/shell/chat-message.js +195 -0
  160. package/dist/components/shell/chat-message.js.map +1 -0
  161. package/dist/components/shell/chat-panel.js +137 -0
  162. package/dist/components/shell/chat-panel.js.map +1 -0
  163. package/dist/components/shell/chat-stream-text.js +69 -0
  164. package/dist/components/shell/chat-stream-text.js.map +1 -0
  165. package/dist/components/shell/chat-typing.js +26 -0
  166. package/dist/components/shell/chat-typing.js.map +1 -0
  167. package/dist/components/{footer.js → shell/footer.js} +1 -1
  168. package/dist/components/shell/footer.js.map +1 -0
  169. package/dist/components/shell/header-controls.js +149 -0
  170. package/dist/components/shell/header-controls.js.map +1 -0
  171. package/dist/components/{list-rows.js → shell/list-rows.js} +1 -1
  172. package/dist/components/shell/list-rows.js.map +1 -0
  173. package/dist/components/{logo.js → shell/logo.js} +1 -1
  174. package/dist/components/shell/logo.js.map +1 -0
  175. package/dist/components/{mobile-app-bar.js → shell/mobile-app-bar.js} +1 -1
  176. package/dist/components/shell/mobile-app-bar.js.map +1 -0
  177. package/dist/components/{nav-badge.js → shell/nav-badge.js} +1 -1
  178. package/dist/components/shell/nav-badge.js.map +1 -0
  179. package/dist/components/shell/nav-collapse.js +142 -0
  180. package/dist/components/shell/nav-collapse.js.map +1 -0
  181. package/dist/components/shell/nav-link.js.map +1 -0
  182. package/dist/components/shell/nav-model.js.map +1 -0
  183. package/dist/components/{page-frame.js → shell/page-frame.js} +3 -6
  184. package/dist/components/shell/page-frame.js.map +1 -0
  185. package/dist/components/{resource-error.js → shell/resource-error.js} +4 -4
  186. package/dist/components/shell/resource-error.js.map +1 -0
  187. package/dist/components/shell/section-nav.js +139 -0
  188. package/dist/components/shell/section-nav.js.map +1 -0
  189. package/dist/components/shell/settled.js.map +1 -0
  190. package/dist/components/shell/shell-icons.js.map +1 -0
  191. package/dist/components/{shell-nav.js → shell/shell-nav.js} +81 -39
  192. package/dist/components/shell/shell-nav.js.map +1 -0
  193. package/dist/components/shell/smart-header.js +53 -0
  194. package/dist/components/shell/smart-header.js.map +1 -0
  195. package/dist/components/{status-screen.js → shell/status-screen.js} +3 -3
  196. package/dist/components/shell/status-screen.js.map +1 -0
  197. package/dist/components/{system-banner.js → shell/system-banner.js} +23 -13
  198. package/dist/components/shell/system-banner.js.map +1 -0
  199. package/dist/components/{top-bar.js → shell/top-bar.js} +5 -4
  200. package/dist/components/shell/top-bar.js.map +1 -0
  201. package/dist/components/shell/use-shell-nav.js.map +1 -0
  202. package/dist/components/shell/workspace-tab.js +140 -0
  203. package/dist/components/shell/workspace-tab.js.map +1 -0
  204. package/dist/generated/accent.js +2 -2
  205. package/dist/generated/accent.js.map +1 -1
  206. package/dist/generated/account-list.js +35 -0
  207. package/dist/generated/account-list.js.map +1 -0
  208. package/dist/generated/account-settings-dialog.js +27 -0
  209. package/dist/generated/account-settings-dialog.js.map +1 -0
  210. package/dist/generated/alert-dialog.js +1 -1
  211. package/dist/generated/alert-dialog.js.map +1 -1
  212. package/dist/generated/alert.js +2 -2
  213. package/dist/generated/alert.js.map +1 -1
  214. package/dist/generated/aside.js +36 -0
  215. package/dist/generated/aside.js.map +1 -0
  216. package/dist/generated/badge.js +7 -6
  217. package/dist/generated/badge.js.map +1 -1
  218. package/dist/generated/band.js +1 -1
  219. package/dist/generated/band.js.map +1 -1
  220. package/dist/generated/breadcrumb.js +2 -2
  221. package/dist/generated/breadcrumb.js.map +1 -1
  222. package/dist/generated/button-group.js +2 -2
  223. package/dist/generated/button-group.js.map +1 -1
  224. package/dist/generated/button.js +4 -4
  225. package/dist/generated/button.js.map +1 -1
  226. package/dist/generated/card.js +6 -6
  227. package/dist/generated/card.js.map +1 -1
  228. package/dist/generated/chat-composer.js +21 -0
  229. package/dist/generated/chat-composer.js.map +1 -0
  230. package/dist/generated/chat-message.js +86 -0
  231. package/dist/generated/chat-message.js.map +1 -0
  232. package/dist/generated/chat-typing.js +9 -0
  233. package/dist/generated/chat-typing.js.map +1 -0
  234. package/dist/generated/command-palette.js +17 -0
  235. package/dist/generated/command-palette.js.map +1 -0
  236. package/dist/generated/command.js +2 -2
  237. package/dist/generated/command.js.map +1 -1
  238. package/dist/generated/confirm-dialog.js +28 -0
  239. package/dist/generated/confirm-dialog.js.map +1 -0
  240. package/dist/generated/context-menu.js +1 -1
  241. package/dist/generated/context-menu.js.map +1 -1
  242. package/dist/generated/date-time-picker.js +1 -1
  243. package/dist/generated/date-time-picker.js.map +1 -1
  244. package/dist/generated/dialog.js +1 -1
  245. package/dist/generated/dialog.js.map +1 -1
  246. package/dist/generated/drawer.js +1 -1
  247. package/dist/generated/drawer.js.map +1 -1
  248. package/dist/generated/dropdown-menu.js +1 -1
  249. package/dist/generated/dropdown-menu.js.map +1 -1
  250. package/dist/generated/form-row.js +17 -0
  251. package/dist/generated/form-row.js.map +1 -0
  252. package/dist/generated/header-controls.js +56 -0
  253. package/dist/generated/header-controls.js.map +1 -0
  254. package/dist/generated/icon-button.js +13 -0
  255. package/dist/generated/icon-button.js.map +1 -0
  256. package/dist/generated/key-value.js +15 -0
  257. package/dist/generated/key-value.js.map +1 -0
  258. package/dist/generated/menubar.js +1 -1
  259. package/dist/generated/menubar.js.map +1 -1
  260. package/dist/generated/navigation-menu.js +1 -1
  261. package/dist/generated/navigation-menu.js.map +1 -1
  262. package/dist/generated/popover-form.js +13 -0
  263. package/dist/generated/popover-form.js.map +1 -0
  264. package/dist/generated/section-label.js +10 -0
  265. package/dist/generated/section-label.js.map +1 -0
  266. package/dist/generated/select.js +15 -3
  267. package/dist/generated/select.js.map +1 -1
  268. package/dist/generated/settings.js +36 -0
  269. package/dist/generated/settings.js.map +1 -0
  270. package/dist/generated/sheet.js +1 -1
  271. package/dist/generated/sheet.js.map +1 -1
  272. package/dist/generated/slider.js +1 -1
  273. package/dist/generated/slider.js.map +1 -1
  274. package/dist/generated/smart-header.js +11 -0
  275. package/dist/generated/smart-header.js.map +1 -0
  276. package/dist/generated/sonner.js +4 -4
  277. package/dist/generated/sonner.js.map +1 -1
  278. package/dist/generated/status-item.js +17 -0
  279. package/dist/generated/status-item.js.map +1 -0
  280. package/dist/generated/system-banner.js +33 -0
  281. package/dist/generated/system-banner.js.map +1 -0
  282. package/dist/generated/top-bar.js +9 -0
  283. package/dist/generated/top-bar.js.map +1 -0
  284. package/dist/generated/workspace-tab.js +17 -0
  285. package/dist/generated/workspace-tab.js.map +1 -0
  286. package/dist/icons/select-chevron.js.map +1 -0
  287. package/dist/icons/ui-icons.js +77 -0
  288. package/dist/icons/ui-icons.js.map +1 -0
  289. package/dist/index.d.ts +852 -156
  290. package/dist/index.js +96 -76
  291. package/dist/lib/cn.js +8 -1
  292. package/dist/lib/cn.js.map +1 -1
  293. package/dist/lib/number-parts.js +90 -0
  294. package/dist/lib/number-parts.js.map +1 -0
  295. package/dist/palette-cli.js +1 -1
  296. package/dist/palette.d.ts +6 -4
  297. package/dist/palette.js +3 -1
  298. package/dist/primitives/dismissable-layer.js +76 -4
  299. package/dist/primitives/dismissable-layer.js.map +1 -1
  300. package/dist/primitives/focus-scope.js +20 -5
  301. package/dist/primitives/focus-scope.js.map +1 -1
  302. package/dist/primitives/use-floating.js +16 -1
  303. package/dist/primitives/use-floating.js.map +1 -1
  304. package/dist/primitives/use-reduced-motion.js +20 -0
  305. package/dist/primitives/use-reduced-motion.js.map +1 -0
  306. package/package.json +2 -2
  307. package/styles/ev.css +281 -49
  308. package/styles/theme.css +335 -39
  309. package/styles/tokens.css +643 -103
  310. package/dist/chunk-3IPB4QQJ.js.map +0 -1
  311. package/dist/components/accordion.js.map +0 -1
  312. package/dist/components/account-menu.js +0 -70
  313. package/dist/components/account-menu.js.map +0 -1
  314. package/dist/components/alert-dialog.js.map +0 -1
  315. package/dist/components/alert.js.map +0 -1
  316. package/dist/components/app-shell.js.map +0 -1
  317. package/dist/components/auth-wall.js.map +0 -1
  318. package/dist/components/avatar.js.map +0 -1
  319. package/dist/components/badge.js.map +0 -1
  320. package/dist/components/band.js.map +0 -1
  321. package/dist/components/bottom-tab-bar.js.map +0 -1
  322. package/dist/components/breadcrumb.js.map +0 -1
  323. package/dist/components/button-group.js.map +0 -1
  324. package/dist/components/button.js.map +0 -1
  325. package/dist/components/calendar.js.map +0 -1
  326. package/dist/components/card.js.map +0 -1
  327. package/dist/components/carousel.js.map +0 -1
  328. package/dist/components/checkbox.js.map +0 -1
  329. package/dist/components/collapsible.js.map +0 -1
  330. package/dist/components/command.js.map +0 -1
  331. package/dist/components/container.js.map +0 -1
  332. package/dist/components/context-menu.js.map +0 -1
  333. package/dist/components/date-time-picker.js.map +0 -1
  334. package/dist/components/dialog.js.map +0 -1
  335. package/dist/components/drawer.js.map +0 -1
  336. package/dist/components/dropdown-menu.js.map +0 -1
  337. package/dist/components/empty.js.map +0 -1
  338. package/dist/components/field-context.js.map +0 -1
  339. package/dist/components/field.js.map +0 -1
  340. package/dist/components/footer.js.map +0 -1
  341. package/dist/components/form.js.map +0 -1
  342. package/dist/components/hover-card.js.map +0 -1
  343. package/dist/components/info-tip.js.map +0 -1
  344. package/dist/components/input-group.js.map +0 -1
  345. package/dist/components/input-otp.js.map +0 -1
  346. package/dist/components/input.js.map +0 -1
  347. package/dist/components/item.js.map +0 -1
  348. package/dist/components/kbd.js.map +0 -1
  349. package/dist/components/label.js.map +0 -1
  350. package/dist/components/list-rows.js.map +0 -1
  351. package/dist/components/logo.js.map +0 -1
  352. package/dist/components/menubar.js.map +0 -1
  353. package/dist/components/mobile-app-bar.js.map +0 -1
  354. package/dist/components/native-select.js.map +0 -1
  355. package/dist/components/nav-badge.js.map +0 -1
  356. package/dist/components/nav-link.js.map +0 -1
  357. package/dist/components/nav-model.js.map +0 -1
  358. package/dist/components/navigation-menu.js.map +0 -1
  359. package/dist/components/page-frame.js.map +0 -1
  360. package/dist/components/pagination.js.map +0 -1
  361. package/dist/components/popover.js.map +0 -1
  362. package/dist/components/progress.js.map +0 -1
  363. package/dist/components/radio-group.js.map +0 -1
  364. package/dist/components/resource-error.js.map +0 -1
  365. package/dist/components/scroll-area.js.map +0 -1
  366. package/dist/components/section-descriptor.js.map +0 -1
  367. package/dist/components/section-nav.js +0 -86
  368. package/dist/components/section-nav.js.map +0 -1
  369. package/dist/components/select-chevron.js.map +0 -1
  370. package/dist/components/select-listbox.js.map +0 -1
  371. package/dist/components/select.js.map +0 -1
  372. package/dist/components/separator.js.map +0 -1
  373. package/dist/components/settled.js.map +0 -1
  374. package/dist/components/sheet.js.map +0 -1
  375. package/dist/components/shell-icons.js.map +0 -1
  376. package/dist/components/shell-nav.js.map +0 -1
  377. package/dist/components/sidebar.js.map +0 -1
  378. package/dist/components/skeleton.js.map +0 -1
  379. package/dist/components/slider.js.map +0 -1
  380. package/dist/components/sonner.js.map +0 -1
  381. package/dist/components/spinner.js.map +0 -1
  382. package/dist/components/status-screen.js.map +0 -1
  383. package/dist/components/switch.js.map +0 -1
  384. package/dist/components/system-banner.js.map +0 -1
  385. package/dist/components/table.js.map +0 -1
  386. package/dist/components/tabs.js.map +0 -1
  387. package/dist/components/terminal.js +0 -206
  388. package/dist/components/terminal.js.map +0 -1
  389. package/dist/components/textarea.js.map +0 -1
  390. package/dist/components/toggle-group.js.map +0 -1
  391. package/dist/components/toggle.js.map +0 -1
  392. package/dist/components/tooltip.js.map +0 -1
  393. package/dist/components/top-bar.js.map +0 -1
  394. package/dist/components/use-shell-nav.js.map +0 -1
  395. package/dist/generated/terminal.js +0 -70
  396. package/dist/generated/terminal.js.map +0 -1
  397. /package/dist/components/{field-context.js → atomic/field-context.js} +0 -0
  398. /package/dist/components/{nav-link.js → shell/nav-link.js} +0 -0
  399. /package/dist/components/{nav-model.js → shell/nav-model.js} +0 -0
  400. /package/dist/components/{settled.js → shell/settled.js} +0 -0
  401. /package/dist/components/{shell-icons.js → shell/shell-icons.js} +0 -0
  402. /package/dist/components/{use-shell-nav.js → shell/use-shell-nav.js} +0 -0
  403. /package/dist/{components → icons}/select-chevron.js +0 -0
@@ -16,6 +16,11 @@ function focusable(root) {
16
16
  (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement
17
17
  );
18
18
  }
19
+ let claimed = [];
20
+ function claim(root, saved) {
21
+ if (claimed.length === 0) queueMicrotask(() => claimed = []);
22
+ claimed.push({ root, saved });
23
+ }
19
24
  function usable(el) {
20
25
  return !!el && el !== document.body && el.isConnected;
21
26
  }
@@ -31,10 +36,18 @@ function useFocusScope(enabled, { initialFocus = "first", returnFocusTo } = {})
31
36
  if (!enabled) return;
32
37
  const root = ref.current;
33
38
  if (!root) return;
34
- const previouslyFocused = document.activeElement;
35
- const first = initialFocus === "first" ? focusable(root)[0] : void 0;
36
- if (first) first.focus();
37
- else focusRoot(root);
39
+ const inner = claimed.find((c) => c.root !== root && c.root.contains(document.activeElement));
40
+ const saved = {
41
+ el: inner ? inner.saved.el : document.activeElement,
42
+ behind: false
43
+ };
44
+ if (inner) inner.saved.behind = true;
45
+ else {
46
+ const first = initialFocus === "first" ? focusable(root)[0] : void 0;
47
+ if (first) first.focus();
48
+ else focusRoot(root);
49
+ }
50
+ claim(root, saved);
38
51
  function onKeyDown(event) {
39
52
  if (event.key !== "Tab" || !root) return;
40
53
  const items = focusable(root);
@@ -59,9 +72,11 @@ function useFocusScope(enabled, { initialFocus = "first", returnFocusTo } = {})
59
72
  root.addEventListener("keydown", onKeyDown);
60
73
  return () => {
61
74
  root.removeEventListener("keydown", onKeyDown);
75
+ claimed = claimed.filter((c) => c.root !== root);
62
76
  const active = document.activeElement;
63
77
  if (active && active !== document.body && !root.contains(active)) return;
64
- const target = usable(previouslyFocused) ? previouslyFocused : fallbackRef.current?.current;
78
+ const anchor = fallbackRef.current?.current;
79
+ const target = saved.behind && usable(anchor) ? anchor : usable(saved.el) ? saved.el : anchor;
65
80
  if (usable(target)) target.focus({ preventScroll: true });
66
81
  };
67
82
  }, [enabled, initialFocus]);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/primitives/focus-scope.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// `tabindex=\"-1\"` opts an element out of Tab order whatever its tag, so the\n// exclusion goes on every branch — otherwise a roving-tabindex grid's parked\n// cells would count as tab stops.\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]\",\n]\n .map(sel => `${sel}:not([tabindex=\"-1\"])`)\n .join(\",\");\n\n/**\n * Every enabled, Tab-reachable candidate under `root` in DOM (= Tab) order,\n * before the layout-based visibility filter; `tabindex=\"-1\"` is skipped on any\n * tag. For content that is fully visible while it exists (a popover), this is\n * the tab order itself. Internal to the kit.\n */\nexport function focusCandidates(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return focusCandidates(root).filter(\n (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement,\n );\n}\n\n/**\n * Where focus lands when the scope opens. `first` is the first Tab stop inside\n * (an input, an alert dialog's least destructive action); `container` is the\n * scope root itself, so a reader starts at the top and the first Tab reaches the\n * first control — right for a panel of content, where the first stop may be an\n * action one keypress away from firing.\n */\nexport type InitialFocus = \"first\" | \"container\";\n\nexport interface FocusScopeOptions {\n /** Default `first`; falls back to the container when nothing inside is focusable. */\n initialFocus?: InitialFocus;\n /**\n * Where focus goes back on close when the element focused before opening is\n * no use: `<body>` (Safari does not focus a clicked button), or detached (the\n * opener was re-rendered away). Pass the overlay's trigger.\n */\n returnFocusTo?: React.RefObject<HTMLElement | null>;\n}\n\nfunction usable(el: HTMLElement | null | undefined): el is HTMLElement {\n return !!el && el !== document.body && el.isConnected;\n}\n\n// A `<div>` without a tabindex ignores `.focus()`, so a scope with no Tab stops\n// used to leave focus on the trigger behind the scrim. `-1` makes the root\n// focusable by script (and by a click on its empty area, which keeps focus in\n// the scope) without adding it to the Tab order.\nfunction focusRoot(root: HTMLElement) {\n if (!root.hasAttribute(\"tabindex\")) root.tabIndex = -1;\n root.focus();\n}\n\n/**\n * Traps Tab focus inside the returned ref's subtree and restores focus to the\n * previously-focused element on unmount — the dep-light core of dialogs/menus,\n * replacing `@radix-ui/react-focus-scope`. On mount, focuses per\n * `initialFocus`; the root gets `tabindex=\"-1\"` whenever it is the target.\n *\n * Rust dialogs rely on the browser's native focus order within a fixed overlay;\n * see the README \"Limitations\".\n */\nexport function useFocusScope(\n enabled: boolean,\n { initialFocus = \"first\", returnFocusTo }: FocusScopeOptions = {},\n): React.RefObject<HTMLDivElement | null> {\n const ref = React.useRef<HTMLDivElement | null>(null);\n // Read at close, not captured at open: a trigger re-rendered while the\n // overlay was up is a new node by then.\n const fallbackRef = React.useRef(returnFocusTo);\n fallbackRef.current = returnFocusTo;\n\n React.useEffect(() => {\n if (!enabled) return;\n const root = ref.current;\n if (!root) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n const first = initialFocus === \"first\" ? focusable(root)[0] : undefined;\n if (first) first.focus();\n else focusRoot(root);\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key !== \"Tab\" || !root) return;\n const items = focusable(root);\n if (items.length === 0) {\n event.preventDefault();\n return;\n }\n const firstEl = items[0]!;\n const lastEl = items[items.length - 1]!;\n const active = document.activeElement;\n // From the root itself Shift+Tab would leave the scope for whatever\n // precedes the portal, so both directions are steered explicitly.\n if (active === root) {\n event.preventDefault();\n (event.shiftKey ? lastEl : firstEl).focus();\n } else if (event.shiftKey && active === firstEl) {\n event.preventDefault();\n lastEl.focus();\n } else if (!event.shiftKey && active === lastEl) {\n event.preventDefault();\n firstEl.focus();\n }\n }\n\n root.addEventListener(\"keydown\", onKeyDown);\n return () => {\n root.removeEventListener(\"keydown\", onKeyDown);\n // Focus that already left the scope (a click outside landed on a field)\n // is the user's; only focus still inside, or dropped to <body>, returns.\n const active = document.activeElement;\n if (active && active !== document.body && !root.contains(active)) return;\n const target = usable(previouslyFocused) ? previouslyFocused : fallbackRef.current?.current;\n if (usable(target)) target.focus({ preventScroll: true });\n };\n }, [enabled, initialFocus]);\n\n return ref;\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAKvB,MAAM,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EACG,IAAI,SAAO,GAAG,GAAG,uBAAuB,EACxC,KAAK,GAAG;AAQJ,SAAS,gBAAgB,MAAkC;AAChE,SAAO,MAAM,KAAK,KAAK,iBAA8B,SAAS,CAAC;AACjE;AAEA,SAAS,UAAU,MAAkC;AACnD,SAAO,gBAAgB,IAAI,EAAE;AAAA,IAC3B,CAAC,OAAO,GAAG,cAAc,KAAK,GAAG,eAAe,KAAK,OAAO,SAAS;AAAA,EACvE;AACF;AAsBA,SAAS,OAAO,IAAuD;AACrE,SAAO,CAAC,CAAC,MAAM,OAAO,SAAS,QAAQ,GAAG;AAC5C;AAMA,SAAS,UAAU,MAAmB;AACpC,MAAI,CAAC,KAAK,aAAa,UAAU,EAAG,MAAK,WAAW;AACpD,OAAK,MAAM;AACb;AAWO,SAAS,cACd,SACA,EAAE,eAAe,SAAS,cAAc,IAAuB,CAAC,GACxB;AACxC,QAAM,MAAM,MAAM,OAA8B,IAAI;AAGpD,QAAM,cAAc,MAAM,OAAO,aAAa;AAC9C,cAAY,UAAU;AAEtB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,IAAI;AACjB,QAAI,CAAC,KAAM;AAEX,UAAM,oBAAoB,SAAS;AACnC,UAAM,QAAQ,iBAAiB,UAAU,UAAU,IAAI,EAAE,CAAC,IAAI;AAC9D,QAAI,MAAO,OAAM,MAAM;AAAA,QAClB,WAAU,IAAI;AAEnB,aAAS,UAAU,OAAsB;AACvC,UAAI,MAAM,QAAQ,SAAS,CAAC,KAAM;AAClC,YAAM,QAAQ,UAAU,IAAI;AAC5B,UAAI,MAAM,WAAW,GAAG;AACtB,cAAM,eAAe;AACrB;AAAA,MACF;AACA,YAAM,UAAU,MAAM,CAAC;AACvB,YAAM,SAAS,MAAM,MAAM,SAAS,CAAC;AACrC,YAAM,SAAS,SAAS;AAGxB,UAAI,WAAW,MAAM;AACnB,cAAM,eAAe;AACrB,SAAC,MAAM,WAAW,SAAS,SAAS,MAAM;AAAA,MAC5C,WAAW,MAAM,YAAY,WAAW,SAAS;AAC/C,cAAM,eAAe;AACrB,eAAO,MAAM;AAAA,MACf,WAAW,CAAC,MAAM,YAAY,WAAW,QAAQ;AAC/C,cAAM,eAAe;AACrB,gBAAQ,MAAM;AAAA,MAChB;AAAA,IACF;AAEA,SAAK,iBAAiB,WAAW,SAAS;AAC1C,WAAO,MAAM;AACX,WAAK,oBAAoB,WAAW,SAAS;AAG7C,YAAM,SAAS,SAAS;AACxB,UAAI,UAAU,WAAW,SAAS,QAAQ,CAAC,KAAK,SAAS,MAAM,EAAG;AAClE,YAAM,SAAS,OAAO,iBAAiB,IAAI,oBAAoB,YAAY,SAAS;AACpF,UAAI,OAAO,MAAM,EAAG,QAAO,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,IAC1D;AAAA,EACF,GAAG,CAAC,SAAS,YAAY,CAAC;AAE1B,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/primitives/focus-scope.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// `tabindex=\"-1\"` opts an element out of Tab order whatever its tag, so the\n// exclusion goes on every branch — otherwise a roving-tabindex grid's parked\n// cells would count as tab stops.\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]\",\n]\n .map(sel => `${sel}:not([tabindex=\"-1\"])`)\n .join(\",\");\n\n/**\n * Every enabled, Tab-reachable candidate under `root` in DOM (= Tab) order,\n * before the layout-based visibility filter; `tabindex=\"-1\"` is skipped on any\n * tag. For content that is fully visible while it exists (a popover), this is\n * the tab order itself. Internal to the kit.\n */\nexport function focusCandidates(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return focusCandidates(root).filter(\n (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement,\n );\n}\n\n/**\n * Where focus lands when the scope opens. `first` is the first Tab stop inside\n * (an input, an alert dialog's least destructive action); `container` is the\n * scope root itself, so a reader starts at the top and the first Tab reaches the\n * first control — right for a panel of content, where the first stop may be an\n * action one keypress away from firing.\n */\nexport type InitialFocus = \"first\" | \"container\";\n\nexport interface FocusScopeOptions {\n /** Default `first`; falls back to the container when nothing inside is focusable. */\n initialFocus?: InitialFocus;\n /**\n * Where focus goes back on close when the element focused before opening is\n * no use: `<body>` (Safari does not focus a clicked button), or detached (the\n * opener was re-rendered away). Pass the overlay's trigger.\n */\n returnFocusTo?: React.RefObject<HTMLElement | null>;\n}\n\ninterface Claim {\n root: HTMLElement;\n // Boxed so an outer scope opened in the same commit can mark it `behind`.\n saved: { el: HTMLElement | null; behind: boolean };\n}\n\n// Scopes that took focus in the current task. Cleared in a microtask, so only\n// scopes opened in the same commit count, never one that has been open a while.\nlet claimed: Claim[] = [];\n\nfunction claim(root: HTMLElement, saved: Claim[\"saved\"]) {\n if (claimed.length === 0) queueMicrotask(() => (claimed = []));\n claimed.push({ root, saved });\n}\n\nfunction usable(el: HTMLElement | null | undefined): el is HTMLElement {\n return !!el && el !== document.body && el.isConnected;\n}\n\n// A `<div>` without a tabindex ignores `.focus()`, so a scope with no Tab stops\n// used to leave focus on the trigger behind the scrim. `-1` makes the root\n// focusable by script (and by a click on its empty area, which keeps focus in\n// the scope) without adding it to the Tab order.\nfunction focusRoot(root: HTMLElement) {\n if (!root.hasAttribute(\"tabindex\")) root.tabIndex = -1;\n root.focus();\n}\n\n/**\n * Traps Tab focus inside the returned ref's subtree and restores focus to the\n * previously-focused element on unmount — the dep-light core of dialogs/menus,\n * replacing `@radix-ui/react-focus-scope`. On mount, focuses per\n * `initialFocus`; the root gets `tabindex=\"-1\"` whenever it is the target.\n *\n * Rust dialogs rely on the browser's native focus order within a fixed overlay;\n * see the README \"Limitations\".\n */\nexport function useFocusScope(\n enabled: boolean,\n { initialFocus = \"first\", returnFocusTo }: FocusScopeOptions = {},\n): React.RefObject<HTMLDivElement | null> {\n const ref = React.useRef<HTMLDivElement | null>(null);\n // Read at close, not captured at open: a trigger re-rendered while the\n // overlay was up is a new node by then.\n const fallbackRef = React.useRef(returnFocusTo);\n fallbackRef.current = returnFocusTo;\n\n React.useEffect(() => {\n if (!enabled) return;\n const root = ref.current;\n if (!root) return;\n\n // A scope opened in the same commit (a popover inside this dialog) has\n // already run, since children's effects come first, and taken focus. This\n // one must not pull focus out of it. Both saved the element focused before\n // either opened; the inner scope sits inside this one, so on close it goes\n // back to its own trigger first and to that element only as a fallback.\n const inner = claimed.find((c) => c.root !== root && c.root.contains(document.activeElement));\n const saved: Claim[\"saved\"] = {\n el: inner ? inner.saved.el : (document.activeElement as HTMLElement | null),\n behind: false,\n };\n if (inner) inner.saved.behind = true;\n else {\n const first = initialFocus === \"first\" ? focusable(root)[0] : undefined;\n if (first) first.focus();\n else focusRoot(root);\n }\n claim(root, saved);\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key !== \"Tab\" || !root) return;\n const items = focusable(root);\n if (items.length === 0) {\n event.preventDefault();\n return;\n }\n const firstEl = items[0]!;\n const lastEl = items[items.length - 1]!;\n const active = document.activeElement;\n // From the root itself Shift+Tab would leave the scope for whatever\n // precedes the portal, so both directions are steered explicitly.\n if (active === root) {\n event.preventDefault();\n (event.shiftKey ? lastEl : firstEl).focus();\n } else if (event.shiftKey && active === firstEl) {\n event.preventDefault();\n lastEl.focus();\n } else if (!event.shiftKey && active === lastEl) {\n event.preventDefault();\n firstEl.focus();\n }\n }\n\n root.addEventListener(\"keydown\", onKeyDown);\n return () => {\n root.removeEventListener(\"keydown\", onKeyDown);\n claimed = claimed.filter((c) => c.root !== root);\n // Focus that already left the scope (a click outside landed on a field)\n // is the user's; only focus still inside, or dropped to <body>, returns.\n const active = document.activeElement;\n if (active && active !== document.body && !root.contains(active)) return;\n const anchor = fallbackRef.current?.current;\n const target = saved.behind && usable(anchor) ? anchor : usable(saved.el) ? saved.el : anchor;\n if (usable(target)) target.focus({ preventScroll: true });\n };\n }, [enabled, initialFocus]);\n\n return ref;\n}\n"],"mappings":"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","names":[]}
@@ -21,6 +21,12 @@ function toContainingBlock(floating, x, y) {
21
21
  function layoutSize(el) {
22
22
  return { width: el.offsetWidth, height: el.offsetHeight };
23
23
  }
24
+ function hasRunningAnimationAbove(el) {
25
+ for (let node = el.parentElement; node; node = node.parentElement) {
26
+ if (node.getAnimations?.().some((a) => a.playState === "running")) return true;
27
+ }
28
+ return false;
29
+ }
24
30
  function viewport() {
25
31
  const el = document.documentElement;
26
32
  return {
@@ -105,7 +111,15 @@ function useFloating(opts) {
105
111
  const { top, left } = ideal(a, f, placed);
106
112
  write(floating, f, top + shiftY, left + shiftX, true);
107
113
  }
108
- place();
114
+ let frame = 0;
115
+ function follow() {
116
+ frame = 0;
117
+ place();
118
+ if (anchorRef.current && hasRunningAnimationAbove(anchorRef.current)) {
119
+ frame = requestAnimationFrame(follow);
120
+ }
121
+ }
122
+ follow();
109
123
  window.addEventListener("scroll", sync, true);
110
124
  window.addEventListener("resize", place);
111
125
  let observer;
@@ -115,6 +129,7 @@ function useFloating(opts) {
115
129
  if (floatingRef.current) observer.observe(floatingRef.current);
116
130
  }
117
131
  return () => {
132
+ if (frame) cancelAnimationFrame(frame);
118
133
  window.removeEventListener("scroll", sync, true);
119
134
  window.removeEventListener("resize", place);
120
135
  observer?.disconnect();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/primitives/use-floating.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nexport type Side = \"top\" | \"right\" | \"bottom\" | \"left\";\nexport type Align = \"start\" | \"center\" | \"end\";\n\nexport interface FloatingResult {\n floatingRef: React.RefObject<HTMLElement | null>;\n style: React.CSSProperties;\n side: Side;\n align: Align;\n}\n\n// The element sits at its containing block's origin and is moved with the\n// `translate` property, never with `top`/`left`: an absolute box's\n// shrink-to-fit width is \"containing block minus `left`\", so a `w-fit` surface\n// pushed past the viewport's right edge (horizontal scroll, a nested scroller)\n// would collapse to one word per line. `translate` composes with `transform`,\n// so the enter animations (which animate `transform`) are unaffected. The\n// offset never goes through React: `place()` / `sync()` write it straight to\n// the element, so this style prop stays one frozen object.\nconst ABSOLUTE: React.CSSProperties = Object.freeze({ position: \"absolute\", top: 0, left: 0 });\n\nconst POSITIONED = new Set([\"relative\", \"absolute\", \"fixed\", \"sticky\"]);\n\n/**\n * Converts a viewport-relative point into the coordinate space of the\n * floating element's containing block, i.e. the numbers `position: absolute`\n * expects. floating-ui's `strategy: \"absolute\"` does the same walk.\n */\nfunction toContainingBlock(\n floating: HTMLElement,\n x: number,\n y: number,\n): { x: number; y: number; root: boolean } {\n const parent = floating.offsetParent;\n const doc = floating.ownerDocument;\n const root = parent === doc.body || parent === doc.documentElement || parent === null;\n // `offsetParent` reports `body` even when nothing is positioned; only a\n // positioned body/html really forms a containing block.\n const positioned =\n parent instanceof HTMLElement &&\n (!root || POSITIONED.has(doc.defaultView?.getComputedStyle(parent).position ?? \"\"));\n if (!positioned) {\n const win = doc.defaultView;\n return { x: x + (win?.scrollX ?? 0), y: y + (win?.scrollY ?? 0), root: true };\n }\n const p = parent.getBoundingClientRect();\n return {\n x: x - p.left - parent.clientLeft + parent.scrollLeft,\n y: y - p.top - parent.clientTop + parent.scrollTop,\n root: false,\n };\n}\n\ninterface Size2D {\n width: number;\n height: number;\n}\n\n// The layout box, not `getBoundingClientRect()`: the latter reports the box\n// mid-animation (`zoom-in-95` scales it to 0.95 on the first frame), which\n// placed a top-side popover 5 % of its height too low, over the anchor.\nfunction layoutSize(el: HTMLElement): Size2D {\n return { width: el.offsetWidth, height: el.offsetHeight };\n}\n\nfunction viewport(): { vw: number; vh: number } {\n // `clientWidth` excludes the scrollbar, which the floating element cannot\n // sit under anyway; jsdom reports 0 there, hence the fallback.\n const el = document.documentElement;\n return {\n vw: el.clientWidth || window.innerWidth,\n vh: el.clientHeight || window.innerHeight,\n };\n}\n\n/**\n * Positions a floating element against an anchor with `position: absolute` in\n * the coordinate space of its containing block (the `Portal` puts it in\n * `document.body`, so normally document coordinates) — the dep-light stand-in\n * for `@radix-ui/react-popper` / floating-ui. Supports a preferred\n * `side`/`align`, an `offset`, and a single collision flip onto the opposite\n * side when the preferred side overflows the viewport.\n *\n * Two update modes:\n * - `place()` — full pass (flip + clamp into the viewport) on open, on window\n * `resize`, and whenever the anchor or the floating element changes size.\n * - `sync()` — re-derives the coordinates from the anchor with the side and\n * the clamp shift already chosen; runs on every `scroll` (captured on\n * `window`, so nested scroll containers count too).\n *\n * Because the element is absolutely positioned in the document, a document\n * scroll moves it natively with the page — `sync()` then computes the same\n * numbers and is a no-op — so the overlay never lags behind the compositor.\n * Only a nested scroll container changes the coordinates, and there a frame\n * of lag is acceptable. The side is never revisited on scroll: no jumping.\n * A page that scrolls `body` instead of the document (`html { overflow:\n * hidden } body { overflow: auto }`) is such a nested scroller: correct, but\n * with the frame of lag the `fixed` strategy had.\n *\n * Rust's overlays use CSS-only placement (`data-side`) and do not measure; see\n * the README \"Limitations\".\n */\nexport function useFloating(opts: {\n anchorRef: React.RefObject<Element | null>;\n open: boolean;\n side?: Side;\n align?: Align;\n offset?: number;\n}): FloatingResult {\n const { anchorRef, open, side = \"bottom\", align = \"center\", offset = 4 } = opts;\n const floatingRef = React.useRef<HTMLElement | null>(null);\n const [placedSide, setPlacedSide] = React.useState<Side>(side);\n // The last full placement: the side that won the flip and how far the\n // viewport clamp pushed the element off its ideal spot. `sync()` reuses both\n // instead of measuring against the viewport again.\n const placement = React.useRef<{ side: Side; shiftX: number; shiftY: number }>({\n side,\n shiftX: 0,\n shiftY: 0,\n });\n\n React.useLayoutEffect(() => {\n if (!open) return;\n\n function ideal(a: DOMRect, f: Size2D, placed: Side): { top: number; left: number } {\n let top = 0;\n let left = 0;\n const vertical = placed === \"top\" || placed === \"bottom\";\n\n if (placed === \"bottom\") top = a.bottom + offset;\n else if (placed === \"top\") top = a.top - offset - f.height;\n else if (placed === \"right\") left = a.right + offset;\n else left = a.left - offset - f.width;\n\n if (vertical) {\n if (align === \"start\") left = a.left;\n else if (align === \"end\") left = a.right - f.width;\n else left = a.left + a.width / 2 - f.width / 2;\n } else {\n if (align === \"start\") top = a.top;\n else if (align === \"end\") top = a.bottom - f.height;\n else top = a.top + a.height / 2 - f.height / 2;\n }\n return { top, left };\n }\n\n // `contain` keeps the box inside the initial containing block: a box past\n // the document's edge grows the page (a scrollbar shows for as long as the\n // overlay is open). `place()` already clamps against the viewport;\n // `sync()` follows a nested scroller's anchor and only needs this bound.\n function write(floating: HTMLElement, f: Size2D, top: number, left: number, contain: boolean) {\n const p = toContainingBlock(floating, left, top);\n let { x, y } = p;\n if (contain && p.root) {\n const icbWidth = floating.ownerDocument.documentElement.clientWidth || window.innerWidth;\n x = Math.max(0, Math.min(x, icbWidth - f.width));\n y = Math.max(0, y);\n }\n floating.style.translate = `${Math.round(x)}px ${Math.round(y)}px`;\n }\n\n function place() {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const a = anchor.getBoundingClientRect();\n const f = layoutSize(floating);\n const { vw, vh } = viewport();\n\n let placed: Side = side;\n if (side === \"bottom\" && a.bottom + offset + f.height > vh && a.top - offset - f.height > 0)\n placed = \"top\";\n else if (side === \"top\" && a.top - offset - f.height < 0 && a.bottom + offset + f.height < vh)\n placed = \"bottom\";\n else if (side === \"right\" && a.right + offset + f.width > vw && a.left - offset - f.width > 0)\n placed = \"left\";\n else if (side === \"left\" && a.left - offset - f.width < 0 && a.right + offset + f.width < vw)\n placed = \"right\";\n\n const { top, left } = ideal(a, f, placed);\n const clampedLeft = Math.max(offset, Math.min(left, vw - f.width - offset));\n const clampedTop = Math.max(offset, Math.min(top, vh - f.height - offset));\n\n placement.current = {\n side: placed,\n shiftX: clampedLeft - left,\n shiftY: clampedTop - top,\n };\n write(floating, f, clampedTop, clampedLeft, false);\n // Only a real change may re-render: this runs on every resize tick.\n setPlacedSide(prev => (prev === placed ? prev : placed));\n }\n\n function sync() {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const a = anchor.getBoundingClientRect();\n const f = layoutSize(floating);\n const { side: placed, shiftX, shiftY } = placement.current;\n const { top, left } = ideal(a, f, placed);\n write(floating, f, top + shiftY, left + shiftX, true);\n }\n\n place();\n // No rAF: `scroll` already fires at most once per frame, and a synchronous\n // DOM write from the handler lands in that same frame — rAF would cost one.\n window.addEventListener(\"scroll\", sync, true);\n window.addEventListener(\"resize\", place);\n\n let observer: ResizeObserver | undefined;\n if (typeof ResizeObserver !== \"undefined\") {\n observer = new ResizeObserver(place);\n if (anchorRef.current) observer.observe(anchorRef.current);\n if (floatingRef.current) observer.observe(floatingRef.current);\n }\n\n return () => {\n window.removeEventListener(\"scroll\", sync, true);\n window.removeEventListener(\"resize\", place);\n observer?.disconnect();\n };\n }, [open, side, align, offset, anchorRef]);\n\n return { floatingRef, style: ABSOLUTE, side: placedSide, align };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/primitives/use-floating.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nexport type Side = \"top\" | \"right\" | \"bottom\" | \"left\";\nexport type Align = \"start\" | \"center\" | \"end\";\n\nexport interface FloatingResult {\n floatingRef: React.RefObject<HTMLElement | null>;\n style: React.CSSProperties;\n side: Side;\n align: Align;\n}\n\n// The element sits at its containing block's origin and is moved with the\n// `translate` property, never with `top`/`left`: an absolute box's\n// shrink-to-fit width is \"containing block minus `left`\", so a `w-fit` surface\n// pushed past the viewport's right edge (horizontal scroll, a nested scroller)\n// would collapse to one word per line. `translate` composes with `transform`,\n// so the enter animations (which animate `transform`) are unaffected. The\n// offset never goes through React: `place()` / `sync()` write it straight to\n// the element, so this style prop stays one frozen object.\nconst ABSOLUTE: React.CSSProperties = Object.freeze({ position: \"absolute\", top: 0, left: 0 });\n\nconst POSITIONED = new Set([\"relative\", \"absolute\", \"fixed\", \"sticky\"]);\n\n/**\n * Converts a viewport-relative point into the coordinate space of the\n * floating element's containing block, i.e. the numbers `position: absolute`\n * expects. floating-ui's `strategy: \"absolute\"` does the same walk.\n */\nfunction toContainingBlock(\n floating: HTMLElement,\n x: number,\n y: number,\n): { x: number; y: number; root: boolean } {\n const parent = floating.offsetParent;\n const doc = floating.ownerDocument;\n const root = parent === doc.body || parent === doc.documentElement || parent === null;\n // `offsetParent` reports `body` even when nothing is positioned; only a\n // positioned body/html really forms a containing block.\n const positioned =\n parent instanceof HTMLElement &&\n (!root || POSITIONED.has(doc.defaultView?.getComputedStyle(parent).position ?? \"\"));\n if (!positioned) {\n const win = doc.defaultView;\n return { x: x + (win?.scrollX ?? 0), y: y + (win?.scrollY ?? 0), root: true };\n }\n const p = parent.getBoundingClientRect();\n return {\n x: x - p.left - parent.clientLeft + parent.scrollLeft,\n y: y - p.top - parent.clientTop + parent.scrollTop,\n root: false,\n };\n}\n\ninterface Size2D {\n width: number;\n height: number;\n}\n\n// The layout box, not `getBoundingClientRect()`: the latter reports the box\n// mid-animation (`zoom-in-95` scales it to 0.95 on the first frame), which\n// placed a top-side popover 5 % of its height too low, over the anchor.\nfunction layoutSize(el: HTMLElement): Size2D {\n return { width: el.offsetWidth, height: el.offsetHeight };\n}\n\n// jsdom has no `getAnimations`; there nothing animates.\nfunction hasRunningAnimationAbove(el: Element): boolean {\n for (let node = el.parentElement; node; node = node.parentElement) {\n if (node.getAnimations?.().some((a) => a.playState === \"running\")) return true;\n }\n return false;\n}\n\nfunction viewport(): { vw: number; vh: number } {\n // `clientWidth` excludes the scrollbar, which the floating element cannot\n // sit under anyway; jsdom reports 0 there, hence the fallback.\n const el = document.documentElement;\n return {\n vw: el.clientWidth || window.innerWidth,\n vh: el.clientHeight || window.innerHeight,\n };\n}\n\n/**\n * Positions a floating element against an anchor with `position: absolute` in\n * the coordinate space of its containing block (the `Portal` puts it in\n * `document.body`, so normally document coordinates) — the dep-light stand-in\n * for `@radix-ui/react-popper` / floating-ui. Supports a preferred\n * `side`/`align`, an `offset`, and a single collision flip onto the opposite\n * side when the preferred side overflows the viewport.\n *\n * Two update modes:\n * - `place()` — full pass (flip + clamp into the viewport) on open, on window\n * `resize`, and whenever the anchor or the floating element changes size.\n * - `sync()` — re-derives the coordinates from the anchor with the side and\n * the clamp shift already chosen; runs on every `scroll` (captured on\n * `window`, so nested scroll containers count too).\n *\n * Because the element is absolutely positioned in the document, a document\n * scroll moves it natively with the page — `sync()` then computes the same\n * numbers and is a no-op — so the overlay never lags behind the compositor.\n * Only a nested scroll container changes the coordinates, and there a frame\n * of lag is acceptable. The side is never revisited on scroll: no jumping.\n * A page that scrolls `body` instead of the document (`html { overflow:\n * hidden } body { overflow: auto }`) is such a nested scroller: correct, but\n * with the frame of lag the `fixed` strategy had.\n *\n * Rust's overlays use CSS-only placement (`data-side`) and do not measure; see\n * the README \"Limitations\".\n */\nexport function useFloating(opts: {\n anchorRef: React.RefObject<Element | null>;\n open: boolean;\n side?: Side;\n align?: Align;\n offset?: number;\n}): FloatingResult {\n const { anchorRef, open, side = \"bottom\", align = \"center\", offset = 4 } = opts;\n const floatingRef = React.useRef<HTMLElement | null>(null);\n const [placedSide, setPlacedSide] = React.useState<Side>(side);\n // The last full placement: the side that won the flip and how far the\n // viewport clamp pushed the element off its ideal spot. `sync()` reuses both\n // instead of measuring against the viewport again.\n const placement = React.useRef<{ side: Side; shiftX: number; shiftY: number }>({\n side,\n shiftX: 0,\n shiftY: 0,\n });\n\n React.useLayoutEffect(() => {\n if (!open) return;\n\n function ideal(a: DOMRect, f: Size2D, placed: Side): { top: number; left: number } {\n let top = 0;\n let left = 0;\n const vertical = placed === \"top\" || placed === \"bottom\";\n\n if (placed === \"bottom\") top = a.bottom + offset;\n else if (placed === \"top\") top = a.top - offset - f.height;\n else if (placed === \"right\") left = a.right + offset;\n else left = a.left - offset - f.width;\n\n if (vertical) {\n if (align === \"start\") left = a.left;\n else if (align === \"end\") left = a.right - f.width;\n else left = a.left + a.width / 2 - f.width / 2;\n } else {\n if (align === \"start\") top = a.top;\n else if (align === \"end\") top = a.bottom - f.height;\n else top = a.top + a.height / 2 - f.height / 2;\n }\n return { top, left };\n }\n\n // `contain` keeps the box inside the initial containing block: a box past\n // the document's edge grows the page (a scrollbar shows for as long as the\n // overlay is open). `place()` already clamps against the viewport;\n // `sync()` follows a nested scroller's anchor and only needs this bound.\n function write(floating: HTMLElement, f: Size2D, top: number, left: number, contain: boolean) {\n const p = toContainingBlock(floating, left, top);\n let { x, y } = p;\n if (contain && p.root) {\n const icbWidth = floating.ownerDocument.documentElement.clientWidth || window.innerWidth;\n x = Math.max(0, Math.min(x, icbWidth - f.width));\n y = Math.max(0, y);\n }\n floating.style.translate = `${Math.round(x)}px ${Math.round(y)}px`;\n }\n\n function place() {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const a = anchor.getBoundingClientRect();\n const f = layoutSize(floating);\n const { vw, vh } = viewport();\n\n let placed: Side = side;\n if (side === \"bottom\" && a.bottom + offset + f.height > vh && a.top - offset - f.height > 0)\n placed = \"top\";\n else if (side === \"top\" && a.top - offset - f.height < 0 && a.bottom + offset + f.height < vh)\n placed = \"bottom\";\n else if (side === \"right\" && a.right + offset + f.width > vw && a.left - offset - f.width > 0)\n placed = \"left\";\n else if (side === \"left\" && a.left - offset - f.width < 0 && a.right + offset + f.width < vw)\n placed = \"right\";\n\n const { top, left } = ideal(a, f, placed);\n const clampedLeft = Math.max(offset, Math.min(left, vw - f.width - offset));\n const clampedTop = Math.max(offset, Math.min(top, vh - f.height - offset));\n\n placement.current = {\n side: placed,\n shiftX: clampedLeft - left,\n shiftY: clampedTop - top,\n };\n write(floating, f, clampedTop, clampedLeft, false);\n // Only a real change may re-render: this runs on every resize tick.\n setPlacedSide(prev => (prev === placed ? prev : placed));\n }\n\n function sync() {\n const anchor = anchorRef.current;\n const floating = floatingRef.current;\n if (!anchor || !floating) return;\n\n const a = anchor.getBoundingClientRect();\n const f = layoutSize(floating);\n const { side: placed, shiftX, shiftY } = placement.current;\n const { top, left } = ideal(a, f, placed);\n write(floating, f, top + shiftY, left + shiftX, true);\n }\n\n // An anchor inside an entering dialog is measured scaled and shifted by the\n // dialog's animation (`zoom-in-95`), which places the floating element off\n // its anchor for good: no resize or scroll follows the animation's end. So\n // keep re-placing, a frame at a time, until no ancestor animates.\n let frame = 0;\n function follow() {\n frame = 0;\n place();\n if (anchorRef.current && hasRunningAnimationAbove(anchorRef.current)) {\n frame = requestAnimationFrame(follow);\n }\n }\n follow();\n // No rAF: `scroll` already fires at most once per frame, and a synchronous\n // DOM write from the handler lands in that same frame — rAF would cost one.\n window.addEventListener(\"scroll\", sync, true);\n window.addEventListener(\"resize\", place);\n\n let observer: ResizeObserver | undefined;\n if (typeof ResizeObserver !== \"undefined\") {\n observer = new ResizeObserver(place);\n if (anchorRef.current) observer.observe(anchorRef.current);\n if (floatingRef.current) observer.observe(floatingRef.current);\n }\n\n return () => {\n if (frame) cancelAnimationFrame(frame);\n window.removeEventListener(\"scroll\", sync, true);\n window.removeEventListener(\"resize\", place);\n observer?.disconnect();\n };\n }, [open, side, align, offset, anchorRef]);\n\n return { floatingRef, style: ABSOLUTE, side: placedSide, align };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const QUERY = "(prefers-reduced-motion: reduce)";
4
+ function subscribe(notify) {
5
+ if (typeof window.matchMedia !== "function") return () => {
6
+ };
7
+ const mq = window.matchMedia(QUERY);
8
+ mq.addEventListener("change", notify);
9
+ return () => mq.removeEventListener("change", notify);
10
+ }
11
+ function snapshot() {
12
+ return typeof window.matchMedia === "function" && window.matchMedia(QUERY).matches;
13
+ }
14
+ function useReducedMotion() {
15
+ return React.useSyncExternalStore(subscribe, snapshot, () => false);
16
+ }
17
+ export {
18
+ useReducedMotion
19
+ };
20
+ //# sourceMappingURL=use-reduced-motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/primitives/use-reduced-motion.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nconst QUERY = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribe(notify: () => void): () => void {\n if (typeof window.matchMedia !== \"function\") return () => {};\n const mq = window.matchMedia(QUERY);\n mq.addEventListener(\"change\", notify);\n return () => mq.removeEventListener(\"change\", notify);\n}\n\nfunction snapshot(): boolean {\n return typeof window.matchMedia === \"function\" && window.matchMedia(QUERY).matches;\n}\n\n/**\n * True when the user asked for less motion. The server and the hydration pass\n * answer false, so markup matches; a reduced-motion client corrects it on the\n * first commit, before anything has animated.\n */\nexport function useReducedMotion(): boolean {\n return React.useSyncExternalStore(subscribe, snapshot, () => false);\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAEvB,MAAM,QAAQ;AAEd,SAAS,UAAU,QAAgC;AACjD,MAAI,OAAO,OAAO,eAAe,WAAY,QAAO,MAAM;AAAA,EAAC;AAC3D,QAAM,KAAK,OAAO,WAAW,KAAK;AAClC,KAAG,iBAAiB,UAAU,MAAM;AACpC,SAAO,MAAM,GAAG,oBAAoB,UAAU,MAAM;AACtD;AAEA,SAAS,WAAoB;AAC3B,SAAO,OAAO,OAAO,eAAe,cAAc,OAAO,WAAW,KAAK,EAAE;AAC7E;AAOO,SAAS,mBAA4B;AAC1C,SAAO,MAAM,qBAAqB,WAAW,UAAU,MAAM,KAAK;AACpE;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evinvest/uikit",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Dep-light React UI kit: shadcn-semantics components with zero @radix-ui / cva, mirroring the ev_lib::uikit Rust feature. Ships the shared design tokens.",
5
5
  "license": "BlueOak-1.0.0",
6
6
  "publishConfig": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/EV-Main/lib.git",
11
+ "url": "git+https://github.com/EV-Main/web_std.git",
12
12
  "directory": "ts/uikit"
13
13
  },
14
14
  "type": "module",
package/styles/ev.css CHANGED
@@ -1,75 +1,307 @@
1
1
  /* AUTO-GENERATED by `nix run .#gen` from the repo-root ev.css — do not edit. */
2
2
  /* ═══ PALETTE: EV ════════════════════════════════════════════════
3
3
  *
4
- * Zero specificity: EV is the default a brand scope overrides, whatever order
5
- * the two sheets are imported in.
4
+ * Three themes, each a zero-specificity rule: a brand scope overrides them
5
+ * whatever order the sheets are imported in, and later rules here win over
6
+ * earlier ones on the same element (so the order below is load-bearing).
6
7
  *
7
- * Surface ladder — dark UI reads as unfinished when every plane is the same
8
- * value, so the steps below are deliberate and measured; keep them apart if you
9
- * retune. background #070d18 → secondary #0d1526 (1.07:1) → card #16233c (1.24:1
10
- * vs background) → popover #1d2c49 (1.13:1 vs card) → muted #26375c. Elevation
11
- * rides the same ladder via --shadow-card / -elevated / -overlay. Card is also
12
- * pinned from above by a second floor: --primary has to clear 3:1 against it
13
- * (3.39:1 today), so lifting card one step breaks the tokens test.
8
+ * u2 Dark the default: `:root`, `[data-brand="ev"]`, or a `.dark` wrapper
9
+ * u2 Light a `.light` wrapper (on <html> or on any section)
10
+ * Oldschool `[data-theme="oldschool"]`: the navy look the kit shipped before
11
+ * 1.0, single-polarity, rounder, with shadows
12
+ *
13
+ * Each rule pins every EV-specific token, so a wrapper of one theme nested in
14
+ * another replaces all of it and leaks nothing. A brand scope is not EV: the
15
+ * `:not(…)` guard keeps these pins out of a `[data-brand]` subtree (and off the
16
+ * brand's own element), where the brand's palette and the derived formulas in
17
+ * theme.css apply instead. Without it a brand's `.light` would inherit EV
18
+ * Light's hover and borders.
19
+ *
20
+ * Surface ladder — the u2 planes sit 1.04–1.14:1 apart, which is the design
21
+ * (flat, no tinted fills) and under the ~1.2 floor in AGENTS.md. Planes are
22
+ * separated by a `--border` hairline instead: 1.37–1.46:1 on Dark and 1.35:1 on
23
+ * Light, measured with the alpha composited onto the plane. Do not lift a plane
24
+ * to chase the ratio; retune the hairline, and tokens.test.ts will measure it.
25
+ * Oldschool keeps its own, wider ladder: #070d18 → card #16233c (1.24:1) →
26
+ * popover #1d2c49 (1.13:1), with --primary held at 3:1 against the popover.
27
+ *
28
+ * Where a Figma value failed a floor the nearest passing one is pinned
29
+ * instead; the comment beside each says which.
14
30
  */
15
- :where(:root, [data-brand="ev"]) {
31
+
32
+ /* ── u2 Dark ─────────────────────────────────────────────────── */
33
+ :where(:is(:root, [data-brand="ev"], .dark):not([data-brand]:not([data-brand="ev"]), [data-brand]:not([data-brand="ev"]) *)) {
16
34
  /* the polarity, for what the platform draws itself (a native <select>'s
17
35
  * popup); a custom property so nothing but the kit's opt-ins reads it */
18
36
  --scheme: dark;
19
37
 
20
- /* ── surfaces ────────────────────────────────────────────────── */
38
+ /* surfaces */
39
+ --background: #0d0d0f;
40
+ --card: #151518;
41
+ --secondary: #1c1c20;
42
+ --on-secondary: var(--ink);
43
+ --popover: #252529;
44
+ --muted: #070709;
45
+ --hover: #e6e1d30f;
46
+ --active: #e6e1d31a;
47
+ --selected: #e6e1d31a;
48
+ --scrim: #000000a8;
49
+
50
+ /* ink: the Figma steps, opaque, so the text floor is measured on the value
51
+ * that paints (tertiary is 4.53:1 on the popover, the tightest plane) */
52
+ --ink: #e6e1d3;
53
+ --ink-mid: #bebab1;
54
+ --ink-soft: #8d8c87;
55
+ --ink-faint: #5e5e5b;
56
+
57
+ /* lines. --input stays the derived 48 % ink: the control edge owes 3:1 and
58
+ * Figma has no variable for it. */
59
+ --border-subtle: #e6e1d314;
60
+ --border: #e6e1d324;
61
+ --border-strong: #e6e1d342;
62
+ --ring: #35a395;
63
+
64
+ /* brand and primary. A fill and an ink are different jobs (docs/spec/accents.md):
65
+ * --primary carries white at 4.62:1 and reads as a shape on every plane (3.30:1
66
+ * on the popover); --primary-ink is the teal that reads as text (4.96:1 there).
67
+ * Figma's hover #18998b is 3.52:1 under white, so #127c71 (5.06:1) is pinned. */
68
+ --brand: #0d1627;
69
+ --primary: #138377;
70
+ --on-primary: #ffffff;
71
+ --primary-ink: #35a395;
72
+ --primary-hover: #127c71;
73
+ --primary-border: #35a3958c;
74
+
75
+ /* status. Fills carry the dark label (white fails 3:1–3.4:1 on these); the
76
+ * *-ink values are the ones to use as text. */
77
+ --positive: #2e9e5c;
78
+ --on-positive: #0d0d0f;
79
+ --positive-ink: #4abf76;
80
+ --accent-trace: #6b7fa0;
81
+ --on-accent-trace: #0d0d0f;
82
+ --accent-debug: #35a395;
83
+ --on-accent-debug: #0d0d0f;
84
+ --accent-info: #4990d8;
85
+ --on-accent-info: #0d0d0f;
86
+ --accent-info-ink: #6ca6e4;
87
+ --accent-warn: #f5c84e;
88
+ --on-accent-warn: #0d0d0f;
89
+ --accent-error: #f05b52;
90
+ --on-accent-error: #0d0d0f;
91
+
92
+ --chart-1: #35a395;
93
+ --chart-2: #4990d8;
94
+ --chart-3: #f5c84e;
95
+ --chart-4: #e68aaf;
96
+ --chart-5: #2e9e5c;
97
+ --chart-6: #988eef;
98
+
99
+ --button-primary-bg: #e6e1d300;
100
+ --button-primary-hover: #e6e1d314;
101
+ --button-primary-border: #e6e1d399;
102
+ --button-primary-ink: #e6e1d3;
103
+ --button-danger-bg: #f05b5200;
104
+ --button-danger-hover: #f05b521a;
105
+ --button-danger-border: #f05b52;
106
+ --button-danger-ink: var(--accent-error);
107
+
108
+ /* chips are outlines: a transparent fill and the label's hue at 50 % */
109
+ --chip-neutral-bg: var(--hover);
110
+ --chip-neutral-border: var(--border);
111
+ --chip-primary-bg: #35a39500;
112
+ --chip-primary-border: #35a3958c;
113
+ --chip-positive-bg: #2e9e5c00;
114
+ --chip-positive-border: #4abf7680;
115
+ --chip-info-bg: #4990d800;
116
+ --chip-info-border: #6ca6e480;
117
+ --chip-warn-bg: #f5c84e00;
118
+ --chip-warn-border: #f5c84e80;
119
+ --chip-error-bg: #f05b5200;
120
+ --chip-error-border: #f05b5280;
121
+ }
122
+
123
+ /* ── u2 Light ────────────────────────────────────────────────── */
124
+ :where(.light:not([data-brand]:not([data-brand="ev"]), [data-brand]:not([data-brand="ev"]) *)) {
125
+ --scheme: light;
126
+
127
+ --background: #efebdf;
128
+ --card: #f6f3eb;
129
+ --secondary: #fbfaf6;
130
+ --on-secondary: var(--ink);
131
+ --popover: #ffffff;
132
+ --muted: #e6e1d3;
133
+ --hover: #0d0d0f0d;
134
+ --active: #0d0d0f17;
135
+ --selected: #0d0d0f14;
136
+ --scrim: #00000066;
137
+
138
+ --ink: #0d0d0f;
139
+ --ink-mid: #3d3d41;
140
+ --ink-soft: #67696c;
141
+ --ink-faint: #9a9b9e;
142
+
143
+ --border-subtle: #0d0d0f14;
144
+ --border: #0d0d0f24;
145
+ --border-strong: #0d0d0f47;
146
+ --ring: #138377;
147
+
148
+ --brand: #080e1a;
149
+ --primary: #138377;
150
+ --on-primary: #ffffff;
151
+ --primary-ink: #116f65;
152
+ --primary-hover: #116f65;
153
+ --primary-border: #116f658c;
154
+
155
+ /* Figma's positive #228a4e carries neither label at 4.5:1 (white 4.36, dark
156
+ * 4.45), so the fill is one step darker: white reads at 5.01:1. */
157
+ --positive: #1f7f48;
158
+ --on-positive: #ffffff;
159
+ --positive-ink: #1b7943;
160
+ --accent-trace: #5a6b7c;
161
+ --on-accent-trace: #ffffff;
162
+ --accent-debug: #116f65;
163
+ --on-accent-debug: #ffffff;
164
+ --accent-info: #2d79c2;
165
+ --on-accent-info: #ffffff;
166
+ --accent-info-ink: #25639f;
167
+ /* Figma's #d8a523 is 1.89:1 on the page plane; warn is text in its own
168
+ * colour, so it is darkened toward Figma's warn text #846305 until it clears
169
+ * the non-text 3:1 (3.10:1) */
170
+ --accent-warn: #a77f12;
171
+ --on-accent-warn: #0d0d0f;
172
+ --accent-error: #db3834;
173
+ --on-accent-error: #ffffff;
174
+
175
+ --chart-1: #116f65;
176
+ --chart-2: #2d79c2;
177
+ --chart-3: #d8a523;
178
+ --chart-4: #b74679;
179
+ --chart-5: #228a4e;
180
+ --chart-6: #6d5acc;
181
+
182
+ --button-primary-bg: #0d0d0f00;
183
+ --button-primary-hover: #0d0d0f0f;
184
+ --button-primary-border: #0d0d0fa6;
185
+ --button-primary-ink: #080e1a;
186
+ --button-danger-bg: #db383400;
187
+ --button-danger-hover: #db383414;
188
+ --button-danger-border: #db3834;
189
+ --button-danger-ink: var(--accent-error);
190
+
191
+ /* the solid hue at 50 % (warning 60 %), where Dark uses the ink's */
192
+ --chip-neutral-bg: var(--hover);
193
+ --chip-neutral-border: var(--border);
194
+ --chip-primary-bg: #13837700;
195
+ --chip-primary-border: #116f658c;
196
+ --chip-positive-bg: #228a4e00;
197
+ --chip-positive-border: #228a4e80;
198
+ --chip-info-bg: #2d79c200;
199
+ --chip-info-border: #2d79c280;
200
+ --chip-warn-bg: #a77f1200;
201
+ --chip-warn-border: #a77f1299;
202
+ --chip-error-bg: #db383400;
203
+ --chip-error-border: #db383480;
204
+ }
205
+
206
+ /* ── Oldschool ───────────────────────────────────────────────── *
207
+ * Opt-in. Dark-only, so a `.light` or `.dark` inside it takes these values
208
+ * too. The surfaces are the ones the kit shipped (they pass every floor in
209
+ * tokens.test.ts); Figma's raised/overlay #1f2d4b measured 2.96:1 for the fill
210
+ * and 4.06:1 for tertiary text, so the popover stays #1d2c49. Its ink steps and
211
+ * --border are the derived formulas for the same reason: Figma's opaque
212
+ * border #1b2843 is 1.1:1 on the card and would leave the planes unseparated.
213
+ */
214
+ :where([data-theme="oldschool"], [data-theme="oldschool"] :is(.light, .dark)) {
215
+ --scheme: dark;
216
+
217
+ --brand-font-serif: var(--font-playfair, "Playfair Display");
218
+ --brand-font-mono: var(--font-inter, "Inter");
219
+
21
220
  --background: #070d18;
22
221
  --card: #16233c;
222
+ --secondary: #0d1526;
223
+ --on-secondary: var(--ink);
23
224
  --popover: #1d2c49;
24
225
  --muted: #26375c;
226
+ --hover: #e6e1d312;
227
+ --active: #e6e1d31f;
228
+ --selected: #35a3952e;
229
+ --scrim: #02050ea3;
25
230
 
26
- /* ── ink ─────────────────────────────────────────────────────── */
27
231
  --ink: #e6e1d3;
232
+ --ink-mid: color-mix(in srgb, var(--ink) 82%, transparent);
233
+ --ink-soft: color-mix(in srgb, var(--ink) 60%, transparent);
234
+ --ink-faint: color-mix(in srgb, var(--ink) 38%, transparent);
28
235
 
29
- /* ── roles ───────────────────────────────────────────────────── */
30
- --brand: #001e4e;
31
- /* teal, not the brand navy: navy filled a Button/Switch at ~1.1:1 against
32
- * these surfaces, i.e. an invisible control, and consumers hardcoded teal to
33
- * get around it. The brand navy still lives in --brand.
34
- *
35
- * A fill and an ink are different jobs, so the primary role carries two
36
- * values (docs/spec/accents.md). --primary-ink is the teal that reads on the
37
- * surfaces: 5.85:1 on background, 4.72:1 on card. --primary is the same hue
38
- * and chroma one step down in lightness — OKLCH L 0.550 C 0.094 H 183°
39
- * against the ink's L 0.630 C 0.101 H 183° — so the CTA carries white:
40
- * --on-primary reads on it at 4.63:1, and the fill clears the 3:1 non-text
41
- * floor against background (4.20:1), secondary (3.94:1), card (3.39:1) and
42
- * popover (3.01:1), so a checked Switch on a Card still reads as one. Muted
43
- * is the one surface under the floor (2.54:1); the controls that fill there
44
- * carry an on-primary mark, see the spec. Darkening the single value instead
45
- * would have dropped every teal label below AA. */
46
- --primary: #128377;
236
+ --border-subtle: #e6e1d312;
237
+ --border: color-mix(in srgb, var(--ink) 14%, transparent);
238
+ --border-strong: #2e3e64;
239
+ --ring: #35a395;
240
+
241
+ --brand: #080e1a;
242
+ --primary: #138377;
47
243
  --on-primary: #ffffff;
48
- --primary-ink: #2a9d8f;
49
- --secondary: #0d1526;
50
- --on-secondary: var(--ink);
51
- /* valence, not significance: a success reads as good while staying quiet */
52
- --positive: #2e9e5b;
244
+ --primary-ink: #35a395;
245
+ --primary-hover: #127c71;
246
+ --primary-border: #35a39500;
247
+
248
+ --positive: #2e9e5c;
53
249
  --on-positive: #070d18;
250
+ --positive-ink: #4abf76;
54
251
  --accent-trace: #6b7fa0;
55
252
  --on-accent-trace: #070d18;
56
- --accent-debug: #2a9d8f;
253
+ --accent-debug: #35a395;
57
254
  --on-accent-debug: #070d18;
58
- --accent-info: #4a90d9;
255
+ --accent-info: #4990d8;
59
256
  --on-accent-info: #070d18;
60
- --accent-warn: #f2c94c;
257
+ --accent-info-ink: #6ca6e4;
258
+ --accent-warn: #f5c84e;
61
259
  --on-accent-warn: #070d18;
62
- /* lifted off the brand red #da251d, which failed AA on a card. This one value
63
- * is the fill and the text on a bare surface; on its own 10 % tint (a
64
- * destructive menu row) the text is the derived --accent-error-ink, which
65
- * EV leaves to the contract: #ec8379, 4.83:1 on that tint over popover. The
66
- * vivid brand red is a gitops constant, not a UI token. */
67
- --accent-error: #ef5b52;
260
+ --accent-error: #f05b52;
68
261
  --on-accent-error: #070d18;
69
262
 
70
- --chart-1: #2a9d8f;
71
- --chart-2: #2e9e5b;
72
- --chart-3: #f2c94c;
73
- --chart-4: #e58aae;
74
- --chart-5: #4a90d9;
263
+ --chart-1: #35a395;
264
+ --chart-2: #4990d8;
265
+ --chart-3: #f5c84e;
266
+ --chart-4: #e68aaf;
267
+ --chart-5: #2e9e5c;
268
+ --chart-6: #988eef;
269
+
270
+ /* filled buttons. The danger label stays dark: white on #f05b52 is 3.32:1. */
271
+ --button-primary-bg: #138377;
272
+ --button-primary-hover: #127c71;
273
+ --button-primary-border: #138377;
274
+ --button-primary-ink: #ffffff;
275
+ --button-danger-bg: #f05b52;
276
+ --button-danger-hover: #f06c61;
277
+ --button-danger-border: #f05b52;
278
+ --button-danger-ink: #070d18;
279
+
280
+ /* chips are tinted pills with no edge */
281
+ --chip-neutral-bg: var(--hover);
282
+ --chip-neutral-border: #e6e1d300;
283
+ /* Figma tints the accent chip 16 %, which leaves its teal label at 4.03:1 on
284
+ * the card; 8 % holds 4.56:1 */
285
+ --chip-primary-bg: #35a39514;
286
+ --chip-primary-border: #35a39500;
287
+ --chip-positive-bg: #2e9e5c2e;
288
+ --chip-positive-border: #4abf7600;
289
+ --chip-info-bg: #4990d82e;
290
+ --chip-info-border: #6ca6e400;
291
+ --chip-warn-bg: #f5c84e29;
292
+ --chip-warn-border: #f5c84e00;
293
+ --chip-error-bg: #f05b522e;
294
+ --chip-error-border: #f06c6100;
295
+
296
+ --radius: 0.5rem;
297
+ --control-radius: 0.375rem;
298
+ --chip-radius: 9999px;
299
+ /* the ladder is three tiers, not one drop shadow: on a dark theme the value
300
+ * steps alone are too close to separate a resting card from a menu from a
301
+ * modal, so each tier gets its own spread and opacity */
302
+ --depth-card: 0 1px 2px 0 rgb(0 0 0 / 0.3);
303
+ --depth-elevated:
304
+ 0 4px 12px -2px rgb(0 0 0 / 0.4),
305
+ 0 2px 4px -2px rgb(0 0 0 / 0.3);
306
+ --depth-overlay: 0 16px 48px -12px rgb(0 0 0 / 0.6);
75
307
  }