@nastechai-research/ui 0.18.2

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 (352) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +21 -0
  3. package/dist/assets/filler-bg0.webp +0 -0
  4. package/dist/assets.d.ts +38 -0
  5. package/dist/fonts/Collapse-Bold.woff2 +0 -0
  6. package/dist/fonts/Collapse-BoldItalic.woff2 +0 -0
  7. package/dist/fonts/Collapse-Italic.woff2 +0 -0
  8. package/dist/fonts/Collapse-Light.woff2 +0 -0
  9. package/dist/fonts/Collapse-LightItalic.woff2 +0 -0
  10. package/dist/fonts/Collapse-Regular.woff2 +0 -0
  11. package/dist/fonts/Collapse-Thin.woff2 +0 -0
  12. package/dist/fonts/Collapse-ThinItalic.woff2 +0 -0
  13. package/dist/fonts/Mondwest-Regular.woff2 +0 -0
  14. package/dist/fonts/Neuebit-Bold.woff2 +0 -0
  15. package/dist/fonts/RulesCompressed-Medium.woff2 +0 -0
  16. package/dist/fonts/RulesCompressed-Regular.woff2 +0 -0
  17. package/dist/fonts/RulesExpanded-Bold.woff2 +0 -0
  18. package/dist/fonts/RulesExpanded-Regular.woff2 +0 -0
  19. package/dist/fonts.d.ts +6 -0
  20. package/dist/fonts.js +6 -0
  21. package/dist/hooks/use-below-breakpoint.d.ts +2 -0
  22. package/dist/hooks/use-below-breakpoint.js +17 -0
  23. package/dist/hooks/use-capped-frame.d.ts +2 -0
  24. package/dist/hooks/use-capped-frame.js +15 -0
  25. package/dist/hooks/use-confirm-delete.d.ts +10 -0
  26. package/dist/hooks/use-confirm-delete.js +35 -0
  27. package/dist/hooks/use-css-var-dims.d.ts +1 -0
  28. package/dist/hooks/use-css-var-dims.js +29 -0
  29. package/dist/hooks/use-gpu-tier.d.ts +34 -0
  30. package/dist/hooks/use-gpu-tier.js +111 -0
  31. package/dist/hooks/use-render-loop.d.ts +41 -0
  32. package/dist/hooks/use-render-loop.js +63 -0
  33. package/dist/hooks/use-smooth-controls.d.ts +16 -0
  34. package/dist/hooks/use-smooth-controls.js +217 -0
  35. package/dist/hooks/use-toast.d.ts +7 -0
  36. package/dist/hooks/use-toast.js +21 -0
  37. package/dist/index.d.ts +79 -0
  38. package/dist/index.js +107 -0
  39. package/dist/ui/basic-page.d.ts +7 -0
  40. package/dist/ui/basic-page.js +18 -0
  41. package/dist/ui/build.css +4 -0
  42. package/dist/ui/components/animated-count.d.ts +10 -0
  43. package/dist/ui/components/animated-count.js +113 -0
  44. package/dist/ui/components/ascii.d.ts +10 -0
  45. package/dist/ui/components/ascii.js +79 -0
  46. package/dist/ui/components/badge.d.ts +6 -0
  47. package/dist/ui/components/badge.js +40 -0
  48. package/dist/ui/components/badges/nous-girl.d.ts +2 -0
  49. package/dist/ui/components/badges/nous-girl.js +83 -0
  50. package/dist/ui/components/blend-mode.d.ts +28 -0
  51. package/dist/ui/components/blend-mode.js +69 -0
  52. package/dist/ui/components/blink.d.ts +6 -0
  53. package/dist/ui/components/blink.js +17 -0
  54. package/dist/ui/components/bottom-sheet.d.ts +15 -0
  55. package/dist/ui/components/bottom-sheet.js +192 -0
  56. package/dist/ui/components/button.d.ts +14 -0
  57. package/dist/ui/components/button.js +147 -0
  58. package/dist/ui/components/card.d.ts +5 -0
  59. package/dist/ui/components/card.js +74 -0
  60. package/dist/ui/components/checkbox.d.ts +2 -0
  61. package/dist/ui/components/checkbox.js +27 -0
  62. package/dist/ui/components/command-block.d.ts +24 -0
  63. package/dist/ui/components/command-block.js +56 -0
  64. package/dist/ui/components/confirm-dialog.d.ts +13 -0
  65. package/dist/ui/components/confirm-dialog.js +113 -0
  66. package/dist/ui/components/cursor.d.ts +3 -0
  67. package/dist/ui/components/cursor.js +97 -0
  68. package/dist/ui/components/dialog.d.ts +15 -0
  69. package/dist/ui/components/dialog.js +171 -0
  70. package/dist/ui/components/dropdown-menu.d.ts +12 -0
  71. package/dist/ui/components/dropdown-menu.js +102 -0
  72. package/dist/ui/components/fit-text/fit-text.css +42 -0
  73. package/dist/ui/components/fit-text/index.d.ts +9 -0
  74. package/dist/ui/components/fit-text/index.js +25 -0
  75. package/dist/ui/components/graphs/bar-chart.d.ts +12 -0
  76. package/dist/ui/components/graphs/bar-chart.js +129 -0
  77. package/dist/ui/components/graphs/index.d.ts +3 -0
  78. package/dist/ui/components/graphs/index.js +4 -0
  79. package/dist/ui/components/graphs/line-chart.d.ts +14 -0
  80. package/dist/ui/components/graphs/line-chart.js +175 -0
  81. package/dist/ui/components/graphs/utils.d.ts +52 -0
  82. package/dist/ui/components/graphs/utils.js +162 -0
  83. package/dist/ui/components/grid/grid.css +79 -0
  84. package/dist/ui/components/grid/index.d.ts +2 -0
  85. package/dist/ui/components/grid/index.js +17 -0
  86. package/dist/ui/components/hover-bg.d.ts +1 -0
  87. package/dist/ui/components/hover-bg.js +14 -0
  88. package/dist/ui/components/icons/arrow.d.ts +6 -0
  89. package/dist/ui/components/icons/arrow.js +44 -0
  90. package/dist/ui/components/icons/check.d.ts +2 -0
  91. package/dist/ui/components/icons/check.js +13 -0
  92. package/dist/ui/components/icons/chevron.d.ts +6 -0
  93. package/dist/ui/components/icons/chevron.js +51 -0
  94. package/dist/ui/components/icons/discord.d.ts +2 -0
  95. package/dist/ui/components/icons/discord.js +15 -0
  96. package/dist/ui/components/icons/eye.d.ts +2 -0
  97. package/dist/ui/components/icons/eye.js +8 -0
  98. package/dist/ui/components/icons/gear.d.ts +6 -0
  99. package/dist/ui/components/icons/gear.js +30 -0
  100. package/dist/ui/components/icons/github.d.ts +2 -0
  101. package/dist/ui/components/icons/github.js +15 -0
  102. package/dist/ui/components/icons/hamburger.d.ts +6 -0
  103. package/dist/ui/components/icons/hamburger.js +56 -0
  104. package/dist/ui/components/icons/heart.d.ts +2 -0
  105. package/dist/ui/components/icons/heart.js +11 -0
  106. package/dist/ui/components/icons/index.d.ts +12 -0
  107. package/dist/ui/components/icons/index.js +13 -0
  108. package/dist/ui/components/icons/link.d.ts +2 -0
  109. package/dist/ui/components/icons/link.js +13 -0
  110. package/dist/ui/components/icons/minus.d.ts +2 -0
  111. package/dist/ui/components/icons/minus.js +13 -0
  112. package/dist/ui/components/icons/search.d.ts +2 -0
  113. package/dist/ui/components/icons/search.js +33 -0
  114. package/dist/ui/components/image-distortion.d.ts +21 -0
  115. package/dist/ui/components/image-distortion.js +398 -0
  116. package/dist/ui/components/input.d.ts +1 -0
  117. package/dist/ui/components/input.js +21 -0
  118. package/dist/ui/components/label.d.ts +1 -0
  119. package/dist/ui/components/label.js +18 -0
  120. package/dist/ui/components/leva-client.d.ts +1 -0
  121. package/dist/ui/components/leva-client.js +12 -0
  122. package/dist/ui/components/list-item.d.ts +6 -0
  123. package/dist/ui/components/list-item.js +27 -0
  124. package/dist/ui/components/overlays/blend-modes.d.ts +1 -0
  125. package/dist/ui/components/overlays/blend-modes.js +14 -0
  126. package/dist/ui/components/overlays/glitch.d.ts +6 -0
  127. package/dist/ui/components/overlays/glitch.js +209 -0
  128. package/dist/ui/components/overlays/greys.d.ts +6 -0
  129. package/dist/ui/components/overlays/greys.js +339 -0
  130. package/dist/ui/components/overlays/index.d.ts +14 -0
  131. package/dist/ui/components/overlays/index.js +34 -0
  132. package/dist/ui/components/overlays/lens-layers.d.ts +14 -0
  133. package/dist/ui/components/overlays/lens-layers.js +95 -0
  134. package/dist/ui/components/overlays/lens.d.ts +44 -0
  135. package/dist/ui/components/overlays/lens.js +60 -0
  136. package/dist/ui/components/overlays/noise.d.ts +6 -0
  137. package/dist/ui/components/overlays/noise.js +136 -0
  138. package/dist/ui/components/overlays/vignette.d.ts +6 -0
  139. package/dist/ui/components/overlays/vignette.js +47 -0
  140. package/dist/ui/components/poster.d.ts +62 -0
  141. package/dist/ui/components/poster.js +256 -0
  142. package/dist/ui/components/progress.d.ts +9 -0
  143. package/dist/ui/components/progress.js +53 -0
  144. package/dist/ui/components/scene-canvas.d.ts +23 -0
  145. package/dist/ui/components/scene-canvas.js +179 -0
  146. package/dist/ui/components/scramble.d.ts +9 -0
  147. package/dist/ui/components/scramble.js +63 -0
  148. package/dist/ui/components/segmented.d.ts +20 -0
  149. package/dist/ui/components/segmented.js +51 -0
  150. package/dist/ui/components/select.d.ts +18 -0
  151. package/dist/ui/components/select.js +215 -0
  152. package/dist/ui/components/selection-switcher.d.ts +1 -0
  153. package/dist/ui/components/selection-switcher.js +34 -0
  154. package/dist/ui/components/separator.d.ts +5 -0
  155. package/dist/ui/components/separator.js +22 -0
  156. package/dist/ui/components/shader.d.ts +7 -0
  157. package/dist/ui/components/shader.js +60 -0
  158. package/dist/ui/components/socials.d.ts +20 -0
  159. package/dist/ui/components/socials.js +21 -0
  160. package/dist/ui/components/spinner.d.ts +20 -0
  161. package/dist/ui/components/spinner.js +38 -0
  162. package/dist/ui/components/stats.d.ts +16 -0
  163. package/dist/ui/components/stats.js +36 -0
  164. package/dist/ui/components/switch.d.ts +7 -0
  165. package/dist/ui/components/switch.js +37 -0
  166. package/dist/ui/components/tabs.d.ts +14 -0
  167. package/dist/ui/components/tabs.js +44 -0
  168. package/dist/ui/components/terminal-demo.d.ts +32 -0
  169. package/dist/ui/components/terminal-demo.js +125 -0
  170. package/dist/ui/components/theme-toggle.d.ts +6 -0
  171. package/dist/ui/components/theme-toggle.js +66 -0
  172. package/dist/ui/components/tier-card.d.ts +53 -0
  173. package/dist/ui/components/tier-card.js +146 -0
  174. package/dist/ui/components/toast.d.ts +8 -0
  175. package/dist/ui/components/toast.js +39 -0
  176. package/dist/ui/components/tv.d.ts +3 -0
  177. package/dist/ui/components/tv.js +239 -0
  178. package/dist/ui/components/typography/h1.d.ts +11 -0
  179. package/dist/ui/components/typography/h1.js +18 -0
  180. package/dist/ui/components/typography/h2.d.ts +11 -0
  181. package/dist/ui/components/typography/h2.js +18 -0
  182. package/dist/ui/components/typography/index.d.ts +15 -0
  183. package/dist/ui/components/typography/index.js +41 -0
  184. package/dist/ui/components/typography/legend.d.ts +6 -0
  185. package/dist/ui/components/typography/legend.js +20 -0
  186. package/dist/ui/components/typography/small.d.ts +2 -0
  187. package/dist/ui/components/typography/small.js +9 -0
  188. package/dist/ui/components/watchlist.d.ts +11 -0
  189. package/dist/ui/components/watchlist.js +80 -0
  190. package/dist/ui/fonts.css +63 -0
  191. package/dist/ui/footer.d.ts +20 -0
  192. package/dist/ui/footer.js +65 -0
  193. package/dist/ui/globals.css +395 -0
  194. package/dist/ui/header.d.ts +41 -0
  195. package/dist/ui/header.js +270 -0
  196. package/dist/ui/layout-wrapper.d.ts +1 -0
  197. package/dist/ui/layout-wrapper.js +7 -0
  198. package/dist/utils/color.d.ts +4 -0
  199. package/dist/utils/color.js +14 -0
  200. package/dist/utils/index.d.ts +15 -0
  201. package/dist/utils/index.js +48 -0
  202. package/dist/utils/poly.d.ts +8 -0
  203. package/dist/utils/poly.js +3 -0
  204. package/package.json +120 -0
  205. package/src/assets/filler-bg0.webp +0 -0
  206. package/src/assets.d.ts +38 -0
  207. package/src/fonts/Collapse-Bold.woff2 +0 -0
  208. package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
  209. package/src/fonts/Collapse-Italic.woff2 +0 -0
  210. package/src/fonts/Collapse-Light.woff2 +0 -0
  211. package/src/fonts/Collapse-LightItalic.woff2 +0 -0
  212. package/src/fonts/Collapse-Regular.woff2 +0 -0
  213. package/src/fonts/Collapse-Thin.woff2 +0 -0
  214. package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
  215. package/src/fonts/Mondwest-Regular.woff2 +0 -0
  216. package/src/fonts/Neuebit-Bold.woff2 +0 -0
  217. package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
  218. package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
  219. package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
  220. package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
  221. package/src/fonts.ts +6 -0
  222. package/src/hooks/use-below-breakpoint.ts +21 -0
  223. package/src/hooks/use-capped-frame.ts +18 -0
  224. package/src/hooks/use-confirm-delete.ts +43 -0
  225. package/src/hooks/use-css-var-dims.ts +39 -0
  226. package/src/hooks/use-gpu-tier.ts +190 -0
  227. package/src/hooks/use-render-loop.ts +121 -0
  228. package/src/hooks/use-smooth-controls.ts +318 -0
  229. package/src/hooks/use-toast.ts +29 -0
  230. package/src/index.ts +130 -0
  231. package/src/ui/basic-page.tsx +34 -0
  232. package/src/ui/build.css +4 -0
  233. package/src/ui/components/animated-count.stories.tsx +67 -0
  234. package/src/ui/components/animated-count.tsx +168 -0
  235. package/src/ui/components/ascii.stories.tsx +30 -0
  236. package/src/ui/components/ascii.tsx +110 -0
  237. package/src/ui/components/badge.stories.tsx +31 -0
  238. package/src/ui/components/badge.tsx +60 -0
  239. package/src/ui/components/badges/nous-girl.tsx +52 -0
  240. package/src/ui/components/blend-mode.stories.tsx +33 -0
  241. package/src/ui/components/blend-mode.tsx +129 -0
  242. package/src/ui/components/blink.stories.tsx +32 -0
  243. package/src/ui/components/blink.tsx +21 -0
  244. package/src/ui/components/bottom-sheet.stories.tsx +43 -0
  245. package/src/ui/components/bottom-sheet.tsx +227 -0
  246. package/src/ui/components/button.stories.tsx +68 -0
  247. package/src/ui/components/button.tsx +170 -0
  248. package/src/ui/components/card.stories.tsx +63 -0
  249. package/src/ui/components/card.tsx +85 -0
  250. package/src/ui/components/checkbox.stories.tsx +113 -0
  251. package/src/ui/components/checkbox.tsx +36 -0
  252. package/src/ui/components/command-block.stories.tsx +52 -0
  253. package/src/ui/components/command-block.tsx +86 -0
  254. package/src/ui/components/confirm-dialog.stories.tsx +91 -0
  255. package/src/ui/components/confirm-dialog.tsx +130 -0
  256. package/src/ui/components/cursor.tsx +115 -0
  257. package/src/ui/components/dialog.stories.tsx +169 -0
  258. package/src/ui/components/dialog.tsx +177 -0
  259. package/src/ui/components/dropdown-menu.stories.tsx +52 -0
  260. package/src/ui/components/dropdown-menu.tsx +117 -0
  261. package/src/ui/components/fit-text/fit-text.css +42 -0
  262. package/src/ui/components/fit-text/index.stories.tsx +33 -0
  263. package/src/ui/components/fit-text/index.tsx +45 -0
  264. package/src/ui/components/forms.stories.tsx +173 -0
  265. package/src/ui/components/graphs/bar-chart.tsx +153 -0
  266. package/src/ui/components/graphs/index.stories.tsx +64 -0
  267. package/src/ui/components/graphs/index.tsx +4 -0
  268. package/src/ui/components/graphs/line-chart.tsx +213 -0
  269. package/src/ui/components/graphs/utils.tsx +265 -0
  270. package/src/ui/components/grid/grid.css +79 -0
  271. package/src/ui/components/grid/index.tsx +19 -0
  272. package/src/ui/components/hover-bg.stories.tsx +29 -0
  273. package/src/ui/components/hover-bg.tsx +15 -0
  274. package/src/ui/components/icons/arrow.tsx +42 -0
  275. package/src/ui/components/icons/check.tsx +14 -0
  276. package/src/ui/components/icons/chevron.tsx +45 -0
  277. package/src/ui/components/icons/discord.tsx +16 -0
  278. package/src/ui/components/icons/eye.tsx +12 -0
  279. package/src/ui/components/icons/gear.tsx +51 -0
  280. package/src/ui/components/icons/github.tsx +16 -0
  281. package/src/ui/components/icons/hamburger.tsx +52 -0
  282. package/src/ui/components/icons/heart.tsx +12 -0
  283. package/src/ui/components/icons/index.ts +12 -0
  284. package/src/ui/components/icons/link.tsx +14 -0
  285. package/src/ui/components/icons/minus.tsx +14 -0
  286. package/src/ui/components/icons/search.tsx +28 -0
  287. package/src/ui/components/image-distortion.stories.tsx +120 -0
  288. package/src/ui/components/image-distortion.tsx +499 -0
  289. package/src/ui/components/input.stories.tsx +39 -0
  290. package/src/ui/components/input.tsx +20 -0
  291. package/src/ui/components/label.stories.tsx +26 -0
  292. package/src/ui/components/label.tsx +16 -0
  293. package/src/ui/components/leva-client.tsx +14 -0
  294. package/src/ui/components/list-item.stories.tsx +83 -0
  295. package/src/ui/components/list-item.tsx +37 -0
  296. package/src/ui/components/overlays/blend-modes.ts +13 -0
  297. package/src/ui/components/overlays/glitch.tsx +243 -0
  298. package/src/ui/components/overlays/greys.tsx +386 -0
  299. package/src/ui/components/overlays/index.tsx +47 -0
  300. package/src/ui/components/overlays/lens-layers.tsx +121 -0
  301. package/src/ui/components/overlays/lens.ts +91 -0
  302. package/src/ui/components/overlays/noise.tsx +174 -0
  303. package/src/ui/components/overlays/vignette.tsx +60 -0
  304. package/src/ui/components/poster.stories.tsx +513 -0
  305. package/src/ui/components/poster.tsx +411 -0
  306. package/src/ui/components/progress.stories.tsx +48 -0
  307. package/src/ui/components/progress.tsx +56 -0
  308. package/src/ui/components/scene-canvas.tsx +254 -0
  309. package/src/ui/components/scramble.stories.tsx +49 -0
  310. package/src/ui/components/scramble.tsx +95 -0
  311. package/src/ui/components/segmented.stories.tsx +101 -0
  312. package/src/ui/components/segmented.tsx +81 -0
  313. package/src/ui/components/select.stories.tsx +88 -0
  314. package/src/ui/components/select.tsx +267 -0
  315. package/src/ui/components/selection-switcher.tsx +44 -0
  316. package/src/ui/components/separator.stories.tsx +33 -0
  317. package/src/ui/components/separator.tsx +24 -0
  318. package/src/ui/components/shader.tsx +83 -0
  319. package/src/ui/components/socials.tsx +42 -0
  320. package/src/ui/components/spinner.stories.tsx +101 -0
  321. package/src/ui/components/spinner.tsx +60 -0
  322. package/src/ui/components/stats.stories.tsx +24 -0
  323. package/src/ui/components/stats.tsx +53 -0
  324. package/src/ui/components/switch.stories.tsx +77 -0
  325. package/src/ui/components/switch.tsx +48 -0
  326. package/src/ui/components/tabs.stories.tsx +101 -0
  327. package/src/ui/components/tabs.tsx +66 -0
  328. package/src/ui/components/terminal-demo.stories.tsx +67 -0
  329. package/src/ui/components/terminal-demo.tsx +189 -0
  330. package/src/ui/components/theme-toggle.stories.tsx +47 -0
  331. package/src/ui/components/theme-toggle.tsx +66 -0
  332. package/src/ui/components/tier-card.stories.tsx +217 -0
  333. package/src/ui/components/tier-card.tsx +190 -0
  334. package/src/ui/components/toast.stories.tsx +55 -0
  335. package/src/ui/components/toast.tsx +49 -0
  336. package/src/ui/components/tv.stories.tsx +37 -0
  337. package/src/ui/components/tv.tsx +257 -0
  338. package/src/ui/components/typography/h1.tsx +18 -0
  339. package/src/ui/components/typography/h2.tsx +18 -0
  340. package/src/ui/components/typography/index.tsx +54 -0
  341. package/src/ui/components/typography/legend.tsx +24 -0
  342. package/src/ui/components/typography/small.tsx +11 -0
  343. package/src/ui/components/watchlist.stories.tsx +33 -0
  344. package/src/ui/components/watchlist.tsx +105 -0
  345. package/src/ui/fonts.css +63 -0
  346. package/src/ui/footer.tsx +111 -0
  347. package/src/ui/globals.css +395 -0
  348. package/src/ui/header.tsx +398 -0
  349. package/src/ui/layout-wrapper.tsx +11 -0
  350. package/src/utils/color.ts +21 -0
  351. package/src/utils/index.ts +62 -0
  352. package/src/utils/poly.ts +26 -0
Binary file
Binary file
package/src/fonts.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** CSS variable names for the design language fonts. Set automatically by fonts.css. */
2
+ export const FONT_SANS = '--font-sans'
3
+ export const FONT_MONO = '--font-mono'
4
+ export const FONT_RULES_COMPRESSED = '--font-rules-compressed'
5
+ export const FONT_RULES_EXPANDED = '--font-rules-expanded'
6
+ export const FONT_MONDWEST = '--font-mondwest'
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ /** True when viewport width is strictly below `px`. */
6
+ export function useBelowBreakpoint(px: number) {
7
+ const query = `(max-width: ${px - 1}px)`
8
+ const [matches, setMatches] = useState(() =>
9
+ typeof window !== 'undefined' ? window.matchMedia(query).matches : false
10
+ )
11
+
12
+ useEffect(() => {
13
+ const mql = window.matchMedia(query)
14
+ const sync = () => setMatches(mql.matches)
15
+ sync()
16
+ mql.addEventListener('change', sync)
17
+ return () => mql.removeEventListener('change', sync)
18
+ }, [query])
19
+
20
+ return matches
21
+ }
@@ -0,0 +1,18 @@
1
+ 'use client'
2
+
3
+ import { useFrame, useThree } from '@react-three/fiber'
4
+ import type { RenderCallback } from '@react-three/fiber'
5
+ import { useRef } from 'react'
6
+
7
+ export function useCappedFrame(cb: RenderCallback, max?: number) {
8
+ const last = useRef(performance.now())
9
+ const { size } = useThree()
10
+ const interval = 1e3 / (max ?? (size.width < 1024 ? 60 : 120))
11
+
12
+ useFrame((st, delta) => {
13
+ if (performance.now() - last.current > interval) {
14
+ last.current = performance.now()
15
+ cb(st, delta)
16
+ }
17
+ })
18
+ }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useState } from 'react'
4
+
5
+ export function useConfirmDelete<TId>({
6
+ onDelete
7
+ }: {
8
+ onDelete: (id: TId) => Promise<void>
9
+ }) {
10
+ const [pendingId, setPendingId] = useState<TId | null>(null)
11
+ const [isDeleting, setIsDeleting] = useState(false)
12
+
13
+ const requestDelete = useCallback((id: TId) => {
14
+ setPendingId(id)
15
+ }, [])
16
+
17
+ const cancel = useCallback(() => {
18
+ if (!isDeleting) setPendingId(null)
19
+ }, [isDeleting])
20
+
21
+ const confirm = useCallback(async () => {
22
+ if (pendingId === null) return
23
+ const id = pendingId
24
+ setIsDeleting(true)
25
+ try {
26
+ await onDelete(id)
27
+ setPendingId(null)
28
+ } catch {
29
+ // Dialog stays open; caller can surface errors in onDelete
30
+ } finally {
31
+ setIsDeleting(false)
32
+ }
33
+ }, [pendingId, onDelete])
34
+
35
+ return {
36
+ cancel,
37
+ confirm,
38
+ isDeleting,
39
+ isOpen: pendingId !== null,
40
+ pendingId,
41
+ requestDelete
42
+ } as const
43
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ export function useCssVarDims(
6
+ name: string,
7
+ ref: React.RefObject<HTMLElement | null>
8
+ ) {
9
+ useEffect(() => {
10
+ if (!ref.current) {
11
+ return
12
+ }
13
+
14
+ const update = (width: number, height: number) => {
15
+ document.documentElement.style.setProperty(
16
+ `--${name}-width`,
17
+ `${width}px`
18
+ )
19
+
20
+ document.documentElement.style.setProperty(
21
+ `--${name}-height`,
22
+ `${height}px`
23
+ )
24
+ }
25
+
26
+ const { height, width } = ref.current.getBoundingClientRect()
27
+ update(width, height)
28
+
29
+ const ro = new ResizeObserver(entries => {
30
+ for (const entry of entries) {
31
+ update(entry.contentRect.width, entry.contentRect.height)
32
+ }
33
+ })
34
+
35
+ ro.observe(ref.current)
36
+
37
+ return () => ro.disconnect()
38
+ }, [name, ref])
39
+ }
@@ -0,0 +1,190 @@
1
+ 'use client'
2
+
3
+ import { useStore } from '@nanostores/react'
4
+ import { atom } from 'nanostores'
5
+
6
+ /**
7
+ * Tiers:
8
+ * 0 — no WebGL / software renderer / prefers-reduced-motion / WebGL ctx
9
+ * creation failed / detection not yet run
10
+ * 1 — low-end GPU (integrated, mobile, or failed perf benchmark)
11
+ * 2 — capable GPU (discrete / high-end integrated)
12
+ *
13
+ * The atom starts **pessimistic** (`0`) and the probe is scheduled to run
14
+ * *after the first paint* (see `scheduleDetection` below), never synchronously
15
+ * at module-evaluation time.
16
+ *
17
+ * Why pessimistic + deferred:
18
+ * - Every consumer gates its `<canvas>` / `THREE.WebGLRenderer` on `tier > 0`
19
+ * (or `=== 2`). Starting at `0` means a consumer reading `$gpuTier` during
20
+ * its first render never attempts to create a renderer before detection has
21
+ * run, so the `THREE.WebGLRenderer: Error creating WebGL context` crash on
22
+ * hardware where context creation fails cannot happen — the component
23
+ * renders its fallback and upgrades once detection resolves to a capable
24
+ * tier. (A previous version made the probe synchronous-at-module-load to
25
+ * dodge this crash; the pessimistic default removes the crash without that
26
+ * cost.)
27
+ * - Probing WebGL is *expensive* on software renderers: creating a context
28
+ * under SwiftShader / llvmpipe can block the main thread for hundreds of
29
+ * milliseconds. Running it synchronously at module load stalled first paint
30
+ * and produced a visible boot-time flash in apps that merely import this
31
+ * hook (e.g. the Hermes dashboard backdrop). Deferring past first paint
32
+ * keeps boot smooth; the tier just upgrades a frame or two later.
33
+ * - For SSR the server keeps the default `0` and the client's first render
34
+ * also reads `0`, so there is no hydration mismatch.
35
+ */
36
+ export const $gpuTier = atom<GpuTier>(0)
37
+
38
+ const SOFTWARE_PATTERNS =
39
+ /swiftshader|llvmpipe|softpipe|software|microsoft basic/i
40
+
41
+ const LOW_END_PATTERNS =
42
+ /intel.*hd|intel.*uhd|intel.*iris|mali|adreno\s?[1-5]|powervr|apple gpu/i
43
+
44
+ let detected = false
45
+
46
+ function detectGpuTier() {
47
+ if (detected || typeof window === 'undefined') {
48
+ return
49
+ }
50
+
51
+ detected = true
52
+
53
+ // The atom already holds 0; the early returns below simply leave it there.
54
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
55
+ return
56
+ }
57
+
58
+ let gl: null | WebGLRenderingContext = null
59
+
60
+ try {
61
+ const canvas = document.createElement('canvas')
62
+ gl = (canvas.getContext('webgl') ||
63
+ canvas.getContext('experimental-webgl')) as null | WebGLRenderingContext
64
+ } catch {
65
+ // Some sandboxed / hardened contexts throw on getContext rather than
66
+ // returning null (e.g. certain corporate browser policies). Treat as
67
+ // "no WebGL available".
68
+ return
69
+ }
70
+
71
+ if (!gl) {
72
+ return
73
+ }
74
+
75
+ const ext = gl.getExtension('WEBGL_debug_renderer_info')
76
+ const renderer = String(
77
+ ext
78
+ ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)
79
+ : gl.getParameter(gl.RENDERER)
80
+ )
81
+
82
+ if (SOFTWARE_PATTERNS.test(renderer)) {
83
+ gl.getExtension('WEBGL_lose_context')?.loseContext()
84
+
85
+ return
86
+ }
87
+
88
+ if (LOW_END_PATTERNS.test(renderer)) {
89
+ $gpuTier.set(1)
90
+ gl.getExtension('WEBGL_lose_context')?.loseContext()
91
+
92
+ return
93
+ }
94
+
95
+ $gpuTier.set(2)
96
+
97
+ runBenchmark(gl)
98
+ .then(fps => $gpuTier.set(fps < 30 ? 1 : 2))
99
+ .catch(() => $gpuTier.set(1))
100
+ .finally(() => gl?.getExtension('WEBGL_lose_context')?.loseContext())
101
+ }
102
+
103
+ /**
104
+ * Run detection once, *after* the first paint so the (potentially expensive)
105
+ * WebGL probe never blocks initial render. Prefer `requestIdleCallback` so the
106
+ * probe yields to rendering and input; fall back to a double
107
+ * `requestAnimationFrame` (guarantees at least one painted frame) and finally
108
+ * `setTimeout` where neither exists.
109
+ */
110
+ function scheduleDetection() {
111
+ if (typeof window === 'undefined') {
112
+ return
113
+ }
114
+
115
+ if (typeof window.requestIdleCallback === 'function') {
116
+ window.requestIdleCallback(() => detectGpuTier(), { timeout: 1000 })
117
+ } else if (typeof window.requestAnimationFrame === 'function') {
118
+ window.requestAnimationFrame(() =>
119
+ window.requestAnimationFrame(() => detectGpuTier())
120
+ )
121
+ } else {
122
+ setTimeout(() => detectGpuTier(), 0)
123
+ }
124
+ }
125
+
126
+ scheduleDetection()
127
+
128
+ function runBenchmark(gl: WebGLRenderingContext): Promise<number> {
129
+ return new Promise(resolve => {
130
+ const vs = gl.createShader(gl.VERTEX_SHADER)!
131
+ const fs = gl.createShader(gl.FRAGMENT_SHADER)!
132
+ gl.shaderSource(
133
+ vs,
134
+ 'attribute vec2 a;void main(){gl_Position=vec4(a,0,1);}'
135
+ )
136
+ gl.shaderSource(
137
+ fs,
138
+ 'precision highp float;uniform float t;void main(){float v=0.;for(int i=0;i<64;i++)v+=sin(float(i)*t*.01);gl_FragColor=vec4(v*.001);}'
139
+ )
140
+ gl.compileShader(vs)
141
+ gl.compileShader(fs)
142
+
143
+ const prog = gl.createProgram()!
144
+ gl.attachShader(prog, vs)
145
+ gl.attachShader(prog, fs)
146
+ gl.linkProgram(prog)
147
+ gl.useProgram(prog)
148
+
149
+ const buf = gl.createBuffer()
150
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf)
151
+ gl.bufferData(
152
+ gl.ARRAY_BUFFER,
153
+ new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
154
+ gl.STATIC_DRAW
155
+ )
156
+ const a = gl.getAttribLocation(prog, 'a')
157
+ gl.enableVertexAttribArray(a)
158
+ gl.vertexAttribPointer(a, 2, gl.FLOAT, false, 0, 0)
159
+
160
+ const uT = gl.getUniformLocation(prog, 't')
161
+ let frames = 0
162
+ const start = performance.now()
163
+
164
+ const tick = () => {
165
+ gl.uniform1f(uT, frames)
166
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
167
+ gl.finish()
168
+ frames++
169
+
170
+ if (performance.now() - start < 200) {
171
+ requestAnimationFrame(tick)
172
+ } else {
173
+ const elapsed = performance.now() - start
174
+ gl.deleteProgram(prog)
175
+ gl.deleteShader(vs)
176
+ gl.deleteShader(fs)
177
+ gl.deleteBuffer(buf)
178
+ resolve((frames / elapsed) * 1000)
179
+ }
180
+ }
181
+
182
+ requestAnimationFrame(tick)
183
+ })
184
+ }
185
+
186
+ export function useGpuTier() {
187
+ return useStore($gpuTier)
188
+ }
189
+
190
+ type GpuTier = 0 | 1 | 2
@@ -0,0 +1,121 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Visibility- and intersection-aware render-loop helper for the WebGL
5
+ * overlays.
6
+ *
7
+ * The overlays were previously running fragment shaders at 60fps for the
8
+ * entire lifetime of the page — including when the tab was hidden, the
9
+ * canvas had been scrolled out of view, or the user had been idle for
10
+ * hours. On retina laptops the compositor cost of mix-blend-mode on a
11
+ * full-viewport canvas plus continuous WebGL rasterisation is enough to
12
+ * keep the GPU hot indefinitely, which is what manifests as "fans go
13
+ * crazy after 2 hours of idle".
14
+ *
15
+ * `runRenderLoop` wraps a frame callback so that it:
16
+ *
17
+ * 1. Pauses entirely when `document.hidden` is true (background tab,
18
+ * minimised window, screen locked).
19
+ * 2. Pauses when the canvas's bounding rect is offscreen (we tell
20
+ * `IntersectionObserver` to look at the canvas itself).
21
+ * 3. Optionally caps the frame rate via a min-interval — the previous
22
+ * `gpuTier === 1 ? setTimeout(loop, 100) : raf` trick is preserved
23
+ * and extended so even tier-2 GPUs cap at e.g. 30fps for overlays
24
+ * that don't need 60.
25
+ *
26
+ * The callback receives the *delta* time in seconds since the last call
27
+ * (so `uTime` advances correctly across pauses without ever skipping
28
+ * forward by hours).
29
+ */
30
+
31
+ interface RunRenderLoopOptions {
32
+ /** Element to observe with IntersectionObserver. When fully out of
33
+ * view, the loop pauses. Pass the canvas element itself. */
34
+ el: Element
35
+ /** Min ms between frames. 0 = no cap (uses requestAnimationFrame).
36
+ * Anything > 0 uses setTimeout-driven scheduling. */
37
+ minIntervalMs?: number
38
+ /** Frame callback. Receives the elapsed seconds since the previous
39
+ * *executed* frame (not since the previous scheduled frame), so
40
+ * uniforms keyed off this value will not jump after a long pause. */
41
+ onFrame: (deltaSeconds: number) => void
42
+ }
43
+
44
+ export function runRenderLoop({
45
+ el,
46
+ minIntervalMs = 0,
47
+ onFrame
48
+ }: RunRenderLoopOptions) {
49
+ let running = true
50
+ let visible = !document.hidden
51
+ let inView = true
52
+ let last = performance.now()
53
+ let raf = 0
54
+ let timer: ReturnType<typeof setTimeout> | undefined
55
+
56
+ const onVisibility = () => {
57
+ visible = !document.hidden
58
+
59
+ // When we come back from a hidden tab, reset the clock so the next
60
+ // frame's delta is ~one frame, not "hours since I was hidden".
61
+ if (visible) {
62
+ last = performance.now()
63
+ schedule()
64
+ }
65
+ }
66
+
67
+ const io = new IntersectionObserver(
68
+ entries => {
69
+ const wasInView = inView
70
+ inView = entries.some(e => e.isIntersecting)
71
+
72
+ if (!wasInView && inView) {
73
+ last = performance.now()
74
+ schedule()
75
+ }
76
+ },
77
+ { threshold: 0 }
78
+ )
79
+
80
+ io.observe(el)
81
+ document.addEventListener('visibilitychange', onVisibility)
82
+
83
+ const tick = () => {
84
+ if (!running) return
85
+
86
+ if (!visible || !inView) {
87
+ // Don't reschedule — we'll be re-kicked by visibilitychange or IO.
88
+ return
89
+ }
90
+
91
+ const now = performance.now()
92
+ const delta = (now - last) / 1000
93
+ last = now
94
+
95
+ onFrame(delta)
96
+ schedule()
97
+ }
98
+
99
+ function schedule() {
100
+ if (!running || !visible || !inView) return
101
+
102
+ if (minIntervalMs > 0) {
103
+ timer = setTimeout(tick, minIntervalMs)
104
+ } else {
105
+ raf = requestAnimationFrame(tick)
106
+ }
107
+ }
108
+
109
+ schedule()
110
+
111
+ return () => {
112
+ running = false
113
+ io.disconnect()
114
+ document.removeEventListener('visibilitychange', onVisibility)
115
+ cancelAnimationFrame(raf)
116
+
117
+ if (timer !== undefined) {
118
+ clearTimeout(timer)
119
+ }
120
+ }
121
+ }