@hraness/design-kit 0.31.1

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 (345) hide show
  1. package/ARTICLE_COPY.md +208 -0
  2. package/HERO_FIELDS.md +55 -0
  3. package/ICONS.md +56 -0
  4. package/LANTERN_MATERIAL.md +60 -0
  5. package/LICENSE +21 -0
  6. package/MARKETING_COPY.md +49 -0
  7. package/MARKETING_PRESET.md +108 -0
  8. package/PALETTES.md +90 -0
  9. package/PAPER_THEME.md +128 -0
  10. package/README.md +745 -0
  11. package/STATUS_PAGES.md +67 -0
  12. package/STYLE.md +245 -0
  13. package/WRITING.md +72 -0
  14. package/dist/browser/index.js +2345 -0
  15. package/dist/chunk-44djxk16.js +6872 -0
  16. package/dist/chunk-5gtx3pza.js +9 -0
  17. package/dist/chunk-77391vmq.js +454 -0
  18. package/dist/chunk-7b1586eb.js +1209 -0
  19. package/dist/chunk-8834fh4n.js +6 -0
  20. package/dist/chunk-9t8xyqte.js +870 -0
  21. package/dist/chunk-cejpzyfh.js +446 -0
  22. package/dist/chunk-eh71jz57.js +429 -0
  23. package/dist/chunk-evb02bc1.js +516 -0
  24. package/dist/chunk-h4k7yv6x.js +368 -0
  25. package/dist/chunk-he8eznb1.js +760 -0
  26. package/dist/chunk-mr1vdcjq.js +404 -0
  27. package/dist/chunk-wzvdn8ey.js +116 -0
  28. package/dist/fonts/nebula-sans/social-fonts.generated.js +53 -0
  29. package/dist/icons.js +48 -0
  30. package/dist/index.js +237 -0
  31. package/dist/launch.js +41 -0
  32. package/dist/mockups/client.js +422 -0
  33. package/dist/mockups/index.js +1850 -0
  34. package/dist/portfolio.js +1549 -0
  35. package/dist/provider-marks.js +19 -0
  36. package/dist/react/charts.js +11 -0
  37. package/dist/react/hero-backdrop.js +8 -0
  38. package/dist/react/index.js +7252 -0
  39. package/dist/react/platform-install.js +10 -0
  40. package/dist/react/server.js +137 -0
  41. package/dist/stylex-manifest.json +1 -0
  42. package/dist/stylex.css +5392 -0
  43. package/dist/syntax-highlighting.js +15 -0
  44. package/dist/testing.js +250 -0
  45. package/package.json +377 -0
  46. package/portfolio-inventory.json +31 -0
  47. package/scripts/check-lantern-material-snapshot.d.mts +26 -0
  48. package/scripts/check-lantern-material-snapshot.mjs +126 -0
  49. package/scripts/check-marketing-snapshot.d.mts +12 -0
  50. package/scripts/check-marketing-snapshot.mjs +58 -0
  51. package/scripts/lantern-material-snapshot.ts +101 -0
  52. package/scripts/marketing-textures.ts +48 -0
  53. package/scripts/paper-theme-snapshot.ts +97 -0
  54. package/scripts/product-marketing-snapshot.ts +149 -0
  55. package/src/appearance-menu.css +266 -0
  56. package/src/appearance.ts +32 -0
  57. package/src/article-html.ts +269 -0
  58. package/src/article.ts +686 -0
  59. package/src/browser/appearance-menu.ts +604 -0
  60. package/src/browser/artifact-share.ts +164 -0
  61. package/src/browser/design-palette.ts +197 -0
  62. package/src/browser/foil.ts +124 -0
  63. package/src/browser/hero-light.ts +11 -0
  64. package/src/browser/index.ts +29 -0
  65. package/src/browser/status-field.ts +432 -0
  66. package/src/browser/status-page.ts +100 -0
  67. package/src/browser/sticky-offset.ts +82 -0
  68. package/src/browser/theme-color-sync.ts +213 -0
  69. package/src/charts.css +7 -0
  70. package/src/compiler-components.css +34 -0
  71. package/src/compiler-foundation.css +18 -0
  72. package/src/compiler-palettes.css +7 -0
  73. package/src/compiler-tokens.css +342 -0
  74. package/src/components.css +3 -0
  75. package/src/design-gallery.css +431 -0
  76. package/src/effects.css +14 -0
  77. package/src/fonts/geist-mono/GeistMono[wght].woff2 +0 -0
  78. package/src/fonts/geist-mono/OFL.txt +93 -0
  79. package/src/fonts/geist-mono/PROVENANCE.md +12 -0
  80. package/src/fonts/instrument-serif/OFL.txt +93 -0
  81. package/src/fonts/instrument-serif/UPSTREAM.md +14 -0
  82. package/src/fonts/instrument-serif/instrument-serif-latin-400.woff2 +0 -0
  83. package/src/fonts/nebula-sans/LICENSE.txt +96 -0
  84. package/src/fonts/nebula-sans/NebulaSans-Black.woff2 +0 -0
  85. package/src/fonts/nebula-sans/NebulaSans-BlackItalic.woff2 +0 -0
  86. package/src/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
  87. package/src/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
  88. package/src/fonts/nebula-sans/NebulaSans-BoldItalic.woff2 +0 -0
  89. package/src/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
  90. package/src/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  91. package/src/fonts/nebula-sans/NebulaSans-BookItalic.woff2 +0 -0
  92. package/src/fonts/nebula-sans/NebulaSans-Light.woff2 +0 -0
  93. package/src/fonts/nebula-sans/NebulaSans-LightItalic.woff2 +0 -0
  94. package/src/fonts/nebula-sans/NebulaSans-Medium.woff2 +0 -0
  95. package/src/fonts/nebula-sans/NebulaSans-MediumItalic.woff2 +0 -0
  96. package/src/fonts/nebula-sans/NebulaSans-Semibold.woff2 +0 -0
  97. package/src/fonts/nebula-sans/NebulaSans-SemiboldItalic.woff2 +0 -0
  98. package/src/fonts/nebula-sans/PROVENANCE.md +14 -0
  99. package/src/fonts/nebula-sans/social-fonts.generated.ts +3873 -0
  100. package/src/fonts.css +106 -0
  101. package/src/icons/act60/act60.svg +5 -0
  102. package/src/icons/act60/application.svg +9 -0
  103. package/src/icons/act60/chapter-compare.svg +3 -0
  104. package/src/icons/act60/decree-residence.svg +6 -0
  105. package/src/icons/act60/estimate.svg +7 -0
  106. package/src/icons/act60/export-services.svg +4 -0
  107. package/src/icons/act60/investor.svg +13 -0
  108. package/src/icons/act60/presence.svg +7 -0
  109. package/src/icons/aicharts/aicharts.svg +6 -0
  110. package/src/icons/aicharts/cost-compare.svg +4 -0
  111. package/src/icons/aicharts/task-audio.svg +7 -0
  112. package/src/icons/aicharts/task-coding.svg +6 -0
  113. package/src/icons/aicharts/task-images.svg +3 -0
  114. package/src/icons/aicharts/task-reasoning.svg +9 -0
  115. package/src/icons/aicharts/task-research.svg +5 -0
  116. package/src/icons/aicharts/task-video.svg +4 -0
  117. package/src/icons/manifest.json +1371 -0
  118. package/src/icons/platonik/conversation.svg +3 -0
  119. package/src/icons/platonik/creature.svg +7 -0
  120. package/src/icons/platonik/economy.svg +4 -0
  121. package/src/icons/platonik/keep-light.svg +6 -0
  122. package/src/icons/platonik/platonik.svg +7 -0
  123. package/src/icons/platonik/spark-route.svg +15 -0
  124. package/src/icons/receipts/act60-retry.receipt.json +888 -0
  125. package/src/icons/receipts/act60.receipt.json +1366 -0
  126. package/src/icons/receipts/aicharts.receipt.json +1546 -0
  127. package/src/icons/receipts/platonik.receipt.json +1011 -0
  128. package/src/icons/receipts/roughday.receipt.json +770 -0
  129. package/src/icons/receipts/shared.receipt.json +1296 -0
  130. package/src/icons/receipts/slopcamera.receipt.json +1495 -0
  131. package/src/icons/receipts/soundfish-retry.receipt.json +460 -0
  132. package/src/icons/receipts/soundfish.receipt.json +1709 -0
  133. package/src/icons/receipts/sponge.receipt.json +1743 -0
  134. package/src/icons/receipts/stripe-history-retry.receipt.json +145 -0
  135. package/src/icons/receipts/stripe-history.receipt.json +1202 -0
  136. package/src/icons/receipts/wordcell.receipt.json +1924 -0
  137. package/src/icons/roughday/finance.svg +7 -0
  138. package/src/icons/roughday/roughday.svg +6 -0
  139. package/src/icons/roughday/technology.svg +4 -0
  140. package/src/icons/roughday/world.svg +4 -0
  141. package/src/icons/sets/act60.json +63 -0
  142. package/src/icons/sets/aicharts.json +63 -0
  143. package/src/icons/sets/platonik.json +39 -0
  144. package/src/icons/sets/roughday.json +25 -0
  145. package/src/icons/sets/shared.json +12 -0
  146. package/src/icons/sets/slopcamera.json +47 -0
  147. package/src/icons/sets/soundfish.json +43 -0
  148. package/src/icons/sets/sponge.json +51 -0
  149. package/src/icons/sets/stripe-history.json +39 -0
  150. package/src/icons/sets/wordcell.json +51 -0
  151. package/src/icons/shared/agent-skill.svg +4 -0
  152. package/src/icons/shared/cli.svg +5 -0
  153. package/src/icons/shared/privacy.svg +3 -0
  154. package/src/icons/shared/research.svg +7 -0
  155. package/src/icons/shared/sdk.svg +3 -0
  156. package/src/icons/slopcamera/diagram.svg +3 -0
  157. package/src/icons/slopcamera/direct-scene.svg +5 -0
  158. package/src/icons/slopcamera/edit-video.svg +17 -0
  159. package/src/icons/slopcamera/mcp.svg +3 -0
  160. package/src/icons/slopcamera/native-world.svg +3 -0
  161. package/src/icons/slopcamera/slopcamera.svg +11 -0
  162. package/src/icons/soundfish/agent-ui.svg +7 -0
  163. package/src/icons/soundfish/collaborate.svg +5 -0
  164. package/src/icons/soundfish/compose.svg +3 -0
  165. package/src/icons/soundfish/hear.svg +6 -0
  166. package/src/icons/soundfish/human-ui.svg +6 -0
  167. package/src/icons/soundfish/share.svg +3 -0
  168. package/src/icons/soundfish/soundfish.svg +6 -0
  169. package/src/icons/sponge/api.svg +3 -0
  170. package/src/icons/sponge/documents.svg +3 -0
  171. package/src/icons/sponge/library.svg +3 -0
  172. package/src/icons/sponge/one-question.svg +5 -0
  173. package/src/icons/sponge/people-direct.svg +5 -0
  174. package/src/icons/sponge/read.svg +3 -0
  175. package/src/icons/sponge/save.svg +4 -0
  176. package/src/icons/sponge/sponge.svg +7 -0
  177. package/src/icons/stripe-history/company-history.svg +3 -0
  178. package/src/icons/stripe-history/evidence.svg +4 -0
  179. package/src/icons/stripe-history/independence.svg +7 -0
  180. package/src/icons/stripe-history/publications.svg +11 -0
  181. package/src/icons/stripe-history/sources.svg +6 -0
  182. package/src/icons/stripe-history/stripe-history.svg +3 -0
  183. package/src/icons/wordcell/backlinks.svg +6 -0
  184. package/src/icons/wordcell/capture.svg +6 -0
  185. package/src/icons/wordcell/git-provenance.svg +4 -0
  186. package/src/icons/wordcell/kb.svg +4 -0
  187. package/src/icons/wordcell/markdown.svg +4 -0
  188. package/src/icons/wordcell/scopes.svg +3 -0
  189. package/src/icons/wordcell/search.svg +7 -0
  190. package/src/icons.generated.ts +734 -0
  191. package/src/icons.ts +51 -0
  192. package/src/index.ts +275 -0
  193. package/src/lantern-material.css +213 -0
  194. package/src/launch.ts +624 -0
  195. package/src/marketing-assets/UPSTREAM.md +18 -0
  196. package/src/marketing-assets/cells.svg +1 -0
  197. package/src/marketing-assets/grain.svg +1 -0
  198. package/src/marketing-forced-colors.css +12 -0
  199. package/src/mockups/client.tsx +415 -0
  200. package/src/mockups/core.tsx +318 -0
  201. package/src/mockups/frames.tsx +368 -0
  202. package/src/mockups/index.ts +14 -0
  203. package/src/mockups/surfaces.tsx +681 -0
  204. package/src/mockups.css +2607 -0
  205. package/src/palette-appearance.ts +45 -0
  206. package/src/palette-bridge.css +124 -0
  207. package/src/palette-color.ts +41 -0
  208. package/src/palette-system.css +721 -0
  209. package/src/palette-themes.ts +23 -0
  210. package/src/palette-tokens.stylex.ts +519 -0
  211. package/src/palettes.css +3 -0
  212. package/src/palettes.ts +161 -0
  213. package/src/paper-theme.css +244 -0
  214. package/src/plain-publication.css +1241 -0
  215. package/src/plain-site.css +397 -0
  216. package/src/platforms.ts +151 -0
  217. package/src/portfolio.generated.json +1595 -0
  218. package/src/portfolio.generated.ts +1596 -0
  219. package/src/portfolio.ts +226 -0
  220. package/src/product-marketing-foundation.css +433 -0
  221. package/src/product-marketing-preset.css +261 -0
  222. package/src/product-marketing.css +2186 -0
  223. package/src/provider-marks.generated.ts +112 -0
  224. package/src/provider-marks.ts +143 -0
  225. package/src/react/animated-rail-stage.stylex.ts +15 -0
  226. package/src/react/animated-rail-stage.tsx +51 -0
  227. package/src/react/app-shell.stylex.ts +104 -0
  228. package/src/react/app-shell.tsx +121 -0
  229. package/src/react/article.tsx +602 -0
  230. package/src/react/aurora-dots-background.tsx +34 -0
  231. package/src/react/charts.stylex.ts +563 -0
  232. package/src/react/charts.tsx +610 -0
  233. package/src/react/chat.stylex.ts +34 -0
  234. package/src/react/chat.tsx +155 -0
  235. package/src/react/design-gallery.tsx +943 -0
  236. package/src/react/design-palette.stylex.ts +124 -0
  237. package/src/react/design-palette.tsx +196 -0
  238. package/src/react/design-theme-context.tsx +41 -0
  239. package/src/react/effects.stylex.ts +325 -0
  240. package/src/react/fader.stylex.ts +71 -0
  241. package/src/react/fader.tsx +151 -0
  242. package/src/react/foil-card-math.ts +100 -0
  243. package/src/react/foil-card-surface.stylex.ts +326 -0
  244. package/src/react/foil-card-surface.tsx +742 -0
  245. package/src/react/foil-mark.tsx +35 -0
  246. package/src/react/foil.stylex.ts +163 -0
  247. package/src/react/haptics.ts +228 -0
  248. package/src/react/hero-backdrop.tsx +22 -0
  249. package/src/react/index.ts +38 -0
  250. package/src/react/keyboard-shortcuts.ts +228 -0
  251. package/src/react/lantern-material-gallery.tsx +166 -0
  252. package/src/react/lantern-material.stylex.ts +39 -0
  253. package/src/react/launch-beats.tsx +73 -0
  254. package/src/react/navigation-rail.stylex.ts +130 -0
  255. package/src/react/navigation-rail.tsx +228 -0
  256. package/src/react/particle-halo.tsx +115 -0
  257. package/src/react/phaser-dots.tsx +501 -0
  258. package/src/react/platform-icons.tsx +94 -0
  259. package/src/react/platform-install.stylex.ts +385 -0
  260. package/src/react/platform-install.tsx +389 -0
  261. package/src/react/playback-transport.stylex.ts +14 -0
  262. package/src/react/playback-transport.tsx +112 -0
  263. package/src/react/procedural-backdrop.tsx +192 -0
  264. package/src/react/procedural-recipe.ts +323 -0
  265. package/src/react/product-marketing.stylex.ts +4469 -0
  266. package/src/react/product-marketing.tsx +1611 -0
  267. package/src/react/production-data-preview-notice.stylex.ts +40 -0
  268. package/src/react/production-data-preview-notice.tsx +34 -0
  269. package/src/react/provider-mark.stylex.ts +132 -0
  270. package/src/react/provider-mark.tsx +120 -0
  271. package/src/react/relative-time.tsx +117 -0
  272. package/src/react/route-state.stylex.ts +39 -0
  273. package/src/react/route-state.tsx +350 -0
  274. package/src/react/server.ts +11 -0
  275. package/src/react/social-kit-panel.tsx +103 -0
  276. package/src/react/sticky-offset.tsx +33 -0
  277. package/src/react/surfaces.stylex.ts +179 -0
  278. package/src/react/surfaces.tsx +330 -0
  279. package/src/react/syntax-code.tsx +33 -0
  280. package/src/react/theme-color-sync.ts +1 -0
  281. package/src/react/theme-resolution.ts +11 -0
  282. package/src/react/theme.stylex.ts +276 -0
  283. package/src/react/theme.tsx +402 -0
  284. package/src/reading.css +72 -0
  285. package/src/relative-time.ts +185 -0
  286. package/src/reset.css +2 -0
  287. package/src/site-shell.css +16 -0
  288. package/src/status-page-html.ts +63 -0
  289. package/src/status-page.css +276 -0
  290. package/src/status-page.ts +323 -0
  291. package/src/styles.css +21 -0
  292. package/src/syntax-code-lexers.ts +303 -0
  293. package/src/syntax-highlighting.css +78 -0
  294. package/src/syntax-highlighting.ts +477 -0
  295. package/src/syntax-tokens.ts +33 -0
  296. package/src/testing.ts +307 -0
  297. package/src/tokens.css +3 -0
  298. package/src/typography.css +86 -0
  299. package/vendor/evilcharts/LICENSE +21 -0
  300. package/vendor/evilcharts/UPSTREAM.md +14 -0
  301. package/vendor/platform-marks/LICENSE +30 -0
  302. package/vendor/platform-marks/UPSTREAM.md +15 -0
  303. package/vendor/platform-marks/apple.svg +1 -0
  304. package/vendor/platform-marks/linux.svg +1 -0
  305. package/vendor/provider-marks/CRUSH-LICENSE.md +134 -0
  306. package/vendor/provider-marks/LICENSE +21 -0
  307. package/vendor/provider-marks/UPSTREAM.md +27 -0
  308. package/vendor/provider-marks/aider.svg +3 -0
  309. package/vendor/provider-marks/crush-heartbit.svg +8 -0
  310. package/vendor/provider-marks/lobehub/alibabacloud-color.svg +1 -0
  311. package/vendor/provider-marks/lobehub/alibabacloud.svg +1 -0
  312. package/vendor/provider-marks/lobehub/amp-color.svg +1 -0
  313. package/vendor/provider-marks/lobehub/amp.svg +1 -0
  314. package/vendor/provider-marks/lobehub/anthropic.svg +1 -0
  315. package/vendor/provider-marks/lobehub/claude-color.svg +1 -0
  316. package/vendor/provider-marks/lobehub/claude.svg +1 -0
  317. package/vendor/provider-marks/lobehub/claudecode-color.svg +1 -0
  318. package/vendor/provider-marks/lobehub/claudecode.svg +1 -0
  319. package/vendor/provider-marks/lobehub/codex-color.svg +1 -0
  320. package/vendor/provider-marks/lobehub/codex.svg +1 -0
  321. package/vendor/provider-marks/lobehub/cursor.svg +1 -0
  322. package/vendor/provider-marks/lobehub/deepseek-color.svg +1 -0
  323. package/vendor/provider-marks/lobehub/deepseek.svg +1 -0
  324. package/vendor/provider-marks/lobehub/devin-color.svg +1 -0
  325. package/vendor/provider-marks/lobehub/devin.svg +1 -0
  326. package/vendor/provider-marks/lobehub/gemini-color.svg +1 -0
  327. package/vendor/provider-marks/lobehub/gemini.svg +1 -0
  328. package/vendor/provider-marks/lobehub/geminicli-color.svg +1 -0
  329. package/vendor/provider-marks/lobehub/geminicli.svg +1 -0
  330. package/vendor/provider-marks/lobehub/goose.svg +1 -0
  331. package/vendor/provider-marks/lobehub/meta-color.svg +1 -0
  332. package/vendor/provider-marks/lobehub/meta.svg +1 -0
  333. package/vendor/provider-marks/lobehub/mistral-color.svg +1 -0
  334. package/vendor/provider-marks/lobehub/mistral.svg +1 -0
  335. package/vendor/provider-marks/lobehub/moonshot.svg +1 -0
  336. package/vendor/provider-marks/lobehub/nvidia-color.svg +1 -0
  337. package/vendor/provider-marks/lobehub/nvidia.svg +1 -0
  338. package/vendor/provider-marks/lobehub/openai.svg +1 -0
  339. package/vendor/provider-marks/lobehub/opencode.svg +1 -0
  340. package/vendor/provider-marks/lobehub/perplexity-color.svg +1 -0
  341. package/vendor/provider-marks/lobehub/perplexity.svg +1 -0
  342. package/vendor/provider-marks/lobehub/qwen-color.svg +1 -0
  343. package/vendor/provider-marks/lobehub/qwen.svg +1 -0
  344. package/vendor/provider-marks/lobehub/xai.svg +1 -0
  345. package/vendor/provider-marks/lobehub/zai.svg +1 -0
@@ -0,0 +1,501 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, type CSSProperties, type HTMLAttributes } from "react";
4
+ import { cn } from "@hraness/ui";
5
+ import * as stylex from "@stylexjs/stylex";
6
+
7
+ import { effectsStyles } from "./effects.stylex.js";
8
+
9
+ export interface PhaserDotsProps extends Omit<
10
+ HTMLAttributes<HTMLDivElement>,
11
+ | "aria-hidden"
12
+ | "children"
13
+ | "contentEditable"
14
+ | "inert"
15
+ | "role"
16
+ | "suppressContentEditableWarning"
17
+ | "tabIndex"
18
+ > {
19
+ fadeDirection?: "top" | "bottom" | "left" | "right" | "none";
20
+ mouseGlow?: boolean;
21
+ /** Override the static dot layer's color/opacity classes. */
22
+ dotClassName?: string;
23
+ /** Override the mouse-trail canvas color class. */
24
+ trailClassName?: string;
25
+ }
26
+
27
+ const DOT_PATTERN = "hraness-design-phaser-dots__static";
28
+
29
+ const INERT_PROPS = { inert: true } as unknown as HTMLAttributes<HTMLDivElement>;
30
+
31
+ export function PhaserDots({
32
+ className,
33
+ fadeDirection = "none",
34
+ mouseGlow = false,
35
+ dotClassName,
36
+ trailClassName,
37
+ style,
38
+ ...props
39
+ }: PhaserDotsProps) {
40
+ const containerRef = useRef<HTMLDivElement>(null);
41
+ const canvasRef = useRef<HTMLCanvasElement>(null);
42
+
43
+ useEffect(() => {
44
+ if (!mouseGlow) return;
45
+ // Motion accessibility: the pointer-trail is a JS/canvas animation the
46
+ // global prefers-reduced-motion CSS block can't reach, so gate it here.
47
+ // The static dot layer still renders; only the animated trail is skipped.
48
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
49
+ const container = containerRef.current;
50
+ const canvas = canvasRef.current;
51
+ if (!container || !canvas) return;
52
+
53
+ const ctx = canvas.getContext("2d", { alpha: true });
54
+ if (!ctx) return;
55
+
56
+ const DOT_STEP = 6;
57
+ const PRIMARY_TRAIL_ALPHA = 0.48;
58
+ const PRIMARY_TRAIL_DECAY_MS = 3800;
59
+ const SECONDARY_TRAIL_ALPHA = 0.34;
60
+ const SECONDARY_TRAIL_DECAY_MS = 10500;
61
+ const MAX_TRAIL_POINTS = 160;
62
+ const POINT_SPACING = DOT_STEP;
63
+ const MAX_INTERPOLATION_STEPS = 2;
64
+ const MAX_RENDERED_PRIMARY_TRAIL_POINTS = 24;
65
+ const MAX_RENDERED_SECONDARY_TRAIL_POINTS = 16;
66
+ const CLICK_TRAIL_STAMP = [
67
+ { dx: 0, dy: 0 },
68
+ { dx: DOT_STEP, dy: 0 },
69
+ { dx: -DOT_STEP, dy: 0 },
70
+ { dx: 0, dy: DOT_STEP },
71
+ { dx: 0, dy: -DOT_STEP },
72
+ ] as const;
73
+
74
+ const HOVER_BRUSH_RADIUS = 54;
75
+ const HOVER_BRUSH_ALPHA = 0.6;
76
+ const HOVER_BRUSH_DECAY_MS = 900;
77
+ const MAX_HOVER_POINTS = 6;
78
+
79
+ const INTERACTION_PAD = HOVER_BRUSH_RADIUS;
80
+ const RECT_CACHE_MS = 180;
81
+ const FRAME_MIN_INTERVAL_MS = 33;
82
+ const COLOR_CACHE_MS = 1000;
83
+ const DOT_KEY_STRIDE = 8192;
84
+ const CANVAS_DOT_SIZE = 2;
85
+ const CANVAS_DOT_HALF = CANVAS_DOT_SIZE / 2;
86
+
87
+ const trail: { x: number; y: number; t: number }[] = [];
88
+ const hoverBrush: { x: number; y: number; t: number }[] = [];
89
+ const dotAlpha = new Map<number, number>();
90
+ const hoverOffsets: Array<{ dx: number; dy: number; weight: number }> = [];
91
+
92
+ let lastPoint: { x: number; y: number } | null = null;
93
+ let lastHoverPoint: { x: number; y: number } | null = null;
94
+ let pendingMove: { x: number; y: number } | null = null;
95
+ let pendingDown: { x: number; y: number } | null = null;
96
+ let cachedRect = container.getBoundingClientRect();
97
+ let lastRectRead = 0;
98
+ let lastFrameTime = 0;
99
+ let lastColorRead = 0;
100
+ let drawColor = getComputedStyle(canvas).color;
101
+ let raf = 0;
102
+ let dpr = Math.max(1, window.devicePixelRatio || 1);
103
+ let canvasWidth = Math.max(1, Math.round(cachedRect.width));
104
+ let canvasHeight = Math.max(1, Math.round(cachedRect.height));
105
+
106
+ const snapToGrid = (value: number) =>
107
+ Math.round(value / DOT_STEP) * DOT_STEP;
108
+
109
+ const resizeCanvas = () => {
110
+ const rect = container.getBoundingClientRect();
111
+ cachedRect = rect;
112
+ const width = Math.max(1, Math.round(rect.width));
113
+ const height = Math.max(1, Math.round(rect.height));
114
+ const nextDpr = Math.max(1, window.devicePixelRatio || 1);
115
+ if (
116
+ width === canvasWidth &&
117
+ height === canvasHeight &&
118
+ nextDpr === dpr &&
119
+ canvas.width === Math.round(width * nextDpr) &&
120
+ canvas.height === Math.round(height * nextDpr)
121
+ ) {
122
+ return;
123
+ }
124
+ canvasWidth = width;
125
+ canvasHeight = height;
126
+ dpr = nextDpr;
127
+ canvas.width = Math.round(canvasWidth * dpr);
128
+ canvas.height = Math.round(canvasHeight * dpr);
129
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
130
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
131
+ };
132
+
133
+ for (
134
+ let dx = -HOVER_BRUSH_RADIUS;
135
+ dx <= HOVER_BRUSH_RADIUS;
136
+ dx += DOT_STEP
137
+ ) {
138
+ for (
139
+ let dy = -HOVER_BRUSH_RADIUS;
140
+ dy <= HOVER_BRUSH_RADIUS;
141
+ dy += DOT_STEP
142
+ ) {
143
+ const dist = Math.hypot(dx, dy);
144
+ if (dist > HOVER_BRUSH_RADIUS) continue;
145
+ hoverOffsets.push({
146
+ dx,
147
+ dy,
148
+ weight: 1 - dist / HOVER_BRUSH_RADIUS,
149
+ });
150
+ }
151
+ }
152
+
153
+ resizeCanvas();
154
+ const resizeObserver = new ResizeObserver(() => {
155
+ resizeCanvas();
156
+ });
157
+ resizeObserver.observe(container);
158
+
159
+ const getContainerRect = (now: number) => {
160
+ if (now - lastRectRead > RECT_CACHE_MS) {
161
+ cachedRect = container.getBoundingClientRect();
162
+ lastRectRead = now;
163
+ }
164
+ return cachedRect;
165
+ };
166
+
167
+ const getLocalPoint = (clientX: number, clientY: number, now: number) => {
168
+ const rect = getContainerRect(now);
169
+ const nearContainer =
170
+ clientX >= rect.left - INTERACTION_PAD &&
171
+ clientX <= rect.right + INTERACTION_PAD &&
172
+ clientY >= rect.top - INTERACTION_PAD &&
173
+ clientY <= rect.bottom + INTERACTION_PAD;
174
+ if (!nearContainer) return null;
175
+
176
+ return {
177
+ x: snapToGrid(clientX - rect.left),
178
+ y: snapToGrid(clientY - rect.top),
179
+ };
180
+ };
181
+
182
+ const putDot = (x: number, y: number, alpha: number) => {
183
+ if (alpha <= 0) return;
184
+ const sx = snapToGrid(x);
185
+ const sy = snapToGrid(y);
186
+ if (sx < 0 || sy < 0 || sx > canvasWidth || sy > canvasHeight) return;
187
+ const key = sx + sy * DOT_KEY_STRIDE;
188
+ const prev = dotAlpha.get(key);
189
+ if (prev === undefined || alpha > prev) {
190
+ dotAlpha.set(key, alpha);
191
+ }
192
+ };
193
+
194
+ const pushHoverBrushPoint = (x: number, y: number) => {
195
+ if (lastHoverPoint?.x === x && lastHoverPoint.y === y) return;
196
+ hoverBrush.push({ x, y, t: performance.now() });
197
+ if (hoverBrush.length > MAX_HOVER_POINTS) {
198
+ hoverBrush.splice(0, hoverBrush.length - MAX_HOVER_POINTS);
199
+ }
200
+ lastHoverPoint = { x, y };
201
+ };
202
+
203
+ const pushTrailPoint = (x: number, y: number) => {
204
+ const now = performance.now();
205
+ if (!lastPoint) {
206
+ trail.push({ x, y, t: now });
207
+ } else {
208
+ const dx = x - lastPoint.x;
209
+ const dy = y - lastPoint.y;
210
+ const dist = Math.hypot(dx, dy);
211
+ if (dist < POINT_SPACING) return;
212
+ const steps = Math.min(
213
+ MAX_INTERPOLATION_STEPS,
214
+ Math.max(1, Math.floor(dist / POINT_SPACING)),
215
+ );
216
+ for (let i = 1; i <= steps; i += 1) {
217
+ const ratio = i / steps;
218
+ const px = snapToGrid(lastPoint.x + dx * ratio);
219
+ const py = snapToGrid(lastPoint.y + dy * ratio);
220
+ if (
221
+ trail[trail.length - 1]?.x === px &&
222
+ trail[trail.length - 1]?.y === py
223
+ ) {
224
+ continue;
225
+ }
226
+ trail.push({ x: px, y: py, t: now });
227
+ }
228
+ }
229
+ if (trail.length > MAX_TRAIL_POINTS) {
230
+ trail.splice(0, trail.length - MAX_TRAIL_POINTS);
231
+ }
232
+ lastPoint = { x, y };
233
+ };
234
+
235
+ const stampClickTrail = (x: number, y: number, t: number) => {
236
+ for (const offset of CLICK_TRAIL_STAMP) {
237
+ trail.push({
238
+ x: snapToGrid(x + offset.dx),
239
+ y: snapToGrid(y + offset.dy),
240
+ t,
241
+ });
242
+ }
243
+ if (trail.length > MAX_TRAIL_POINTS) {
244
+ trail.splice(0, trail.length - MAX_TRAIL_POINTS);
245
+ }
246
+ };
247
+
248
+ const renderDots = (now: number) => {
249
+ dotAlpha.clear();
250
+
251
+ for (let i = hoverBrush.length - 1; i >= 0; i -= 1) {
252
+ const p = hoverBrush[i];
253
+ if (p === undefined) continue;
254
+ const baseAlpha =
255
+ HOVER_BRUSH_ALPHA *
256
+ Math.max(0, 1 - (now - p.t) / HOVER_BRUSH_DECAY_MS);
257
+ if (baseAlpha <= 0) continue;
258
+ for (let j = 0; j < hoverOffsets.length; j += 1) {
259
+ const off = hoverOffsets[j];
260
+ if (off === undefined) continue;
261
+ putDot(p.x + off.dx, p.y + off.dy, baseAlpha * off.weight);
262
+ }
263
+ }
264
+
265
+ let primaryCount = 0;
266
+ let secondaryCount = 0;
267
+ let sampledPrimaryCounter = 0;
268
+ let sampledSecondaryCounter = 0;
269
+ for (
270
+ let i = trail.length - 1;
271
+ i >= 0 &&
272
+ (primaryCount < MAX_RENDERED_PRIMARY_TRAIL_POINTS ||
273
+ secondaryCount < MAX_RENDERED_SECONDARY_TRAIL_POINTS);
274
+ i -= 1
275
+ ) {
276
+ const p = trail[i];
277
+ if (p === undefined) continue;
278
+ const agePrimary = (now - p.t) / PRIMARY_TRAIL_DECAY_MS;
279
+ const ageSecondary = (now - p.t) / SECONDARY_TRAIL_DECAY_MS;
280
+ if (ageSecondary > 1) continue;
281
+
282
+ if (
283
+ primaryCount < MAX_RENDERED_PRIMARY_TRAIL_POINTS &&
284
+ agePrimary <= 1
285
+ ) {
286
+ if (agePrimary > 0.35) {
287
+ const stride = agePrimary > 0.75 ? 4 : 2;
288
+ if (sampledPrimaryCounter % stride !== 0) {
289
+ sampledPrimaryCounter += 1;
290
+ } else {
291
+ putDot(
292
+ p.x,
293
+ p.y,
294
+ PRIMARY_TRAIL_ALPHA * Math.max(0, 1 - agePrimary),
295
+ );
296
+ primaryCount += 1;
297
+ sampledPrimaryCounter += 1;
298
+ }
299
+ } else {
300
+ putDot(p.x, p.y, PRIMARY_TRAIL_ALPHA * Math.max(0, 1 - agePrimary));
301
+ primaryCount += 1;
302
+ }
303
+ }
304
+
305
+ if (secondaryCount < MAX_RENDERED_SECONDARY_TRAIL_POINTS) {
306
+ if (ageSecondary > 0.3) {
307
+ const stride = ageSecondary > 0.75 ? 5 : 3;
308
+ if (sampledSecondaryCounter % stride !== 0) {
309
+ sampledSecondaryCounter += 1;
310
+ continue;
311
+ }
312
+ sampledSecondaryCounter += 1;
313
+ }
314
+ putDot(
315
+ p.x,
316
+ p.y,
317
+ SECONDARY_TRAIL_ALPHA * Math.max(0, 1 - ageSecondary),
318
+ );
319
+ secondaryCount += 1;
320
+ }
321
+ }
322
+
323
+ if (dotAlpha.size === 0) {
324
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
325
+ return;
326
+ }
327
+
328
+ if (now - lastColorRead > COLOR_CACHE_MS) {
329
+ drawColor = getComputedStyle(canvas).color;
330
+ lastColorRead = now;
331
+ }
332
+
333
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
334
+ ctx.fillStyle = drawColor;
335
+ for (const [key, alpha] of dotAlpha) {
336
+ const x = key % DOT_KEY_STRIDE;
337
+ const y = (key - x) / DOT_KEY_STRIDE;
338
+ ctx.globalAlpha = alpha;
339
+ ctx.fillRect(
340
+ x - CANVAS_DOT_HALF,
341
+ y - CANVAS_DOT_HALF,
342
+ CANVAS_DOT_SIZE,
343
+ CANVAS_DOT_SIZE,
344
+ );
345
+ }
346
+ ctx.globalAlpha = 1;
347
+ };
348
+
349
+ const tick = (frameTime: number) => {
350
+ if (
351
+ lastFrameTime !== 0 &&
352
+ frameTime - lastFrameTime < FRAME_MIN_INTERVAL_MS
353
+ ) {
354
+ raf = requestAnimationFrame(tick);
355
+ return;
356
+ }
357
+ lastFrameTime = frameTime;
358
+ const now = performance.now();
359
+
360
+ if (pendingDown) {
361
+ const point = getLocalPoint(pendingDown.x, pendingDown.y, now);
362
+ pendingDown = null;
363
+ if (point) {
364
+ pushHoverBrushPoint(point.x, point.y);
365
+ stampClickTrail(point.x, point.y, now);
366
+ pushTrailPoint(point.x, point.y);
367
+ }
368
+ }
369
+
370
+ if (pendingMove) {
371
+ const point = getLocalPoint(pendingMove.x, pendingMove.y, now);
372
+ pendingMove = null;
373
+ if (point) {
374
+ pushHoverBrushPoint(point.x, point.y);
375
+ pushTrailPoint(point.x, point.y);
376
+ } else {
377
+ lastPoint = null;
378
+ lastHoverPoint = null;
379
+ }
380
+ }
381
+
382
+ while (true) {
383
+ const oldest = hoverBrush[0];
384
+ if (oldest === undefined || now - oldest.t <= HOVER_BRUSH_DECAY_MS) break;
385
+ hoverBrush.shift();
386
+ }
387
+ while (true) {
388
+ const oldest = trail[0];
389
+ if (oldest === undefined || now - oldest.t <= SECONDARY_TRAIL_DECAY_MS) break;
390
+ trail.shift();
391
+ }
392
+
393
+ if (hoverBrush.length === 0 && trail.length === 0) {
394
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
395
+ raf = 0;
396
+ return;
397
+ }
398
+
399
+ renderDots(now);
400
+ raf = requestAnimationFrame(tick);
401
+ };
402
+
403
+ const handleMove = (e: PointerEvent) => {
404
+ pendingMove = { x: e.clientX, y: e.clientY };
405
+ if (!raf) raf = requestAnimationFrame(tick);
406
+ };
407
+
408
+ const handleDown = (e: PointerEvent) => {
409
+ if (e.pointerType === "mouse" && e.button !== 0) return;
410
+ pendingDown = { x: e.clientX, y: e.clientY };
411
+ if (!raf) raf = requestAnimationFrame(tick);
412
+ };
413
+
414
+ const handleUp = () => {
415
+ lastPoint = null;
416
+ };
417
+
418
+ const passive = { passive: true };
419
+ document.addEventListener("pointermove", handleMove, passive);
420
+ document.addEventListener("pointerdown", handleDown, passive);
421
+ document.addEventListener("pointerup", handleUp, passive);
422
+ document.addEventListener("pointercancel", handleUp, passive);
423
+
424
+ return () => {
425
+ document.removeEventListener("pointermove", handleMove);
426
+ document.removeEventListener("pointerdown", handleDown);
427
+ document.removeEventListener("pointerup", handleUp);
428
+ document.removeEventListener("pointercancel", handleUp);
429
+ resizeObserver.disconnect();
430
+ if (raf) cancelAnimationFrame(raf);
431
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
432
+ };
433
+ }, [mouseGlow]);
434
+
435
+ const maskGradient =
436
+ fadeDirection === "top"
437
+ ? "linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)"
438
+ : fadeDirection === "bottom"
439
+ ? "linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)"
440
+ : fadeDirection === "left"
441
+ ? "linear-gradient(to left, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)"
442
+ : fadeDirection === "right"
443
+ ? "linear-gradient(to right, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)"
444
+ : undefined;
445
+
446
+ const mergedStyle: CSSProperties | undefined = maskGradient
447
+ ? {
448
+ WebkitMaskImage: maskGradient,
449
+ maskImage: maskGradient,
450
+ ...style,
451
+ }
452
+ : style;
453
+ const rootPresentation = stylex.props(
454
+ effectsStyles.phaserSlot,
455
+ effectsStyles.phaserRoot,
456
+ );
457
+ const staticPresentation = stylex.props(
458
+ effectsStyles.phaserSlot,
459
+ effectsStyles.phaserStatic,
460
+ !dotClassName && effectsStyles.phaserStaticDefault,
461
+ );
462
+ const trailPresentation = stylex.props(
463
+ effectsStyles.phaserSlot,
464
+ effectsStyles.phaserTrail,
465
+ !trailClassName && effectsStyles.phaserTrailDefault,
466
+ );
467
+
468
+ return (
469
+ <div
470
+ {...props}
471
+ {...INERT_PROPS}
472
+ ref={containerRef}
473
+ role="presentation"
474
+ aria-hidden="true"
475
+ className={cn(
476
+ "hraness-design-phaser-dots",
477
+ rootPresentation.className,
478
+ className,
479
+ )}
480
+ style={mergedStyle}
481
+ >
482
+ <div
483
+ className={cn(
484
+ DOT_PATTERN,
485
+ staticPresentation.className,
486
+ dotClassName,
487
+ )}
488
+ />
489
+ {mouseGlow && (
490
+ <canvas
491
+ ref={canvasRef}
492
+ className={cn(
493
+ "hraness-design-phaser-dots__trail",
494
+ trailPresentation.className,
495
+ trailClassName,
496
+ )}
497
+ />
498
+ )}
499
+ </div>
500
+ );
501
+ }
@@ -0,0 +1,94 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { isPlatformId, platformLabel, platformMark, type PlatformId } from "../platforms.js";
4
+ import { platformInstallClassName } from "./platform-install.stylex.js";
5
+
6
+ export type PlatformIconSize = "sm" | "md" | "lg" | "inherit";
7
+
8
+ export interface PlatformIconProps {
9
+ readonly platform: PlatformId;
10
+ readonly className?: string;
11
+ /**
12
+ * Decorative by default: pair the icon with a visible platform name. Pass a
13
+ * label only when the icon stands alone; it then renders as a named image.
14
+ */
15
+ readonly label?: string;
16
+ /** `inherit` (the default) follows the surrounding font size; `sm`, `md`, and `lg` are 1, 1.25, and 1.5rem. */
17
+ readonly size?: PlatformIconSize;
18
+ }
19
+
20
+ const sizeParts = { inherit: undefined, lg: "iconLg", md: "iconMd", sm: "iconSm" } as const;
21
+
22
+ /**
23
+ * A monochrome platform mark drawn in `currentColor`: the Apple logo for
24
+ * macOS, Tux for Linux, the four-pane window for Windows, and a neutral
25
+ * terminal glyph for any other id.
26
+ */
27
+ export function PlatformIcon({ className, label, platform, size = "inherit" }: PlatformIconProps) {
28
+ if (!isPlatformId(platform)) throw new RangeError(`Platform ids are lowercase slugs; received ${JSON.stringify(platform)}.`);
29
+ if (!(size in sizeParts)) throw new RangeError(`Unknown platform icon size: ${String(size)}.`);
30
+ if (label !== undefined && label.trim() === "") throw new RangeError("A platform icon label must not be blank.");
31
+ const mark = platformMark(platform);
32
+ return (
33
+ <svg
34
+ aria-hidden={label === undefined ? true : undefined}
35
+ aria-label={label}
36
+ className={platformInstallClassName(["icon", sizeParts[size]], className)}
37
+ data-platform={platform}
38
+ fill="currentColor"
39
+ focusable="false"
40
+ role={label === undefined ? undefined : "img"}
41
+ viewBox={mark.viewBox}
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ >
44
+ <path d={mark.path} />
45
+ </svg>
46
+ );
47
+ }
48
+
49
+ export interface PlatformBadge {
50
+ readonly id: PlatformId;
51
+ /** Defaults to the known display name (`macOS`, `Linux`, `Windows`) or the id. */
52
+ readonly label?: string;
53
+ /** A short qualifier shown after the name, such as "Apple silicon". */
54
+ readonly note?: ReactNode;
55
+ }
56
+
57
+ export interface PlatformBadgesProps {
58
+ /** Platform ids or badge records, in display order. */
59
+ readonly platforms: readonly (PlatformId | PlatformBadge)[];
60
+ readonly className?: string;
61
+ /** The visible lead-in and list name. Defaults to "Runs on"; pass `null` to omit the visible text. */
62
+ readonly label?: string | null;
63
+ }
64
+
65
+ function toBadge(entry: PlatformId | PlatformBadge): PlatformBadge {
66
+ return typeof entry === "string" ? { id: entry } : entry;
67
+ }
68
+
69
+ /** A compact "Runs on" row: one icon and name per supported platform. */
70
+ export function PlatformBadges({ className, label = "Runs on", platforms }: PlatformBadgesProps) {
71
+ const badges = platforms.map(toBadge);
72
+ if (badges.length === 0) throw new RangeError("PlatformBadges needs at least one platform.");
73
+ const seen = new Set<string>();
74
+ for (const badge of badges) {
75
+ if (!isPlatformId(badge.id)) throw new RangeError(`Platform ids are lowercase slugs; received ${JSON.stringify(badge.id)}.`);
76
+ if (seen.has(badge.id)) throw new RangeError(`Duplicate platform id: ${badge.id}.`);
77
+ seen.add(badge.id);
78
+ }
79
+ const listName = label ?? "Supported platforms";
80
+ return (
81
+ <div className={platformInstallClassName(["badges"], className)} data-hraness-platform-badges="">
82
+ {label === null ? null : <span aria-hidden="true" className={platformInstallClassName(["badgesLabel"])}>{label}</span>}
83
+ <ul aria-label={listName} className={platformInstallClassName(["badgesList"])}>
84
+ {badges.map((badge) => (
85
+ <li className={platformInstallClassName(["badge"])} data-platform={badge.id} key={badge.id}>
86
+ <PlatformIcon platform={badge.id} />
87
+ <span>{badge.label ?? platformLabel(badge.id)}</span>
88
+ {badge.note === undefined ? null : <span className={platformInstallClassName(["badgeNote"])}>{badge.note}</span>}
89
+ </li>
90
+ ))}
91
+ </ul>
92
+ </div>
93
+ );
94
+ }