@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,1611 @@
1
+ import type { ReactNode } from "react";
2
+ import { FoilMark } from "./foil-mark.js";
3
+ import { SyntaxCode } from "./syntax-code.js";
4
+ import { marketingClassName as classNames, marketingColumnClassName, marketingFactCellVariant } from "./product-marketing.stylex.js";
5
+ import type { MarketingColumnCount } from "./product-marketing.stylex.js";
6
+
7
+ export type { MarketingColumnCount } from "./product-marketing.stylex.js";
8
+
9
+ export type MarketingHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
10
+
11
+ const MARKETING_HEADING_TAGS = {
12
+ 1: "h1",
13
+ 2: "h2",
14
+ 3: "h3",
15
+ 4: "h4",
16
+ 5: "h5",
17
+ 6: "h6",
18
+ } as const;
19
+
20
+ export type MarketingTone = "paper" | "accent";
21
+ export type MarketingPreset = "editorial" | "minimal";
22
+ /**
23
+ * Accepted `data-hraness-pattern` values. Patterns are retired: `cells`,
24
+ * `weave`, `contour`, and `mesh` stay valid for compatibility but render
25
+ * exactly like `none`.
26
+ */
27
+ export const marketingPatterns = ["cells", "weave", "contour", "mesh", "none"] as const;
28
+ /** @deprecated Patterns are retired; every value renders as `none`. */
29
+ export type MarketingPattern = (typeof marketingPatterns)[number];
30
+
31
+ function assertMarketingPattern(pattern: MarketingPattern | undefined): void {
32
+ if (pattern !== undefined && !marketingPatterns.includes(pattern)) throw new RangeError("Unknown marketing pattern.");
33
+ }
34
+
35
+ export interface MarketingAction {
36
+ readonly emphasis?: "primary" | "secondary";
37
+ readonly href: string;
38
+ readonly label: string;
39
+ }
40
+
41
+ export interface MarketingFact {
42
+ readonly detail?: string;
43
+ readonly label: string;
44
+ readonly value: string;
45
+ }
46
+
47
+ export interface MarketingStep {
48
+ readonly code?: string;
49
+ readonly detail?: string;
50
+ readonly label: string;
51
+ }
52
+
53
+ export interface MarketingLink {
54
+ readonly current?: boolean;
55
+ readonly href: string;
56
+ readonly label: string;
57
+ }
58
+
59
+ function Heading({
60
+ children,
61
+ className,
62
+ id,
63
+ level,
64
+ }: Readonly<{
65
+ children: ReactNode;
66
+ className?: string;
67
+ id?: string;
68
+ level: MarketingHeadingLevel;
69
+ }>) {
70
+ const properties = { children, className, id };
71
+ const HeadingTag = MARKETING_HEADING_TAGS[level];
72
+ return <HeadingTag {...properties} />;
73
+ }
74
+
75
+ function childHeadingLevel(level: MarketingHeadingLevel): MarketingHeadingLevel {
76
+ return Math.min(level + 1, 6) as MarketingHeadingLevel;
77
+ }
78
+
79
+ function MarketingActions({
80
+ actions,
81
+ className,
82
+ tone,
83
+ context,
84
+ }: Readonly<{ actions: readonly MarketingAction[]; className: string; tone: MarketingTone; context: "hero" | "cta" }>) {
85
+ if (actions.length === 0) return null;
86
+ return (
87
+ <div className={className}>
88
+ {actions.map((action, index) => {
89
+ const emphasis = action.emphasis ?? (index === 0 ? "primary" : "secondary");
90
+ return (
91
+ <a
92
+ className={classNames("hraness-marketing-action", undefined, tone === "accent"
93
+ ? `${context}-${emphasis}`
94
+ : emphasis === "primary" ? "primary" : "default")}
95
+ data-emphasis={emphasis}
96
+ data-foil={emphasis === "primary" ? "" : undefined}
97
+ href={action.href}
98
+ key={`${action.href}-${action.label}`}
99
+ >
100
+ {action.label}
101
+ </a>
102
+ );
103
+ })}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Page root. Binds the marketing tokens, the text face, and the horizontal
110
+ * gutter for every direct child role. Products set `--hraness-site-accent`
111
+ * on this element or on a parent.
112
+ */
113
+ export function MarketingPage({
114
+ children,
115
+ className,
116
+ id,
117
+ preset,
118
+ pattern,
119
+ }: Readonly<{
120
+ children: ReactNode;
121
+ className?: string;
122
+ id?: string;
123
+ /** Opt in to the separately imported product-marketing-preset.css contract. */
124
+ preset?: MarketingPreset;
125
+ /**
126
+ * @deprecated Patterns are retired. Every value, including the default,
127
+ * paints the flat palette background; the attribute is still emitted for
128
+ * compatibility with existing selectors.
129
+ */
130
+ pattern?: MarketingPattern;
131
+ }>) {
132
+ if (preset !== undefined && preset !== "editorial" && preset !== "minimal") throw new RangeError("Unknown marketing preset.");
133
+ assertMarketingPattern(pattern);
134
+ return (
135
+ <div className={classNames("hraness-marketing-page", className)} data-hraness-marketing="page" data-hraness-marketing-preset={preset} data-hraness-pattern={pattern} id={id}>
136
+ {children}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Opening container for the hero. It paints the flat palette background: the
143
+ * retired grain, cell, and pattern textures no longer render. It never creates
144
+ * an overlay or a fixed-position containing block.
145
+ */
146
+ export function MarketingField({ children, className, pattern }: Readonly<{
147
+ children: ReactNode;
148
+ className?: string;
149
+ /** @deprecated Patterns are retired; every value renders as `none`. */
150
+ pattern?: MarketingPattern;
151
+ }>) {
152
+ assertMarketingPattern(pattern);
153
+ return <div className={["hraness-marketing-field", className].filter(Boolean).join(" ")} data-hraness-marketing="field" data-hraness-pattern={pattern}>{children}</div>;
154
+ }
155
+
156
+ export type MarketingMainClearance = "scroll" | "pad";
157
+
158
+ /**
159
+ * Main landmark that inherits --hraness-sticky-offset from a preceding sticky
160
+ * header. Hash and skip targets use the offset as scroll-margin. Pass
161
+ * clearance="pad" when the header is out of flow.
162
+ */
163
+ export function MarketingMain({
164
+ children,
165
+ className,
166
+ clearance = "scroll",
167
+ id = "main-content",
168
+ }: Readonly<{
169
+ children: ReactNode;
170
+ className?: string;
171
+ clearance?: MarketingMainClearance;
172
+ id?: string;
173
+ }>) {
174
+ if (clearance !== "scroll" && clearance !== "pad") throw new RangeError("Marketing main clearance must be scroll or pad.");
175
+ return (
176
+ <main
177
+ className={classNames("hraness-marketing-main", className, clearance === "pad" ? "pad" : "default")}
178
+ data-hraness-clearance={clearance === "pad" ? "pad" : undefined}
179
+ data-hraness-marketing="main"
180
+ id={id}
181
+ >
182
+ {children}
183
+ </main>
184
+ );
185
+ }
186
+
187
+ export interface MarketingCardItem {
188
+ readonly art?: ReactNode;
189
+ readonly href?: string;
190
+ readonly meta?: ReactNode;
191
+ readonly title: string;
192
+ }
193
+
194
+ /**
195
+ * Stretching product card row. Direct children share the tallest item's
196
+ * height. Meta sits in a reserved two-line block; titles wrap without
197
+ * clamping. Per-card art wells clip overflow so a logo surface cannot
198
+ * paint through the gutter.
199
+ */
200
+ export function MarketingCardRow({
201
+ ariaLabel,
202
+ cards,
203
+ children,
204
+ className,
205
+ }: Readonly<{
206
+ ariaLabel?: string;
207
+ cards?: readonly MarketingCardItem[];
208
+ children?: ReactNode;
209
+ className?: string;
210
+ }>) {
211
+ return (
212
+ <div
213
+ aria-label={ariaLabel}
214
+ className={classNames("hraness-marketing-card-row", className)}
215
+ data-hraness-marketing="card-row"
216
+ >
217
+ {cards?.map((card) => (
218
+ <MarketingCard
219
+ key={card.title}
220
+ title={card.title}
221
+ {...(card.art === undefined ? {} : { art: card.art })}
222
+ {...(card.href === undefined ? {} : { href: card.href })}
223
+ {...(card.meta === undefined ? {} : { meta: card.meta })}
224
+ />
225
+ ))}
226
+ {children}
227
+ </div>
228
+ );
229
+ }
230
+
231
+ /** Clipped media well. Keep logo and background paint inside the card. */
232
+ export function MarketingCardArt({
233
+ children,
234
+ className,
235
+ }: Readonly<{
236
+ children?: ReactNode;
237
+ className?: string;
238
+ }>) {
239
+ return (
240
+ <div className={classNames("hraness-marketing-card__art", className)} data-hraness-marketing="card-art">
241
+ {children}
242
+ </div>
243
+ );
244
+ }
245
+
246
+ function isPresentNode(value: ReactNode): boolean {
247
+ return value !== undefined && value !== false && value !== null && value !== "";
248
+ }
249
+
250
+ export function MarketingCard({
251
+ art,
252
+ children,
253
+ className,
254
+ href,
255
+ meta,
256
+ title,
257
+ }: Readonly<{
258
+ art?: ReactNode;
259
+ children?: ReactNode;
260
+ className?: string;
261
+ href?: string;
262
+ meta?: ReactNode;
263
+ title: string;
264
+ }>) {
265
+ const body = (
266
+ <>
267
+ {isPresentNode(art) ? <MarketingCardArt>{art}</MarketingCardArt> : null}
268
+ <h3 className={classNames("hraness-marketing-card__title")}>{title}</h3>
269
+ {meta === undefined || meta === "" ? null : <p className={classNames("hraness-marketing-card__meta")}>{meta}</p>}
270
+ {isPresentNode(children) ? <div className={classNames("hraness-marketing-card__body")}>{children}</div> : null}
271
+ </>
272
+ );
273
+ if (href === undefined) {
274
+ return (
275
+ <article className={classNames("hraness-marketing-card", className)} data-hraness-marketing="card">
276
+ {body}
277
+ </article>
278
+ );
279
+ }
280
+ return (
281
+ <a className={classNames("hraness-marketing-card", className)} data-hraness-marketing="card" href={href}>
282
+ {body}
283
+ </a>
284
+ );
285
+ }
286
+
287
+ export function MarketingSiteHeader({
288
+ action,
289
+ ariaLabel = "Site",
290
+ brand,
291
+ brandHref = "/",
292
+ brandLabel,
293
+ brandMark,
294
+ className,
295
+ links,
296
+ trailing,
297
+ sticky = true,
298
+ }: Readonly<{
299
+ action?: MarketingAction;
300
+ ariaLabel?: string;
301
+ brand: ReactNode;
302
+ brandHref?: string;
303
+ brandLabel?: string;
304
+ /** Transparent product mark; the original image remains the fallback. */
305
+ brandMark?: string;
306
+ className?: string;
307
+ links: readonly MarketingLink[];
308
+ trailing?: ReactNode;
309
+ /** Keep embedded previews in document flow; product headers stick by default. */
310
+ sticky?: boolean;
311
+ }>) {
312
+ const brandProperties = brandLabel === undefined ? {} : { "aria-label": brandLabel };
313
+ return (
314
+ <header
315
+ className={classNames("hraness-marketing-header", className, sticky ? "default" : "static")}
316
+ data-hraness-marketing="header"
317
+ data-position={sticky ? "sticky" : "static"}
318
+ >
319
+ <div className={classNames("hraness-marketing-header__inner")}>
320
+ <a className={classNames("hraness-marketing-header__brand")} data-foil="" href={brandHref} {...brandProperties}>
321
+ {brandMark === undefined ? null : <FoilMark src={brandMark} />}
322
+ {brand}
323
+ </a>
324
+ <nav aria-label={ariaLabel} className={classNames("hraness-marketing-header__nav")}>
325
+ {links.map((link) => (
326
+ <a
327
+ aria-current={link.current === true ? "page" : undefined}
328
+ className={classNames("hraness-marketing-header__link", undefined, link.current === true ? "current" : "default")}
329
+ href={link.href}
330
+ key={`${link.href}-${link.label}`}
331
+ >
332
+ {link.label}
333
+ </a>
334
+ ))}
335
+ </nav>
336
+ {action === undefined && trailing === undefined
337
+ ? null
338
+ : (
339
+ <div className={classNames("hraness-marketing-header__actions")}>
340
+ {action === undefined
341
+ ? null
342
+ : (
343
+ <a
344
+ className={classNames("hraness-marketing-action", undefined, `header-${action.emphasis ?? "primary"}`)}
345
+ data-emphasis={action.emphasis ?? "primary"}
346
+ data-foil={(action.emphasis ?? "primary") === "primary" ? "" : undefined}
347
+ href={action.href}
348
+ >
349
+ {action.label}
350
+ </a>
351
+ )}
352
+ {trailing}
353
+ </div>
354
+ )}
355
+ </div>
356
+ </header>
357
+ );
358
+ }
359
+
360
+ /**
361
+ * In-flow site footer. The brand lockup is always the product mark followed by
362
+ * its name; optional children carry a product note and optional links a quiet
363
+ * footer navigation. It rests at the end of document flow; when the shared
364
+ * network footer follows it directly, the marketing grammar joins the pair
365
+ * into one band.
366
+ */
367
+ export function MarketingSiteFooter({
368
+ ariaLabel = "Site",
369
+ brand,
370
+ brandHref = "/",
371
+ brandLabel,
372
+ brandMark,
373
+ children,
374
+ className,
375
+ links = [],
376
+ linksLabel = "Footer navigation",
377
+ name,
378
+ }: Readonly<{
379
+ /** Landmark label; keep it distinct from the network footer on the same page. */
380
+ ariaLabel?: string;
381
+ /** The product mark, conventionally one inline SVG; rendered before `name`. */
382
+ brand: ReactNode;
383
+ brandHref?: string;
384
+ brandLabel?: string;
385
+ /**
386
+ * Transparent product mark. When set, the lockup renders the shared foil
387
+ * icon-plus-text treatment like the site header; `brand` remains the
388
+ * forced-color and no-mask fallback for that mark.
389
+ */
390
+ brandMark?: string;
391
+ /** Product-owned note content between the lockup and the navigation. */
392
+ children?: ReactNode;
393
+ className?: string;
394
+ links?: readonly MarketingLink[];
395
+ linksLabel?: string;
396
+ /** The product name rendered inside the home link after `brand`. */
397
+ name: string;
398
+ }>) {
399
+ const brandProperties = brandLabel === undefined ? {} : { "aria-label": brandLabel };
400
+ const foilBrand = brandMark !== undefined;
401
+ return (
402
+ <footer aria-label={ariaLabel} className={classNames("hraness-marketing-footer", className)} data-hraness-marketing="footer">
403
+ <div className={classNames("hraness-marketing-footer__inner")}>
404
+ <a
405
+ className={classNames("hraness-marketing-footer__brand", undefined, foilBrand ? "foil" : "default")}
406
+ data-foil={foilBrand ? "" : undefined}
407
+ href={brandHref}
408
+ {...brandProperties}
409
+ >
410
+ {foilBrand ? <FoilMark fallback={brand} size={18} src={brandMark} /> : brand}
411
+ <span className={classNames("hraness-marketing-footer__name")}>{name}</span>
412
+ </a>
413
+ {children}
414
+ {links.length === 0
415
+ ? null
416
+ : (
417
+ <nav aria-label={linksLabel} className={classNames("hraness-marketing-footer__nav")}>
418
+ {links.map((link) => (
419
+ <a
420
+ aria-current={link.current === true ? "page" : undefined}
421
+ className={classNames("hraness-marketing-footer__link", undefined, link.current === true ? "current" : "default")}
422
+ href={link.href}
423
+ key={`${link.href}-${link.label}`}
424
+ >
425
+ {link.label}
426
+ </a>
427
+ ))}
428
+ </nav>
429
+ )}
430
+ </div>
431
+ </footer>
432
+ );
433
+ }
434
+
435
+ export function MarketingFlow({
436
+ ariaLabel,
437
+ className,
438
+ steps,
439
+ }: Readonly<{
440
+ ariaLabel: string;
441
+ className?: string;
442
+ steps: readonly MarketingStep[];
443
+ }>) {
444
+ return (
445
+ <ol
446
+ aria-label={ariaLabel}
447
+ className={classNames("hraness-marketing-flow", className)}
448
+ data-hraness-marketing="flow"
449
+ >
450
+ {steps.map((step, index) => (
451
+ <li className={classNames("hraness-marketing-flow__step", undefined, index === 0 ? "first" : "default")} key={`${String(index)}-${step.label}`}>
452
+ <span aria-hidden="true" className={classNames("hraness-marketing-flow__number")}>
453
+ {String(index + 1).padStart(2, "0")}
454
+ </span>
455
+ <div className={classNames("hraness-marketing-flow__body")}>
456
+ <strong className={classNames("hraness-marketing-flow__label")}>{step.label}</strong>
457
+ {step.code === undefined
458
+ ? null
459
+ : <SyntaxCode className={classNames("hraness-marketing-flow__code")} code={step.code} styles="classes" />}
460
+ {step.detail === undefined
461
+ ? null
462
+ : <p className={classNames("hraness-marketing-flow__detail")}>{step.detail}</p>}
463
+ </div>
464
+ </li>
465
+ ))}
466
+ </ol>
467
+ );
468
+ }
469
+
470
+ export function MarketingFacts({
471
+ className,
472
+ columns,
473
+ facts,
474
+ }: Readonly<{
475
+ className?: string;
476
+ /** Static desktop columns without inline styles; omission uses the collection length. Mobile stays two columns. */
477
+ columns?: MarketingColumnCount;
478
+ facts: readonly MarketingFact[];
479
+ }>) {
480
+ const rootClassName = marketingColumnClassName("hraness-marketing-facts", className, columns);
481
+ if (facts.length === 0) return null;
482
+ return (
483
+ <dl
484
+ className={rootClassName}
485
+ data-hraness-marketing="facts"
486
+ style={columns === undefined ? { "--hraness-marketing-fact-columns": String(facts.length) } as Record<string, string> : undefined}
487
+ >
488
+ {facts.map((fact, index) => (
489
+ <div className={classNames("hraness-marketing-facts__item", undefined, marketingFactCellVariant(index))} key={`${fact.label}-${fact.value}`}>
490
+ <dt className={classNames("hraness-marketing-facts__label")}>{fact.label}</dt>
491
+ <dd className={classNames("hraness-marketing-facts__body")}>
492
+ <strong className={classNames("hraness-marketing-facts__value")}>{fact.value}</strong>
493
+ {fact.detail === undefined ? null : <span className={classNames("hraness-marketing-facts__detail")}>{fact.detail}</span>}
494
+ </dd>
495
+ </div>
496
+ ))}
497
+ </dl>
498
+ );
499
+ }
500
+
501
+ export interface ProductHeroProps {
502
+ readonly actions?: readonly MarketingAction[];
503
+ readonly align?: "center" | "start";
504
+ /**
505
+ * @deprecated Hero backdrops are retired. The hero renders no decorative
506
+ * artwork, light field, or blur, whatever this holds. Show real product
507
+ * proof in `frame` instead.
508
+ */
509
+ readonly backdrop?: ReactNode | false;
510
+ readonly boundary?: string;
511
+ readonly className?: string;
512
+ /** A concrete request a reader could make, shown under the summary. */
513
+ readonly example?: string;
514
+ readonly eyebrow?: string;
515
+ readonly facts?: readonly MarketingFact[];
516
+ /** Static fact columns without inline styles; omission uses the facts length. Mobile stays two columns. */
517
+ readonly factsColumns?: MarketingColumnCount;
518
+ /** A product frame or other proof rendered full width below the copy. */
519
+ readonly frame?: ReactNode;
520
+ readonly heading: string;
521
+ readonly headingId: string;
522
+ readonly headingLevel?: MarketingHeadingLevel;
523
+ readonly name: string;
524
+ /** Product-owned notice after the copy boundary, without an added wrapper. */
525
+ readonly notice?: ReactNode;
526
+ readonly proof?: Readonly<{
527
+ readonly content: ReactNode;
528
+ readonly heading: string;
529
+ readonly kicker?: string;
530
+ }>;
531
+ readonly summary: string;
532
+ readonly tone?: MarketingTone;
533
+ }
534
+
535
+ export function ProductHero({
536
+ actions = [],
537
+ align = "center",
538
+ boundary,
539
+ className,
540
+ example,
541
+ eyebrow,
542
+ facts = [],
543
+ factsColumns,
544
+ frame,
545
+ heading,
546
+ headingId,
547
+ headingLevel = 1,
548
+ name,
549
+ notice,
550
+ proof,
551
+ summary,
552
+ tone = "paper",
553
+ }: Readonly<ProductHeroProps>) {
554
+ return (
555
+ <header
556
+ aria-labelledby={headingId}
557
+ className={classNames("hraness-marketing-hero", className, tone === "accent" ? "accent" : "default")}
558
+ data-align={align}
559
+ data-hraness-marketing="hero"
560
+ data-tone={tone}
561
+ >
562
+ <div className={classNames("hraness-marketing-hero__copy", undefined, align === "start" ? "start" : "default")}>
563
+ {eyebrow === undefined || eyebrow === "" ? null : <p className={classNames("hraness-marketing-hero__eyebrow", undefined, tone === "accent" ? "accent" : "default")}>{eyebrow}</p>}
564
+ <p className={classNames("hraness-marketing-hero__name")}>{name}</p>
565
+ <Heading className={classNames("hraness-marketing-hero__heading")} id={headingId} level={headingLevel}>
566
+ {heading}
567
+ </Heading>
568
+ <p className={classNames("hraness-marketing-hero__summary")}>{summary}</p>
569
+ {example === undefined
570
+ ? null
571
+ : <p className={classNames("hraness-marketing-hero__example")}>{example}</p>}
572
+ <MarketingActions actions={actions} className={classNames("hraness-marketing-hero__actions", undefined, align === "start" ? "start" : "default")} context="hero" tone={tone} />
573
+ {boundary === undefined
574
+ ? null
575
+ : <p className={classNames("hraness-marketing-hero__boundary")}>{boundary}</p>}
576
+ {notice}
577
+ </div>
578
+ {frame === undefined
579
+ ? null
580
+ : <div className={classNames("hraness-marketing-hero__frame")}>{frame}</div>}
581
+ {proof === undefined
582
+ ? null
583
+ : (
584
+ <aside className={classNames("hraness-marketing-proof")} aria-labelledby={`${headingId}-proof`}>
585
+ {proof.kicker === undefined
586
+ ? null
587
+ : <p className={classNames("hraness-marketing-proof__kicker")}>{proof.kicker}</p>}
588
+ <Heading
589
+ className={classNames("hraness-marketing-proof__heading")}
590
+ id={`${headingId}-proof`}
591
+ level={childHeadingLevel(headingLevel)}
592
+ >
593
+ {proof.heading}
594
+ </Heading>
595
+ {proof.content}
596
+ </aside>
597
+ )}
598
+ <MarketingFacts facts={facts} {...(factsColumns === undefined ? {} : { columns: factsColumns })} />
599
+ </header>
600
+ );
601
+ }
602
+
603
+ export interface MarketingPillar {
604
+ readonly label: string;
605
+ readonly summary: string;
606
+ }
607
+
608
+ export function MarketingPillars({
609
+ ariaLabel,
610
+ className,
611
+ columns,
612
+ pillars,
613
+ }: Readonly<{
614
+ ariaLabel: string;
615
+ className?: string;
616
+ /** Static desktop columns without inline styles; omission uses the collection length. Mobile stays one column. */
617
+ columns?: MarketingColumnCount;
618
+ pillars: readonly MarketingPillar[];
619
+ }>) {
620
+ const rootClassName = marketingColumnClassName("hraness-marketing-pillars", className, columns);
621
+ if (pillars.length === 0) return null;
622
+ return (
623
+ <dl
624
+ aria-label={ariaLabel}
625
+ className={rootClassName}
626
+ data-hraness-marketing="pillars"
627
+ style={columns === undefined ? { "--hraness-marketing-pillar-columns": String(pillars.length) } as Record<string, string> : undefined}
628
+ >
629
+ {pillars.map((pillar, index) => (
630
+ <div className={classNames("hraness-marketing-pillars__item", undefined, index === 0 ? "default" : "later")} key={pillar.label}>
631
+ <dt className={classNames("hraness-marketing-pillars__label")}>{pillar.label}</dt>
632
+ <dd className={classNames("hraness-marketing-pillars__summary")}>{pillar.summary}</dd>
633
+ </div>
634
+ ))}
635
+ </dl>
636
+ );
637
+ }
638
+
639
+ export function MarketingInstallPanel({
640
+ children,
641
+ className,
642
+ eyebrow,
643
+ heading,
644
+ headingId,
645
+ headingLevel = 2,
646
+ id,
647
+ note,
648
+ }: Readonly<{
649
+ children: ReactNode;
650
+ className?: string;
651
+ eyebrow?: string;
652
+ heading: string;
653
+ headingId: string;
654
+ headingLevel?: MarketingHeadingLevel;
655
+ id?: string;
656
+ /** Product-owned supporting copy directly after the heading. */
657
+ note?: ReactNode;
658
+ }>) {
659
+ return (
660
+ <section
661
+ aria-labelledby={headingId}
662
+ className={classNames("hraness-marketing-install", className)}
663
+ data-hraness-marketing="install"
664
+ id={id}
665
+ >
666
+ <div className={classNames("hraness-marketing-install__heading-group")}>
667
+ {eyebrow === undefined || eyebrow === "" ? null : <p className={classNames("hraness-marketing-install__eyebrow")}>{eyebrow}</p>}
668
+ <Heading className={classNames("hraness-marketing-install__heading")} id={headingId} level={headingLevel}>
669
+ {heading}
670
+ </Heading>
671
+ {note}
672
+ </div>
673
+ <div className={classNames("hraness-marketing-install__commands")}>{children}</div>
674
+ </section>
675
+ );
676
+ }
677
+
678
+ export type MarketingProofFrameChrome = "window" | "browser" | "terminal";
679
+
680
+ /** Reads a frame URL for display: host and path, no scheme, no query or fragment. */
681
+ export function marketingProofFrameAddress(url: string): string {
682
+ let parsed: URL;
683
+ try {
684
+ parsed = new URL(url);
685
+ } catch {
686
+ throw new RangeError(`Proof frame url must be an absolute URL; received ${JSON.stringify(url)}.`);
687
+ }
688
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new RangeError("Proof frame url must use http or https.");
689
+ const path = parsed.pathname === "/" ? "" : parsed.pathname;
690
+ return `${parsed.host}${path}`;
691
+ }
692
+
693
+ /**
694
+ * A captioned frame around a screenshot, recording, or code-built mockup.
695
+ * `chrome` draws a window title bar, a browser address bar, or a terminal
696
+ * title bar above the content. A `title` without `chrome` keeps the window
697
+ * bar. The browser bar shows `url` as host and path.
698
+ */
699
+ export function MarketingProofFrame({
700
+ caption,
701
+ children,
702
+ chrome,
703
+ className,
704
+ credit,
705
+ title,
706
+ url,
707
+ }: Readonly<{
708
+ caption?: string;
709
+ children: ReactNode;
710
+ /** The title bar drawn above the content. Defaults to `window` when `title` is given, otherwise none. */
711
+ chrome?: MarketingProofFrameChrome;
712
+ className?: string;
713
+ credit?: string;
714
+ /** Window or terminal title; for the browser bar, the address shown when `url` is omitted. */
715
+ title?: string;
716
+ /** The page address shown in the browser bar. */
717
+ url?: string;
718
+ }>) {
719
+ if (chrome !== undefined && chrome !== "window" && chrome !== "browser" && chrome !== "terminal") {
720
+ throw new RangeError(`Unknown proof frame chrome: ${String(chrome)}.`);
721
+ }
722
+ if (url !== undefined && chrome !== "browser") throw new RangeError("Proof frame url needs chrome=\"browser\".");
723
+ const bar = chrome ?? (title === undefined ? undefined : "window");
724
+ const label = bar === "browser" ? (url === undefined ? title : marketingProofFrameAddress(url)) : title;
725
+ if (bar !== undefined && (label === undefined || label.trim() === "")) {
726
+ throw new RangeError(`Proof frame chrome=${bar} needs ${bar === "browser" ? "a url or title" : "a title"}.`);
727
+ }
728
+ return (
729
+ <figure
730
+ className={classNames("hraness-marketing-proof-frame", className)}
731
+ data-chrome={bar}
732
+ data-hraness-marketing="proof-frame"
733
+ >
734
+ {bar === undefined
735
+ ? null
736
+ : (
737
+ <div aria-hidden="true" className={classNames("hraness-marketing-proof-frame__chrome")}>
738
+ <span className={classNames("hraness-marketing-proof-frame__lights")}>
739
+ <span className={classNames("hraness-marketing-proof-frame__light")} />
740
+ <span className={classNames("hraness-marketing-proof-frame__light")} />
741
+ <span className={classNames("hraness-marketing-proof-frame__light")} />
742
+ </span>
743
+ {bar === "browser"
744
+ ? <span className={classNames("hraness-marketing-proof-frame__address")}>{label}</span>
745
+ : <span className={classNames("hraness-marketing-proof-frame__title", undefined, bar === "terminal" ? "terminal" : "default")}>{label}</span>}
746
+ </div>
747
+ )}
748
+ <div className={classNames("hraness-marketing-proof-frame__content")}>{children}</div>
749
+ {caption === undefined && credit === undefined
750
+ ? null
751
+ : (
752
+ <figcaption className={classNames("hraness-marketing-proof-frame__caption")}>
753
+ {caption === undefined ? null : <span>{caption}</span>}
754
+ {credit === undefined ? null : <small className={classNames("hraness-marketing-proof-frame__credit")}>{credit}</small>}
755
+ </figcaption>
756
+ )}
757
+ </figure>
758
+ );
759
+ }
760
+
761
+ export interface MarketingDataTableColumn {
762
+ readonly label: string;
763
+ /** Right-aligns the column and keeps values on one line. */
764
+ readonly numeric?: boolean;
765
+ }
766
+
767
+ export interface MarketingDataTableCell {
768
+ readonly content: ReactNode;
769
+ /** Marks a notable outcome: `positive` accents the value, `negative` mutes it. */
770
+ readonly tone?: "positive" | "negative";
771
+ }
772
+
773
+ function dataTableCell(cell: ReactNode | MarketingDataTableCell): {
774
+ content: ReactNode;
775
+ tone?: "positive" | "negative";
776
+ } {
777
+ if (cell !== null && typeof cell === "object" && !Array.isArray(cell) && "content" in cell) {
778
+ return cell as MarketingDataTableCell;
779
+ }
780
+ return { content: cell };
781
+ }
782
+
783
+ /**
784
+ * A dated, captioned comparison table. Renders a ruled figure whose scrollable
785
+ * region never escapes the measure; the first column is each row's heading and
786
+ * `note` keeps the measurement's scope honest underneath.
787
+ */
788
+ export function MarketingDataTable({
789
+ caption,
790
+ className,
791
+ columns,
792
+ meta,
793
+ note,
794
+ rows,
795
+ }: Readonly<{
796
+ /** Headline naming the measurement. */
797
+ caption: ReactNode;
798
+ className?: string;
799
+ columns: readonly MarketingDataTableColumn[];
800
+ /** Date, source, or sample size — rendered muted beside the caption. */
801
+ meta?: ReactNode;
802
+ /** Caveat rendered under the table; keep the measurement's scope honest. */
803
+ note?: ReactNode;
804
+ rows: readonly ReadonlyArray<ReactNode | MarketingDataTableCell>[];
805
+ }>) {
806
+ if (columns.length === 0) throw new RangeError("Marketing data table needs at least one column.");
807
+ for (const row of rows) {
808
+ if (row.length !== columns.length) throw new RangeError("Marketing data table rows must match the column count.");
809
+ }
810
+ return (
811
+ <figure
812
+ className={classNames("hraness-marketing-data-table", className)}
813
+ data-hraness-marketing="data-table"
814
+ >
815
+ <figcaption className={classNames("hraness-marketing-data-table__head")}>
816
+ <span className={classNames("hraness-marketing-data-table__title")}>{caption}</span>
817
+ {meta === undefined ? null : <span className={classNames("hraness-marketing-data-table__meta")}>{meta}</span>}
818
+ </figcaption>
819
+ <div className={classNames("hraness-marketing-data-table__scroll")}>
820
+ <table className={classNames("hraness-marketing-data-table__table")}>
821
+ <thead>
822
+ <tr>
823
+ {columns.map((column, index) => (
824
+ <th
825
+ className={classNames("hraness-marketing-data-table__heading")}
826
+ data-numeric={column.numeric === true ? "" : undefined}
827
+ key={index}
828
+ scope="col"
829
+ >
830
+ {column.label}
831
+ </th>
832
+ ))}
833
+ </tr>
834
+ </thead>
835
+ <tbody>
836
+ {rows.map((row, rowIndex) => (
837
+ <tr key={rowIndex}>
838
+ {row.map((cell, cellIndex) => {
839
+ const { content, tone } = dataTableCell(cell);
840
+ if (cellIndex === 0) {
841
+ return (
842
+ <th
843
+ className={classNames("hraness-marketing-data-table__row-heading")}
844
+ data-numeric={columns[0]?.numeric === true ? "" : undefined}
845
+ key={cellIndex}
846
+ scope="row"
847
+ >
848
+ {content}
849
+ </th>
850
+ );
851
+ }
852
+ return (
853
+ <td
854
+ className={classNames("hraness-marketing-data-table__cell")}
855
+ data-numeric={columns[cellIndex]?.numeric === true ? "" : undefined}
856
+ data-tone={tone}
857
+ key={cellIndex}
858
+ >
859
+ {content}
860
+ </td>
861
+ );
862
+ })}
863
+ </tr>
864
+ ))}
865
+ </tbody>
866
+ </table>
867
+ </div>
868
+ {note === undefined ? null : <p className={classNames("hraness-marketing-data-table__note")}>{note}</p>}
869
+ </figure>
870
+ );
871
+ }
872
+
873
+ /**
874
+ * A mono code block with shared syntax highlighting. Wraps `SyntaxCode` in the
875
+ * `pre` the marketing grammar styles and scrolls inside the measure when a
876
+ * line overflows.
877
+ */
878
+ export function MarketingCodeBlock({
879
+ className,
880
+ code,
881
+ language,
882
+ }: Readonly<{
883
+ className?: string;
884
+ code: string;
885
+ /** A language/fence hint; omitted hints use conservative automatic selection. */
886
+ language?: string;
887
+ }>) {
888
+ return (
889
+ <pre className={classNames("hraness-marketing-code", className)} data-hraness-marketing="code">
890
+ <SyntaxCode code={code} {...(language === undefined ? {} : { language })} styles="classes" />
891
+ </pre>
892
+ );
893
+ }
894
+
895
+ /** The section's native paragraph label, reusable by product-owned prose. */
896
+ export function MarketingSectionLabel({
897
+ children,
898
+ className,
899
+ size = "default",
900
+ }: Readonly<{
901
+ children: string;
902
+ className?: string;
903
+ size?: "default" | "body";
904
+ }>) {
905
+ if (size !== "default" && size !== "body") throw new RangeError("Marketing label size must be default or body.");
906
+ return <p className={classNames("hraness-marketing-section__label", className, size)}
907
+ data-size={size === "body" ? "body" : undefined}>{children}</p>;
908
+ }
909
+
910
+ export function MarketingSection({
911
+ children,
912
+ className,
913
+ heading,
914
+ headingId,
915
+ headingLevel = 2,
916
+ id,
917
+ label,
918
+ layout = "stack",
919
+ summary,
920
+ }: Readonly<{
921
+ children: ReactNode;
922
+ className?: string;
923
+ heading: string;
924
+ headingId: string;
925
+ headingLevel?: MarketingHeadingLevel;
926
+ id?: string;
927
+ label?: string;
928
+ layout?: "split" | "split-reverse" | "stack";
929
+ summary?: string;
930
+ }>) {
931
+ return (
932
+ <section
933
+ aria-labelledby={headingId}
934
+ className={classNames("hraness-marketing-section", className, layout === "stack" ? "default" : "split")}
935
+ data-hraness-marketing="section"
936
+ data-layout={layout}
937
+ id={id}
938
+ >
939
+ <div className={classNames("hraness-marketing-section__heading-group", undefined, layout === "stack" ? "default" : layout === "split" ? "split" : "reverse")}>
940
+ {label === undefined || label === "" ? null : <MarketingSectionLabel>{label}</MarketingSectionLabel>}
941
+ <Heading className={classNames("hraness-marketing-section__heading")} id={headingId} level={headingLevel}>
942
+ {heading}
943
+ </Heading>
944
+ {summary === undefined
945
+ ? null
946
+ : <p className={classNames("hraness-marketing-section__summary")}>{summary}</p>}
947
+ </div>
948
+ <div className={classNames("hraness-marketing-section__body")}>{children}</div>
949
+ </section>
950
+ );
951
+ }
952
+
953
+ type MarketingCollectionPrefix = "interfaces" | "pricing" | "primitives" | "questions" | "quotes" | "related" | "trust";
954
+
955
+ interface MarketingCollectionHeaderProps {
956
+ readonly heading: string;
957
+ readonly headingId: string;
958
+ readonly headingLevel: MarketingHeadingLevel;
959
+ readonly label: string | undefined;
960
+ readonly prefix: MarketingCollectionPrefix;
961
+ readonly summary: string | undefined;
962
+ }
963
+
964
+ function MarketingCollectionHeader({
965
+ heading,
966
+ headingId,
967
+ headingLevel,
968
+ label,
969
+ prefix,
970
+ summary,
971
+ }: Readonly<MarketingCollectionHeaderProps>) {
972
+ return (
973
+ <header className={classNames(`hraness-marketing-${prefix}__header`)}>
974
+ {label === undefined || label === "" ? null : <p className={classNames(`hraness-marketing-${prefix}__label`)}>{label}</p>}
975
+ <Heading className={classNames(`hraness-marketing-${prefix}__heading`)} id={headingId} level={headingLevel}>
976
+ {heading}
977
+ </Heading>
978
+ {summary === undefined ? null : <p className={classNames(`hraness-marketing-${prefix}__summary`)}>{summary}</p>}
979
+ </header>
980
+ );
981
+ }
982
+
983
+ export interface MarketingPrimitive {
984
+ readonly example?: ReactNode;
985
+ readonly label: string;
986
+ readonly summary: string;
987
+ }
988
+
989
+ export function MarketingPrimitives({
990
+ className,
991
+ heading,
992
+ headingId,
993
+ headingLevel = 2,
994
+ id,
995
+ items,
996
+ label,
997
+ summary,
998
+ }: Readonly<{
999
+ className?: string;
1000
+ heading: string;
1001
+ headingId: string;
1002
+ headingLevel?: MarketingHeadingLevel;
1003
+ id?: string;
1004
+ items: readonly MarketingPrimitive[];
1005
+ label?: string;
1006
+ summary?: string;
1007
+ }>) {
1008
+ return (
1009
+ <section
1010
+ aria-labelledby={headingId}
1011
+ className={classNames("hraness-marketing-primitives", className)}
1012
+ data-hraness-marketing="primitives"
1013
+ id={id}
1014
+ >
1015
+ <MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="primitives" />
1016
+ <ol className={classNames("hraness-marketing-primitives__list")}>
1017
+ {items.map((item, index) => (
1018
+ <li className={classNames("hraness-marketing-primitive")} key={item.label}>
1019
+ <span aria-hidden="true" className={classNames("hraness-marketing-primitive__number")}>
1020
+ {String(index + 1).padStart(2, "0")}
1021
+ </span>
1022
+ <Heading className={classNames("hraness-marketing-primitive__heading")} level={childHeadingLevel(headingLevel)}>
1023
+ {item.label}
1024
+ </Heading>
1025
+ <p className={classNames("hraness-marketing-primitive__summary")}>{item.summary}</p>
1026
+ {item.example}
1027
+ </li>
1028
+ ))}
1029
+ </ol>
1030
+ </section>
1031
+ );
1032
+ }
1033
+
1034
+ export type MarketingNoticeTone = "info" | "success" | "error";
1035
+
1036
+ /**
1037
+ * One status or alert line inside the page column, such as a confirmation
1038
+ * after a redirect. Errors are announced as alerts; other tones as status.
1039
+ */
1040
+ export function MarketingNotice({
1041
+ children,
1042
+ className,
1043
+ tone = "info",
1044
+ }: Readonly<{
1045
+ children: ReactNode;
1046
+ className?: string;
1047
+ tone?: MarketingNoticeTone;
1048
+ }>) {
1049
+ if (tone !== "info" && tone !== "success" && tone !== "error") throw new RangeError("Marketing notice tone must be info, success, or error.");
1050
+ return (
1051
+ <p
1052
+ className={classNames("hraness-marketing-notice", className, tone === "info" ? "default" : tone)}
1053
+ data-hraness-marketing="notice"
1054
+ data-tone={tone}
1055
+ role={tone === "error" ? "alert" : "status"}
1056
+ >
1057
+ {children}
1058
+ </p>
1059
+ );
1060
+ }
1061
+
1062
+ export interface MarketingStat {
1063
+ readonly detail?: string;
1064
+ readonly label: string;
1065
+ readonly value: string;
1066
+ }
1067
+
1068
+ export function MarketingStatStrip({
1069
+ ariaLabel,
1070
+ className,
1071
+ columns,
1072
+ source,
1073
+ stats,
1074
+ }: Readonly<{
1075
+ ariaLabel: string;
1076
+ className?: string;
1077
+ /** Static desktop columns without inline styles; omission uses the collection length. Mobile stays two columns. */
1078
+ columns?: MarketingColumnCount;
1079
+ /** Where the numbers come from and when they were checked. */
1080
+ source?: ReactNode;
1081
+ stats: readonly MarketingStat[];
1082
+ }>) {
1083
+ const listClassName = marketingColumnClassName("hraness-marketing-stats__list", undefined, columns);
1084
+ if (stats.length === 0) return null;
1085
+ return (
1086
+ <section
1087
+ aria-label={ariaLabel}
1088
+ className={classNames("hraness-marketing-stats", className)}
1089
+ data-hraness-marketing="stats"
1090
+ >
1091
+ <dl
1092
+ className={listClassName}
1093
+ style={columns === undefined ? { "--hraness-marketing-fact-columns": String(stats.length) } as Record<string, string> : undefined}
1094
+ >
1095
+ {stats.map((stat, index) => (
1096
+ <div className={classNames("hraness-marketing-facts__item", undefined, marketingFactCellVariant(index))} key={`${stat.label}-${stat.value}`}>
1097
+ <dt className={classNames("hraness-marketing-facts__label")}>{stat.label}</dt>
1098
+ <dd className={classNames("hraness-marketing-facts__body")}>
1099
+ <strong className={classNames("hraness-marketing-stats__value")}>{stat.value}</strong>
1100
+ {stat.detail === undefined ? null : <span className={classNames("hraness-marketing-facts__detail")}>{stat.detail}</span>}
1101
+ </dd>
1102
+ </div>
1103
+ ))}
1104
+ </dl>
1105
+ {source === undefined ? null : <p className={classNames("hraness-marketing-stats__source")}>{source}</p>}
1106
+ </section>
1107
+ );
1108
+ }
1109
+
1110
+ export interface MarketingInterface {
1111
+ readonly example?: ReactNode;
1112
+ readonly label: string;
1113
+ readonly summary: string;
1114
+ }
1115
+
1116
+ export function MarketingInterfaceGrid({
1117
+ className,
1118
+ heading,
1119
+ headingId,
1120
+ headingLevel = 2,
1121
+ id,
1122
+ interfaces,
1123
+ label,
1124
+ summary,
1125
+ }: Readonly<{
1126
+ className?: string;
1127
+ heading: string;
1128
+ headingId: string;
1129
+ headingLevel?: MarketingHeadingLevel;
1130
+ id?: string;
1131
+ interfaces: readonly MarketingInterface[];
1132
+ label?: string;
1133
+ summary?: string;
1134
+ }>) {
1135
+ return (
1136
+ <section
1137
+ aria-labelledby={headingId}
1138
+ className={classNames("hraness-marketing-interfaces", className)}
1139
+ data-hraness-marketing="interfaces"
1140
+ id={id}
1141
+ >
1142
+ <MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="interfaces" />
1143
+ <div className={classNames("hraness-marketing-interface-grid")}>
1144
+ {interfaces.map((entry) => (
1145
+ <article className={classNames("hraness-marketing-interface")} key={entry.label}>
1146
+ <Heading
1147
+ className={classNames("hraness-marketing-interface__heading")}
1148
+ level={childHeadingLevel(headingLevel)}
1149
+ >
1150
+ {entry.label}
1151
+ </Heading>
1152
+ <p className={classNames("hraness-marketing-interface__summary")}>{entry.summary}</p>
1153
+ {entry.example}
1154
+ </article>
1155
+ ))}
1156
+ </div>
1157
+ </section>
1158
+ );
1159
+ }
1160
+
1161
+ export interface MarketingTrustItem {
1162
+ readonly detail: string;
1163
+ readonly label: string;
1164
+ }
1165
+
1166
+ export function MarketingTrustBoundary({
1167
+ className,
1168
+ heading,
1169
+ headingId,
1170
+ headingLevel = 2,
1171
+ id,
1172
+ items,
1173
+ label,
1174
+ summary,
1175
+ }: Readonly<{
1176
+ className?: string;
1177
+ heading: string;
1178
+ headingId: string;
1179
+ headingLevel?: MarketingHeadingLevel;
1180
+ id?: string;
1181
+ items: readonly MarketingTrustItem[];
1182
+ label?: string;
1183
+ summary?: string;
1184
+ }>) {
1185
+ return (
1186
+ <section
1187
+ aria-labelledby={headingId}
1188
+ className={classNames("hraness-marketing-trust", className)}
1189
+ data-hraness-marketing="trust"
1190
+ id={id}
1191
+ >
1192
+ <MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="trust" />
1193
+ <dl className={classNames("hraness-marketing-trust-grid")}>
1194
+ {items.map((item) => (
1195
+ <div className={classNames("hraness-marketing-trust-item")} key={item.label}>
1196
+ <dt className={classNames("hraness-marketing-trust-item__label")}>{item.label}</dt>
1197
+ <dd className={classNames("hraness-marketing-trust-item__detail")}>{item.detail}</dd>
1198
+ </div>
1199
+ ))}
1200
+ </dl>
1201
+ </section>
1202
+ );
1203
+ }
1204
+
1205
+ export interface MarketingQuote {
1206
+ /** Optional profile link for the attribution. */
1207
+ readonly href?: string;
1208
+ readonly name: string;
1209
+ readonly quote: string;
1210
+ /** Handle, title, or affiliation shown after the name. */
1211
+ readonly role?: string;
1212
+ }
1213
+
1214
+ /**
1215
+ * Attributed quotes. Only real, permissioned quotes belong here; render
1216
+ * nothing rather than fill the grid with placeholders.
1217
+ */
1218
+ export function MarketingQuoteGrid({
1219
+ className,
1220
+ heading,
1221
+ headingId,
1222
+ headingLevel = 2,
1223
+ id,
1224
+ label,
1225
+ quotes,
1226
+ summary,
1227
+ }: Readonly<{
1228
+ className?: string;
1229
+ heading: string;
1230
+ headingId: string;
1231
+ headingLevel?: MarketingHeadingLevel;
1232
+ id?: string;
1233
+ label?: string;
1234
+ quotes: readonly MarketingQuote[];
1235
+ summary?: string;
1236
+ }>) {
1237
+ if (quotes.length === 0) return null;
1238
+ return (
1239
+ <section
1240
+ aria-labelledby={headingId}
1241
+ className={classNames("hraness-marketing-quotes", className)}
1242
+ data-hraness-marketing="quotes"
1243
+ id={id}
1244
+ >
1245
+ <MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="quotes" />
1246
+ <ul className={classNames("hraness-marketing-quote-grid")}>
1247
+ {quotes.map((entry) => (
1248
+ <li key={`${entry.name}-${entry.quote.slice(0, 24)}`}>
1249
+ <figure className={classNames("hraness-marketing-quote")}>
1250
+ <blockquote className={classNames("hraness-marketing-quote__body")}>
1251
+ <p className={classNames("hraness-marketing-quote__text")}>{entry.quote}</p>
1252
+ </blockquote>
1253
+ <figcaption className={classNames("hraness-marketing-quote__attribution")}>
1254
+ <strong className={classNames("hraness-marketing-quote__name")}>{entry.name}</strong>
1255
+ {entry.role === undefined
1256
+ ? null
1257
+ : entry.href === undefined
1258
+ ? <span>{entry.role}</span>
1259
+ : <a className={classNames("hraness-marketing-quote__link")} href={entry.href}>{entry.role}</a>}
1260
+ </figcaption>
1261
+ </figure>
1262
+ </li>
1263
+ ))}
1264
+ </ul>
1265
+ </section>
1266
+ );
1267
+ }
1268
+
1269
+ export interface MarketingPlan {
1270
+ readonly action?: MarketingAction;
1271
+ readonly emphasis?: "primary" | "secondary";
1272
+ readonly features: readonly string[];
1273
+ readonly name: string;
1274
+ readonly note?: string;
1275
+ /** Billing period or qualifier shown after the price, such as "per year". */
1276
+ readonly period?: string;
1277
+ readonly price: string;
1278
+ readonly summary?: string;
1279
+ }
1280
+
1281
+ export function MarketingPricing({
1282
+ className,
1283
+ heading,
1284
+ headingId,
1285
+ headingLevel = 2,
1286
+ id,
1287
+ label,
1288
+ plans,
1289
+ summary,
1290
+ }: Readonly<{
1291
+ className?: string;
1292
+ heading: string;
1293
+ headingId: string;
1294
+ headingLevel?: MarketingHeadingLevel;
1295
+ id?: string;
1296
+ label?: string;
1297
+ plans: readonly MarketingPlan[];
1298
+ summary?: string;
1299
+ }>) {
1300
+ return (
1301
+ <section
1302
+ aria-labelledby={headingId}
1303
+ className={classNames("hraness-marketing-pricing", className)}
1304
+ data-hraness-marketing="pricing"
1305
+ id={id}
1306
+ >
1307
+ <MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="pricing" />
1308
+ <ul className={classNames("hraness-marketing-plan-grid")}>
1309
+ {plans.map((plan) => (
1310
+ <li className={classNames("hraness-marketing-plan", undefined, plan.emphasis === "primary" ? "primary" : "default")} data-emphasis={plan.emphasis ?? "secondary"} key={plan.name}>
1311
+ <Heading className={classNames("hraness-marketing-plan__name")} level={childHeadingLevel(headingLevel)}>
1312
+ {plan.name}
1313
+ </Heading>
1314
+ <p className={classNames("hraness-marketing-plan__price")}>
1315
+ <strong className={classNames("hraness-marketing-plan__value")}>{plan.price}</strong>
1316
+ {plan.period === undefined ? null : <span className={classNames("hraness-marketing-plan__period")}>{plan.period}</span>}
1317
+ </p>
1318
+ {plan.summary === undefined ? null : <p className={classNames("hraness-marketing-plan__summary")}>{plan.summary}</p>}
1319
+ {plan.features.length === 0
1320
+ ? null
1321
+ : (
1322
+ <ul className={classNames("hraness-marketing-plan__features")}>
1323
+ {plan.features.map((feature) => <li className={classNames("hraness-marketing-plan__feature")} key={feature}>{feature}</li>)}
1324
+ </ul>
1325
+ )}
1326
+ {plan.action === undefined
1327
+ ? null
1328
+ : (
1329
+ <a
1330
+ className={classNames("hraness-marketing-action", undefined, `plan-${plan.action.emphasis ?? plan.emphasis ?? "secondary"}`)}
1331
+ data-emphasis={plan.action.emphasis ?? plan.emphasis ?? "secondary"}
1332
+ data-foil={(plan.action.emphasis ?? plan.emphasis ?? "secondary") === "primary" ? "" : undefined}
1333
+ href={plan.action.href}
1334
+ >
1335
+ {plan.action.label}
1336
+ </a>
1337
+ )}
1338
+ {plan.note === undefined ? null : <p className={classNames("hraness-marketing-plan__note")}>{plan.note}</p>}
1339
+ </li>
1340
+ ))}
1341
+ </ul>
1342
+ </section>
1343
+ );
1344
+ }
1345
+
1346
+ export interface MarketingQuestion {
1347
+ readonly answer: ReactNode;
1348
+ readonly question: string;
1349
+ }
1350
+
1351
+ export function MarketingQuestionList({
1352
+ className,
1353
+ heading,
1354
+ headingId,
1355
+ headingLevel = 2,
1356
+ id,
1357
+ label,
1358
+ questions,
1359
+ summary,
1360
+ }: Readonly<{
1361
+ className?: string;
1362
+ heading: string;
1363
+ headingId: string;
1364
+ headingLevel?: MarketingHeadingLevel;
1365
+ id?: string;
1366
+ label?: string;
1367
+ questions: readonly MarketingQuestion[];
1368
+ summary?: string;
1369
+ }>) {
1370
+ return (
1371
+ <section
1372
+ aria-labelledby={headingId}
1373
+ className={classNames("hraness-marketing-questions", className)}
1374
+ data-hraness-marketing="questions"
1375
+ id={id}
1376
+ >
1377
+ <MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="questions" />
1378
+ <div className={classNames("hraness-marketing-question-list")}>
1379
+ {questions.map((question, index) => (
1380
+ <details className={classNames("hraness-marketing-question", undefined, index === questions.length - 1 ? "last" : "default")} key={question.question}>
1381
+ <summary className={classNames("hraness-marketing-question__summary")}>{question.question}</summary>
1382
+ <div className={classNames("hraness-marketing-question__answer")}>{question.answer}</div>
1383
+ </details>
1384
+ ))}
1385
+ </div>
1386
+ </section>
1387
+ );
1388
+ }
1389
+
1390
+ /**
1391
+ * The person behind the product, in plain words. `children` is the bio as one
1392
+ * or more paragraphs.
1393
+ */
1394
+ export function MarketingMaker({
1395
+ children,
1396
+ className,
1397
+ heading,
1398
+ headingId,
1399
+ headingLevel = 2,
1400
+ id,
1401
+ label,
1402
+ linkClassName,
1403
+ links = [],
1404
+ portrait,
1405
+ }: Readonly<{
1406
+ children: ReactNode;
1407
+ className?: string;
1408
+ heading: string;
1409
+ headingId: string;
1410
+ headingLevel?: MarketingHeadingLevel;
1411
+ id?: string;
1412
+ label?: string;
1413
+ /** Added only to listed links, never to caller-owned biography content. */
1414
+ linkClassName?: string;
1415
+ links?: readonly MarketingLink[];
1416
+ portrait?: ReactNode;
1417
+ }>) {
1418
+ return (
1419
+ <section
1420
+ aria-labelledby={headingId}
1421
+ className={classNames("hraness-marketing-maker", className)}
1422
+ data-hraness-marketing="maker"
1423
+ id={id}
1424
+ >
1425
+ <header className={classNames("hraness-marketing-maker__header")}>
1426
+ {portrait === undefined ? null : <div className={classNames("hraness-marketing-maker__portrait")}>{portrait}</div>}
1427
+ {label === undefined || label === "" ? null : <p className={classNames("hraness-marketing-maker__label")}>{label}</p>}
1428
+ <Heading className={classNames("hraness-marketing-maker__heading")} id={headingId} level={headingLevel}>
1429
+ {heading}
1430
+ </Heading>
1431
+ </header>
1432
+ <div className={classNames("hraness-marketing-maker__body")}>
1433
+ {children}
1434
+ {links.length === 0
1435
+ ? null
1436
+ : (
1437
+ <ul className={classNames("hraness-marketing-maker__links")}>
1438
+ {links.map((link) => (
1439
+ <li key={`${link.href}-${link.label}`}>
1440
+ <a className={linkClassName} href={link.href}>{link.label}</a>
1441
+ </li>
1442
+ ))}
1443
+ </ul>
1444
+ )}
1445
+ </div>
1446
+ </section>
1447
+ );
1448
+ }
1449
+
1450
+ export interface MarketingRelatedProduct {
1451
+ /** Custom artwork in place of `mark`, drawn in the same 44px slot. */
1452
+ readonly art?: ReactNode;
1453
+ readonly href: string;
1454
+ /** Transparent product mark, such as a portfolio item's `mark`; drawn as a foil mark before the name. */
1455
+ readonly mark?: string;
1456
+ readonly name: string;
1457
+ /**
1458
+ * @deprecated Cards show `role` only. Kept so existing item lists still
1459
+ * type-check; the relation sentence is not rendered.
1460
+ */
1461
+ readonly relationship?: ReactNode;
1462
+ /** The product's one-line description, shown under its name. */
1463
+ readonly role: string;
1464
+ }
1465
+
1466
+ export interface MarketingRelatedGroup {
1467
+ /** Heading naming this tier of siblings, rendered one level below the section heading. */
1468
+ readonly heading: string;
1469
+ readonly headingId: string;
1470
+ readonly items: readonly MarketingRelatedProduct[];
1471
+ readonly summary?: string;
1472
+ }
1473
+
1474
+ function MarketingRelatedCards({
1475
+ items,
1476
+ level,
1477
+ }: Readonly<{
1478
+ items: readonly MarketingRelatedProduct[];
1479
+ level: MarketingHeadingLevel;
1480
+ }>) {
1481
+ return (
1482
+ <>
1483
+ {items.map((item) => (
1484
+ <a className={classNames("hraness-marketing-related__card")} data-foil="" data-hraness-marketing="card" href={item.href} key={item.name}>
1485
+ {isPresentNode(item.art) || (item.mark !== undefined && item.mark !== "")
1486
+ ? (
1487
+ <span aria-hidden="true" className={classNames("hraness-marketing-related__card-mark")}>
1488
+ {isPresentNode(item.art) ? item.art : <FoilMark size={44} src={item.mark ?? ""} />}
1489
+ </span>
1490
+ )
1491
+ : null}
1492
+ <span className={classNames("hraness-marketing-related__card-text")}>
1493
+ <Heading className={classNames("hraness-marketing-related__card-name")} level={level}>{item.name}</Heading>
1494
+ <span className={classNames("hraness-marketing-related__card-role")}>{item.role}</span>
1495
+ </span>
1496
+ </a>
1497
+ ))}
1498
+ </>
1499
+ );
1500
+ }
1501
+
1502
+ type MarketingRelatedBody =
1503
+ | Readonly<{ groups: readonly MarketingRelatedGroup[]; items?: undefined }>
1504
+ | Readonly<{ groups?: undefined; items: readonly MarketingRelatedProduct[] }>;
1505
+
1506
+ /** Linked cards for sibling products: each shows the product's mark, name, and one-line description. */
1507
+ export function MarketingRelated({
1508
+ className,
1509
+ heading,
1510
+ headingId,
1511
+ headingLevel = 2,
1512
+ id,
1513
+ items,
1514
+ groups,
1515
+ label,
1516
+ summary,
1517
+ }: Readonly<{
1518
+ className?: string;
1519
+ heading: string;
1520
+ headingId: string;
1521
+ headingLevel?: MarketingHeadingLevel;
1522
+ id?: string;
1523
+ label?: string;
1524
+ summary?: string;
1525
+ }> &
1526
+ MarketingRelatedBody) {
1527
+ return (
1528
+ <section
1529
+ aria-labelledby={headingId}
1530
+ className={classNames("hraness-marketing-related", className)}
1531
+ data-hraness-marketing="related"
1532
+ id={id}
1533
+ >
1534
+ <MarketingCollectionHeader
1535
+ heading={heading}
1536
+ headingId={headingId}
1537
+ headingLevel={headingLevel}
1538
+ label={label}
1539
+ prefix="related"
1540
+ summary={summary}
1541
+ />
1542
+ {groups === undefined
1543
+ ? (
1544
+ <MarketingCardRow>
1545
+ <MarketingRelatedCards items={items} level={childHeadingLevel(headingLevel)} />
1546
+ </MarketingCardRow>
1547
+ )
1548
+ : groups.map((group) => (
1549
+ <div className={classNames("hraness-marketing-related__group")} key={group.headingId}>
1550
+ <div className={classNames("hraness-marketing-related__group-header")}>
1551
+ <Heading
1552
+ className={classNames("hraness-marketing-related__group-heading")}
1553
+ id={group.headingId}
1554
+ level={childHeadingLevel(headingLevel)}
1555
+ >
1556
+ {group.heading}
1557
+ </Heading>
1558
+ {group.summary === undefined
1559
+ ? null
1560
+ : <p className={classNames("hraness-marketing-related__group-summary")}>{group.summary}</p>}
1561
+ </div>
1562
+ <MarketingCardRow ariaLabel={group.heading}>
1563
+ <MarketingRelatedCards items={group.items} level={childHeadingLevel(childHeadingLevel(headingLevel))} />
1564
+ </MarketingCardRow>
1565
+ </div>
1566
+ ))}
1567
+ </section>
1568
+ );
1569
+ }
1570
+
1571
+ export function MarketingCallToAction({
1572
+ actions,
1573
+ className,
1574
+ eyebrow,
1575
+ footnote,
1576
+ heading,
1577
+ headingId,
1578
+ headingLevel = 2,
1579
+ id,
1580
+ summary,
1581
+ tone = "paper",
1582
+ }: Readonly<{
1583
+ actions: readonly MarketingAction[];
1584
+ className?: string;
1585
+ eyebrow?: string;
1586
+ footnote?: string;
1587
+ heading: string;
1588
+ headingId: string;
1589
+ headingLevel?: MarketingHeadingLevel;
1590
+ id?: string;
1591
+ summary?: string;
1592
+ tone?: MarketingTone;
1593
+ }>) {
1594
+ return (
1595
+ <section
1596
+ aria-labelledby={headingId}
1597
+ className={classNames("hraness-marketing-cta", className, tone === "accent" ? "accent" : "default")}
1598
+ data-hraness-marketing="cta"
1599
+ data-tone={tone}
1600
+ id={id}
1601
+ >
1602
+ {eyebrow === undefined || eyebrow === "" ? null : <p className={classNames("hraness-marketing-cta__eyebrow")}>{eyebrow}</p>}
1603
+ <Heading className={classNames("hraness-marketing-cta__heading")} id={headingId} level={headingLevel}>
1604
+ {heading}
1605
+ </Heading>
1606
+ {summary === undefined ? null : <p className={classNames("hraness-marketing-cta__summary")}>{summary}</p>}
1607
+ <MarketingActions actions={actions} className={classNames("hraness-marketing-cta__actions")} context="cta" tone={tone} />
1608
+ {footnote === undefined ? null : <p className={classNames("hraness-marketing-cta__footnote")}>{footnote}</p>}
1609
+ </section>
1610
+ );
1611
+ }