@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,602 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import {
3
+ ARTICLE_BYLINE_PREFIX,
4
+ ARTICLE_SOURCES_HEADING,
5
+ ARTICLE_TOC_LABEL,
6
+ articleProvenanceSentence,
7
+ assertArticleAuthor,
8
+ assertArticleCalloutTone,
9
+ assertArticleDates,
10
+ assertArticleHref,
11
+ assertArticleVideo,
12
+ formatArticleDate,
13
+ orderedArticleVideoSources,
14
+ type ArticleAuthor,
15
+ type ArticleCalloutTone,
16
+ type ArticleIndexItem,
17
+ type ArticleIsoDate,
18
+ type ArticleProvenanceRecord,
19
+ type ArticleSourceItem,
20
+ type ArticleTocItem,
21
+ type ArticleVideoRecord,
22
+ } from "../article.js";
23
+ import {
24
+ MarketingRelated,
25
+ type MarketingHeadingLevel,
26
+ type MarketingRelatedGroup,
27
+ type MarketingRelatedProduct,
28
+ } from "./product-marketing.js";
29
+
30
+ /*
31
+ * Server-safe article compositions. Every element uses the plain-publication
32
+ * BEM hooks, so the React components and the static HTML renderer in the
33
+ * framework-neutral root produce the same markup and share one stylesheet:
34
+ * plain-publication.css (also included in styles.css).
35
+ */
36
+
37
+ const ROOT_CLASS = "plain-site plain-publication plain-publication--embedded";
38
+
39
+ const HEADING_TAGS = { 1: "h1", 2: "h2", 3: "h3", 4: "h4", 5: "h5", 6: "h6" } as const;
40
+
41
+ function joinClasses(...values: readonly (string | undefined)[]): string {
42
+ return values.filter((value) => value !== undefined && value !== "").join(" ");
43
+ }
44
+
45
+ function ArticleDate({ label, value }: Readonly<{ label: string; value: ArticleIsoDate }>) {
46
+ return <>{label} <time dateTime={value}>{formatArticleDate(value)}</time></>;
47
+ }
48
+
49
+ function Separator() {
50
+ return <span aria-hidden="true"> · </span>;
51
+ }
52
+
53
+ /** An organization or person byline. The name links with rel="author" when `href` is given. */
54
+ export function ArticleByline({ author }: Readonly<{ author: ArticleAuthor }>) {
55
+ assertArticleAuthor(author);
56
+ return (
57
+ <span className="plain-publication__byline" data-author-kind={author.kind}>
58
+ {ARTICLE_BYLINE_PREFIX}{" "}
59
+ {author.href === undefined ? author.name : <a href={author.href} rel="author">{author.name}</a>}
60
+ </span>
61
+ );
62
+ }
63
+
64
+ /**
65
+ * The visible drafting and review note, for example "Drafted with AI from the
66
+ * source code and reviewed by Claude Opus 5.5 (claude-opus-5-5) editorial
67
+ * review." A null review states that no review has happened.
68
+ */
69
+ export function ArticleProvenance({ provenance }: Readonly<{ provenance: ArticleProvenanceRecord }>) {
70
+ const sentence = articleProvenanceSentence(provenance);
71
+ return (
72
+ <p
73
+ className="plain-publication__provenance"
74
+ data-drafting={provenance.drafting}
75
+ data-reviewer-type={provenance.review?.reviewerType ?? "none"}
76
+ >
77
+ {sentence}
78
+ </p>
79
+ );
80
+ }
81
+
82
+ export interface MarketingArticleProps {
83
+ readonly after?: ReactNode;
84
+ readonly author?: ArticleAuthor;
85
+ readonly children: ReactNode;
86
+ readonly className?: string;
87
+ /** One concrete claim, as a complete sentence. */
88
+ readonly dek?: string;
89
+ readonly eyebrow?: string;
90
+ readonly heading: string;
91
+ readonly headingId?: string;
92
+ readonly id?: string;
93
+ /** Required so every article decides its drafting note. Pass null only for writing with no AI involvement and no review record. */
94
+ readonly provenance: ArticleProvenanceRecord | null;
95
+ readonly published: ArticleIsoDate;
96
+ readonly toc?: readonly ArticleTocItem[];
97
+ readonly tocLabel?: string;
98
+ readonly updated?: ArticleIsoDate;
99
+ }
100
+
101
+ /**
102
+ * Long-form article: eyebrow, title, dek, byline with published and updated
103
+ * dates, provenance note, an optional contents list that becomes a side
104
+ * column on wide screens, the body, and an optional footer slot for sources
105
+ * and related products.
106
+ */
107
+ export function MarketingArticle({
108
+ after,
109
+ author,
110
+ children,
111
+ className,
112
+ dek,
113
+ eyebrow,
114
+ heading,
115
+ headingId = "article-title",
116
+ id,
117
+ provenance,
118
+ published,
119
+ toc,
120
+ tocLabel = ARTICLE_TOC_LABEL,
121
+ updated,
122
+ }: Readonly<MarketingArticleProps>) {
123
+ assertArticleDates(updated === undefined ? { published } : { published, updated });
124
+ const tocItems = toc ?? [];
125
+ for (const item of tocItems) {
126
+ if (!item.href.startsWith("#") || item.href.length < 2) throw new RangeError("Contents links must point to a heading in this article.");
127
+ }
128
+ const tocId = `${headingId}-contents`;
129
+ return (
130
+ <article
131
+ aria-labelledby={headingId}
132
+ className={joinClasses(ROOT_CLASS, "plain-publication__article", className)}
133
+ data-hraness-article=""
134
+ data-toc={tocItems.length > 0 ? "aside" : "none"}
135
+ id={id}
136
+ >
137
+ <header className="plain-publication__article-header">
138
+ {eyebrow === undefined || eyebrow === "" ? null : <p className="plain-publication__eyebrow">{eyebrow}</p>}
139
+ <h1 id={headingId}>{heading}</h1>
140
+ {dek === undefined || dek === "" ? null : <p className="plain-publication__article-dek">{dek}</p>}
141
+ <p className="plain-publication__article-meta">
142
+ {author === undefined ? null : <><ArticleByline author={author} /><Separator /></>}
143
+ <ArticleDate label="Published" value={published} />
144
+ {updated === undefined ? null : <><Separator /><ArticleDate label="Updated" value={updated} /></>}
145
+ </p>
146
+ {provenance === null ? null : <ArticleProvenance provenance={provenance} />}
147
+ </header>
148
+ <div className="plain-publication__article-layout">
149
+ {tocItems.length === 0 ? null : (
150
+ <nav aria-labelledby={tocId} className="plain-publication__toc">
151
+ <p id={tocId}>{tocLabel}</p>
152
+ <ol>
153
+ {tocItems.map((item) => <li key={item.href}><a href={item.href}>{item.label}</a></li>)}
154
+ </ol>
155
+ </nav>
156
+ )}
157
+ <div className="plain-publication__article-body">{children}</div>
158
+ </div>
159
+ {after === undefined || after === null ? null : <footer className="plain-publication__article-footer">{after}</footer>}
160
+ </article>
161
+ );
162
+ }
163
+
164
+ /** Numbered primary sources, each with the date its claim was last checked. */
165
+ export function ArticleSources({
166
+ heading = ARTICLE_SOURCES_HEADING,
167
+ headingId = "article-sources",
168
+ sources,
169
+ }: Readonly<{
170
+ heading?: string;
171
+ headingId?: string;
172
+ sources: readonly ArticleSourceItem[];
173
+ }>) {
174
+ if (sources.length === 0) return null;
175
+ for (const source of sources) assertArticleHref(source.href);
176
+ return (
177
+ <section aria-labelledby={headingId} className="plain-publication__sources">
178
+ <h2 id={headingId}>{heading}</h2>
179
+ <ol>
180
+ {sources.map((source) => (
181
+ <li key={`${source.href}-${source.title}`}>
182
+ <a href={source.href}>{source.title}</a>
183
+ <span>
184
+ {source.publisher === undefined || source.publisher === "" ? null : <>{source.publisher}<Separator /></>}
185
+ <ArticleDate label="Checked" value={source.checkedOn} />
186
+ </span>
187
+ </li>
188
+ ))}
189
+ </ol>
190
+ </section>
191
+ );
192
+ }
193
+
194
+ /** A note, limit, or warning inside the article body. String children become one paragraph. */
195
+ export function ArticleCallout({
196
+ children,
197
+ label,
198
+ tone = "note",
199
+ }: Readonly<{
200
+ children: ReactNode;
201
+ label?: string;
202
+ tone?: ArticleCalloutTone;
203
+ }>) {
204
+ assertArticleCalloutTone(tone);
205
+ return (
206
+ <div className="plain-publication__callout" data-tone={tone} role="note">
207
+ {label === undefined || label === "" ? null : <strong>{label}</strong>}
208
+ {typeof children === "string" ? <p>{children}</p> : children}
209
+ </div>
210
+ );
211
+ }
212
+
213
+ type ArticleRelatedBody =
214
+ | Readonly<{ groups: readonly MarketingRelatedGroup[]; items?: undefined }>
215
+ | Readonly<{ groups?: undefined; items: readonly MarketingRelatedProduct[] }>;
216
+
217
+ /**
218
+ * Sibling products at the end of an article, rendered by MarketingRelated.
219
+ * Pass items from the registered relations for this article only.
220
+ */
221
+ export function ArticleRelatedProducts({
222
+ heading = "Related products",
223
+ headingId = "article-related-products",
224
+ headingLevel = 2,
225
+ label,
226
+ summary,
227
+ ...body
228
+ }: Readonly<{
229
+ heading?: string;
230
+ headingId?: string;
231
+ headingLevel?: MarketingHeadingLevel;
232
+ label?: string;
233
+ summary?: string;
234
+ }> & ArticleRelatedBody) {
235
+ const optional = {
236
+ ...(label === undefined ? {} : { label }),
237
+ ...(summary === undefined ? {} : { summary }),
238
+ };
239
+ return (
240
+ <div className="plain-publication__related-products">
241
+ {body.groups === undefined
242
+ ? <MarketingRelated heading={heading} headingId={headingId} headingLevel={headingLevel} items={body.items} {...optional} />
243
+ : <MarketingRelated groups={body.groups} heading={heading} headingId={headingId} headingLevel={headingLevel} {...optional} />}
244
+ </div>
245
+ );
246
+ }
247
+
248
+ /** A list of posts, newest first as given, each with its title, dek, and dates. */
249
+ export function ArticleIndex({
250
+ className,
251
+ heading,
252
+ headingId,
253
+ headingLevel = 2,
254
+ id,
255
+ items,
256
+ summary,
257
+ }: Readonly<{
258
+ className?: string;
259
+ heading: string;
260
+ headingId: string;
261
+ headingLevel?: 1 | 2 | 3 | 4 | 5;
262
+ id?: string;
263
+ items: readonly ArticleIndexItem[];
264
+ summary?: string;
265
+ }>) {
266
+ if (![1, 2, 3, 4, 5].includes(headingLevel)) throw new RangeError("Article index heading level must be 1 to 5.");
267
+ const hrefs = new Set<string>();
268
+ for (const item of items) {
269
+ assertArticleHref(item.href);
270
+ assertArticleDates(item.updated === undefined ? { published: item.published } : { published: item.published, updated: item.updated });
271
+ if (hrefs.has(item.href)) throw new RangeError(`Article index lists ${item.href} more than once.`);
272
+ hrefs.add(item.href);
273
+ }
274
+ const HeadingTag = HEADING_TAGS[headingLevel];
275
+ const EntryTag = HEADING_TAGS[(headingLevel + 1) as 2 | 3 | 4 | 5 | 6];
276
+ return (
277
+ <section
278
+ aria-labelledby={headingId}
279
+ className={joinClasses(ROOT_CLASS, "plain-publication__list", className)}
280
+ data-hraness-article-index=""
281
+ id={id}
282
+ >
283
+ <div className="plain-publication__section-heading">
284
+ <HeadingTag id={headingId}>{heading}</HeadingTag>
285
+ {summary === undefined || summary === "" ? null : <p>{summary}</p>}
286
+ </div>
287
+ <div className="plain-publication__article-list">
288
+ {items.map((item) => (
289
+ <article className="plain-publication__entry" key={item.href}>
290
+ {item.eyebrow === undefined || item.eyebrow === "" ? null : <p className="plain-publication__entry-label">{item.eyebrow}</p>}
291
+ <EntryTag className="plain-publication__entry-title"><a href={item.href}>{item.title}</a></EntryTag>
292
+ <p className="plain-publication__entry-dek">{item.dek}</p>
293
+ <p className="plain-publication__entry-meta">
294
+ <ArticleDate label="Published" value={item.published} />
295
+ {item.updated === undefined ? null : <><Separator /><ArticleDate label="Updated" value={item.updated} /></>}
296
+ </p>
297
+ </article>
298
+ ))}
299
+ </div>
300
+ </section>
301
+ );
302
+ }
303
+
304
+ /** What an article visual is. The label opens the caption so readers can tell a mockup from a screenshot. */
305
+ export const articleFigureKinds = ["illustration", "screenshot", "recording", "diagram", "chart", "table"] as const;
306
+ export type ArticleFigureKind = (typeof articleFigureKinds)[number];
307
+
308
+ const FIGURE_LABELS: Readonly<Record<ArticleFigureKind, string>> = {
309
+ illustration: "Illustration",
310
+ screenshot: "Screenshot",
311
+ recording: "Recording",
312
+ diagram: "Diagram",
313
+ chart: "Chart",
314
+ table: "Table",
315
+ };
316
+
317
+ function FigureCaption({ caption, credit, kind }: Readonly<{ caption: ReactNode; credit?: ReactNode; kind: ArticleFigureKind }>) {
318
+ return (
319
+ <figcaption className="plain-publication__figure-caption">
320
+ <span className="plain-publication__figure-label">{FIGURE_LABELS[kind]}.</span>{" "}
321
+ {caption}
322
+ {credit === undefined || credit === null || credit === "" ? null : <small className="plain-publication__figure-credit">{credit}</small>}
323
+ </figcaption>
324
+ );
325
+ }
326
+
327
+ /**
328
+ * A labelled figure in the article body. The caption opens with the kind,
329
+ * such as "Illustration." for a code-built mockup, so an invented screen is
330
+ * never mistaken for a screenshot. `width="wide"` lets the figure extend past
331
+ * the text measure on wide screens.
332
+ */
333
+ export function ArticleFigure({
334
+ caption,
335
+ children,
336
+ className,
337
+ credit,
338
+ id,
339
+ kind,
340
+ width = "text",
341
+ }: Readonly<{
342
+ caption: ReactNode;
343
+ children: ReactNode;
344
+ className?: string;
345
+ credit?: ReactNode;
346
+ id?: string;
347
+ kind: ArticleFigureKind;
348
+ width?: "text" | "wide";
349
+ }>) {
350
+ if (!articleFigureKinds.includes(kind)) throw new RangeError(`Unknown article figure kind: ${String(kind)}.`);
351
+ if (caption === undefined || caption === null || caption === "") throw new RangeError("Article figures need a caption.");
352
+ return (
353
+ <figure className={joinClasses("plain-publication__figure", className)} data-figure-kind={kind} data-width={width} id={id}>
354
+ <div className="plain-publication__figure-body">{children}</div>
355
+ <FigureCaption caption={caption} credit={credit} kind={kind} />
356
+ </figure>
357
+ );
358
+ }
359
+
360
+ /**
361
+ * A captioned article recording with WebM and MP4 sources, a poster, and a
362
+ * captions track. It never autoplays. Pair it with `articleVideoJsonLd()` in
363
+ * the page's JSON-LD.
364
+ */
365
+ export function ArticleVideo({
366
+ caption,
367
+ className,
368
+ credit,
369
+ id,
370
+ video,
371
+ width = "text",
372
+ }: Readonly<{
373
+ caption: ReactNode;
374
+ className?: string;
375
+ credit?: ReactNode;
376
+ id?: string;
377
+ video: ArticleVideoRecord;
378
+ width?: "text" | "wide";
379
+ }>) {
380
+ assertArticleVideo(video);
381
+ return (
382
+ <ArticleFigure caption={caption} className={joinClasses("plain-publication__video", className)} credit={credit} kind="recording" width={width} {...(id === undefined ? {} : { id })}>
383
+ <video
384
+ aria-label={video.name}
385
+ controls
386
+ height={video.height}
387
+ playsInline
388
+ poster={video.poster}
389
+ preload="metadata"
390
+ width={video.width}
391
+ >
392
+ {orderedArticleVideoSources(video).map((source) => <source key={source.type} src={source.src} type={source.type} />)}
393
+ <track default kind="captions" label="Captions" src={video.captions} srcLang={video.captionsLanguage ?? "en"} />
394
+ </video>
395
+ </ArticleFigure>
396
+ );
397
+ }
398
+
399
+ export interface ArticleTableColumn {
400
+ readonly label: string;
401
+ /** Right-aligns the column with tabular figures. */
402
+ readonly numeric?: boolean;
403
+ }
404
+
405
+ /**
406
+ * A captioned data table. The first column heads each row, and the table
407
+ * scrolls inside the measure on narrow screens instead of widening the page.
408
+ */
409
+ export function ArticleTable({
410
+ caption,
411
+ className,
412
+ columns,
413
+ id,
414
+ note,
415
+ rows,
416
+ }: Readonly<{
417
+ caption: string;
418
+ className?: string;
419
+ columns: readonly ArticleTableColumn[];
420
+ id?: string;
421
+ /** Scope or source of the numbers, under the table. */
422
+ note?: ReactNode;
423
+ rows: readonly (readonly ReactNode[])[];
424
+ }>) {
425
+ if (columns.length === 0) throw new RangeError("Article table needs at least one column.");
426
+ if (caption.trim() === "") throw new RangeError("Article table needs a caption.");
427
+ for (const row of rows) if (row.length !== columns.length) throw new RangeError("Article table rows must match the column count.");
428
+ return (
429
+ <figure className={joinClasses("plain-publication__figure plain-publication__data-table", className)} data-figure-kind="table" id={id}>
430
+ <div className="plain-publication__table-scroll" role="region" aria-label={caption} tabIndex={0}>
431
+ <table className="plain-publication__table">
432
+ <caption>{caption}</caption>
433
+ <thead>
434
+ <tr>
435
+ {columns.map((column, index) => <th data-numeric={column.numeric === true ? "" : undefined} key={index} scope="col">{column.label}</th>)}
436
+ </tr>
437
+ </thead>
438
+ <tbody>
439
+ {rows.map((row, rowIndex) => (
440
+ <tr key={rowIndex}>
441
+ {row.map((cell, cellIndex) => cellIndex === 0
442
+ ? <th key={cellIndex} scope="row">{cell}</th>
443
+ : <td data-numeric={columns[cellIndex]?.numeric === true ? "" : undefined} key={cellIndex}>{cell}</td>)}
444
+ </tr>
445
+ ))}
446
+ </tbody>
447
+ </table>
448
+ </div>
449
+ {note === undefined || note === null ? null : <p className="plain-publication__figure-note">{note}</p>}
450
+ </figure>
451
+ );
452
+ }
453
+
454
+ export type ArticleBarDatum = Readonly<{
455
+ label: string;
456
+ value: number;
457
+ /** The value as readers see it, such as "1.2 s" or "38%". Take it from the facts module. */
458
+ display: string;
459
+ /** Draws this bar in the accent color. */
460
+ highlight?: boolean;
461
+ }>;
462
+
463
+ /**
464
+ * A horizontal bar chart drawn with CSS: no script, no canvas, and every value
465
+ * written out, so it reads the same to screen readers and in forced colors.
466
+ * Bars scale to `max` or the largest value.
467
+ */
468
+ export function ArticleBarChart({
469
+ caption,
470
+ className,
471
+ credit,
472
+ data,
473
+ id,
474
+ max,
475
+ }: Readonly<{
476
+ caption: ReactNode;
477
+ className?: string;
478
+ credit?: ReactNode;
479
+ data: readonly ArticleBarDatum[];
480
+ id?: string;
481
+ max?: number;
482
+ }>) {
483
+ if (data.length === 0) throw new RangeError("Article bar chart needs at least one bar.");
484
+ for (const datum of data) {
485
+ if (!Number.isFinite(datum.value) || datum.value < 0) throw new RangeError(`Article bar chart value for ${JSON.stringify(datum.label)} must be a finite number of at least 0.`);
486
+ if (datum.display.trim() === "" || datum.label.trim() === "") throw new RangeError("Article bar chart bars need a label and a display value.");
487
+ }
488
+ const ceiling = max ?? Math.max(...data.map((datum) => datum.value));
489
+ if (!Number.isFinite(ceiling) || ceiling <= 0) throw new RangeError("Article bar chart max must be greater than 0.");
490
+ for (const datum of data) if (datum.value > ceiling) throw new RangeError(`Article bar chart value for ${JSON.stringify(datum.label)} exceeds max.`);
491
+ return (
492
+ <ArticleFigure caption={caption} className={joinClasses("plain-publication__bar-chart", className)} credit={credit} kind="chart" {...(id === undefined ? {} : { id })}>
493
+ <dl className="plain-publication__bars">
494
+ {data.map((datum) => (
495
+ <div className="plain-publication__bar" data-highlight={datum.highlight === true ? "" : undefined} key={datum.label}>
496
+ <dt>{datum.label}</dt>
497
+ <dd>
498
+ <span aria-hidden="true" className="plain-publication__bar-track">
499
+ <span className="plain-publication__bar-fill" style={{ "--plain-bar": `${Math.round((datum.value / ceiling) * 10000) / 100}%` } as CSSProperties} />
500
+ </span>
501
+ <span className="plain-publication__bar-value">{datum.display}</span>
502
+ </dd>
503
+ </div>
504
+ ))}
505
+ </dl>
506
+ </ArticleFigure>
507
+ );
508
+ }
509
+
510
+ /** A comparison cell: yes, no, partly, or a short note. */
511
+ export type ComparisonValue = boolean | "partial" | Readonly<{ text: string }>;
512
+
513
+ export type ComparisonRow = Readonly<{
514
+ label: string;
515
+ /** One value per option, in column order. */
516
+ values: readonly ComparisonValue[];
517
+ /** A footnote shown under the label. */
518
+ note?: string;
519
+ }>;
520
+
521
+ const COMPARISON_TEXT = { yes: "Yes", no: "No", partial: "Partly" } as const;
522
+
523
+ function ComparisonGlyph({ kind }: Readonly<{ kind: "yes" | "no" | "partial" }>) {
524
+ const path = kind === "yes" ? "M3.5 8.5l3 3 6-7" : kind === "no" ? "M4.5 4.5l7 7m0-7l-7 7" : "M4 8h8";
525
+ return (
526
+ <svg aria-hidden="true" className="plain-publication__comparison-glyph" focusable="false" height="16" viewBox="0 0 16 16" width="16">
527
+ <path d={path} fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.75" />
528
+ </svg>
529
+ );
530
+ }
531
+
532
+ function ComparisonCell({ value }: Readonly<{ value: ComparisonValue }>) {
533
+ if (typeof value === "object") return <>{value.text}</>;
534
+ const kind = value === true ? "yes" : value === false ? "no" : "partial";
535
+ return (
536
+ <span className="plain-publication__comparison-value" data-value={kind}>
537
+ <ComparisonGlyph kind={kind} />
538
+ <span>{COMPARISON_TEXT[kind]}</span>
539
+ </span>
540
+ );
541
+ }
542
+
543
+ /**
544
+ * A feature comparison with check, cross, and partial icons. Each icon keeps
545
+ * its word beside it, so meaning never rests on the glyph or its color.
546
+ * Name options plainly; comparisons with named competitors belong on
547
+ * comparison pages, not launch posts.
548
+ */
549
+ export function ComparisonTable({
550
+ caption,
551
+ className,
552
+ highlight,
553
+ id,
554
+ note,
555
+ options,
556
+ rows,
557
+ }: Readonly<{
558
+ caption: string;
559
+ className?: string;
560
+ /** Index of the option column to emphasize. */
561
+ highlight?: number;
562
+ id?: string;
563
+ note?: ReactNode;
564
+ options: readonly string[];
565
+ rows: readonly ComparisonRow[];
566
+ }>) {
567
+ if (options.length === 0) throw new RangeError("Comparison table needs at least one option.");
568
+ if (caption.trim() === "") throw new RangeError("Comparison table needs a caption.");
569
+ if (highlight !== undefined && (!Number.isInteger(highlight) || highlight < 0 || highlight >= options.length)) {
570
+ throw new RangeError("Comparison table highlight must be an option index.");
571
+ }
572
+ for (const row of rows) if (row.values.length !== options.length) throw new RangeError(`Comparison row ${JSON.stringify(row.label)} must have one value per option.`);
573
+ return (
574
+ <figure className={joinClasses("plain-publication__figure plain-publication__comparison", className)} data-figure-kind="table" id={id}>
575
+ <div className="plain-publication__table-scroll" role="region" aria-label={caption} tabIndex={0}>
576
+ <table className="plain-publication__table">
577
+ <caption>{caption}</caption>
578
+ <thead>
579
+ <tr>
580
+ <td />
581
+ {options.map((option, index) => <th data-highlight={index === highlight ? "" : undefined} key={option} scope="col">{option}</th>)}
582
+ </tr>
583
+ </thead>
584
+ <tbody>
585
+ {rows.map((row) => (
586
+ <tr key={row.label}>
587
+ <th scope="row">
588
+ {row.label}
589
+ {row.note === undefined || row.note === "" ? null : <small className="plain-publication__comparison-note">{row.note}</small>}
590
+ </th>
591
+ {row.values.map((value, index) => (
592
+ <td data-highlight={index === highlight ? "" : undefined} key={index}><ComparisonCell value={value} /></td>
593
+ ))}
594
+ </tr>
595
+ ))}
596
+ </tbody>
597
+ </table>
598
+ </div>
599
+ {note === undefined || note === null ? null : <p className="plain-publication__figure-note">{note}</p>}
600
+ </figure>
601
+ );
602
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { cn } from "@hraness/ui";
4
+ import * as stylex from "@stylexjs/stylex";
5
+
6
+ import { effectsStyles } from "./effects.stylex.js";
7
+ import { PhaserDots } from "./phaser-dots.js";
8
+
9
+ /** The shared fixed aurora canvas and interactive 6 px dot grid. */
10
+ export function AuroraDotsBackground() {
11
+ const backgroundPresentation = stylex.props(effectsStyles.auroraBackground);
12
+ const dotsPresentation = stylex.props(effectsStyles.auroraDots);
13
+
14
+ return (
15
+ <>
16
+ <div
17
+ aria-hidden="true"
18
+ className={cn(
19
+ "hraness-design-aurora-background",
20
+ backgroundPresentation.className,
21
+ )}
22
+ />
23
+ <div
24
+ aria-hidden="true"
25
+ className={cn(
26
+ "hraness-design-aurora-dots",
27
+ dotsPresentation.className,
28
+ )}
29
+ >
30
+ <PhaserDots mouseGlow />
31
+ </div>
32
+ </>
33
+ );
34
+ }