@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,415 @@
1
+ "use client";
2
+
3
+ import {
4
+ type CSSProperties,
5
+ type KeyboardEvent,
6
+ type ReactNode,
7
+ useId,
8
+ useLayoutEffect,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+
13
+ import { joinMockupClasses, type MockupTheme } from "./core.js";
14
+
15
+ /*
16
+ * Interactive shells for the code-built mockups. They hold selection state
17
+ * and nothing else: every surface is a render function over plain props, so
18
+ * the same surface renders statically in a film or a server page.
19
+ */
20
+
21
+ // React warns about useLayoutEffect during server rendering; these shells
22
+ // measure only in the browser.
23
+ const useIsomorphicLayoutEffect: typeof useLayoutEffect = typeof document === "undefined" ? () => undefined : useLayoutEffect;
24
+
25
+ function assertUniqueIds(items: readonly Readonly<{ id: string }>[], what: string): void {
26
+ if (items.length === 0) throw new RangeError(`${what} needs at least one entry.`);
27
+ const seen = new Set<string>();
28
+ for (const item of items) {
29
+ if (typeof item.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/u.test(item.id)) {
30
+ throw new TypeError(`${what} ids must be lowercase letters, digits, and dashes: ${JSON.stringify(item.id)}`);
31
+ }
32
+ if (seen.has(item.id)) throw new RangeError(`${what} ids must be unique: ${item.id}`);
33
+ seen.add(item.id);
34
+ }
35
+ }
36
+
37
+ function itemAt<T>(items: readonly T[], index: number, what: string): T {
38
+ const item = items[index];
39
+ if (item === undefined) throw new RangeError(`${what} has no entry at ${String(index)}.`);
40
+ return item;
41
+ }
42
+
43
+ function assertCaption(caption: string, component: string): void {
44
+ if (typeof caption !== "string" || caption.trim() === "") {
45
+ throw new TypeError(`${component} needs a caption that says the picture is an illustration.`);
46
+ }
47
+ }
48
+
49
+ /** Arrow, Home, and End move between tabs, as in the WAI-ARIA tabs pattern. */
50
+ function nextTabIndex(key: string, index: number, count: number): number | undefined {
51
+ if (key === "ArrowRight" || key === "ArrowDown") return (index + 1) % count;
52
+ if (key === "ArrowLeft" || key === "ArrowUp") return (index - 1 + count) % count;
53
+ if (key === "Home") return 0;
54
+ if (key === "End") return count - 1;
55
+ return undefined;
56
+ }
57
+
58
+ /* ------------------------------------------------------------------ */
59
+ /* FitToWidth */
60
+ /* ------------------------------------------------------------------ */
61
+
62
+ /**
63
+ * Lays its child out at no less than `minWidth` pixels and scales it down to
64
+ * fit a narrower container, so a desktop mockup stays whole on a phone
65
+ * instead of reflowing into something no product looks like.
66
+ */
67
+ export function FitToWidth({ children, className, minWidth = 400 }: Readonly<{ children: ReactNode; minWidth?: number; className?: string }>) {
68
+ if (!(minWidth > 0)) throw new RangeError("FitToWidth minWidth must be positive.");
69
+ const outer = useRef<HTMLDivElement>(null);
70
+ const inner = useRef<HTMLDivElement>(null);
71
+ const [fit, setFit] = useState<Readonly<{ width: number; scale: number; height: number }> | null>(null);
72
+
73
+ useIsomorphicLayoutEffect(() => {
74
+ const stage = outer.current;
75
+ const content = inner.current;
76
+ if (stage === null || content === null || typeof ResizeObserver === "undefined") return undefined;
77
+ const measure = () => {
78
+ const available = stage.clientWidth;
79
+ if (available <= 0) return;
80
+ const width = Math.max(available, minWidth);
81
+ const scale = available / width;
82
+ const height = content.offsetHeight * scale;
83
+ setFit((current) =>
84
+ current !== null && current.width === width && current.scale === scale && current.height === height ? current : { height, scale, width });
85
+ };
86
+ measure();
87
+ const observer = new ResizeObserver(measure);
88
+ observer.observe(stage);
89
+ observer.observe(content);
90
+ return () => observer.disconnect();
91
+ }, [minWidth]);
92
+
93
+ const scaled = fit !== null && fit.scale < 1;
94
+ return (
95
+ <div className={joinMockupClasses("hkm-fit", className)} ref={outer} style={scaled ? { height: fit.height } : undefined}>
96
+ <div
97
+ className="hkm-fit-inner"
98
+ data-hkm-scaled={scaled ? "" : undefined}
99
+ ref={inner}
100
+ style={scaled ? { transform: `scale(${fit.scale})`, width: fit.width } : undefined}
101
+ >
102
+ {children}
103
+ </div>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ /* ------------------------------------------------------------------ */
109
+ /* ModeShowcase */
110
+ /* ------------------------------------------------------------------ */
111
+
112
+ /** One choice on a showcase axis. `hint` says in one sentence what the choice does. */
113
+ export type ShowcaseChoice<I extends string> = Readonly<{ id: I; label: string; hint: string }>;
114
+
115
+ /** What a surface's render function receives. */
116
+ export type ShowcaseState<M extends string, O extends string = string> = Readonly<{
117
+ mode: M;
118
+ /** The mode before the last change; equals `mode` until the first change. */
119
+ previousMode: M;
120
+ option: O | undefined;
121
+ /** False on first paint, true after the first user change, so the first paint never animates. */
122
+ animated: boolean;
123
+ theme: MockupTheme | undefined;
124
+ }>;
125
+
126
+ /** One tab in a showcase. `render` returns a mockup from `@hraness/design-kit/mockups` or a product skin. */
127
+ export type ShowcaseSurface<S extends string, M extends string, O extends string = string> = Readonly<{
128
+ id: S;
129
+ label: string;
130
+ render: (state: ShowcaseState<M, O>) => ReactNode;
131
+ }>;
132
+
133
+ export type ModeShowcaseProps<S extends string, M extends string, O extends string = string> = Readonly<{
134
+ surfaces: readonly ShowcaseSurface<S, M, O>[];
135
+ modes: readonly ShowcaseChoice<M>[];
136
+ /** Label before the mode buttons, such as "Show AI text as". */
137
+ modeLabel?: string;
138
+ /** An optional second axis, such as sensitivity. */
139
+ options?: readonly ShowcaseChoice<O>[];
140
+ optionLabel?: string;
141
+ /** Modes in which the option axis does nothing; its buttons are disabled then. */
142
+ optionInactiveModes?: readonly M[];
143
+ initial?: Readonly<{ surface?: S; mode?: M; option?: O }>;
144
+ /** A live line under the stage that says what the current state shows. */
145
+ status?: (state: Readonly<{ surface: S; mode: M; option: O | undefined }>) => ReactNode;
146
+ /** Required. Say it is an illustration, such as "Illustration. Names and text are made up." */
147
+ caption: string;
148
+ /** Page-window height in CSS pixels at full scale. */
149
+ height?: number;
150
+ /** Narrowest layout width before the stage scales down. */
151
+ minWidth?: number;
152
+ theme?: MockupTheme;
153
+ /** Accessible name for the whole figure. Defaults to "Illustration of <surface>". */
154
+ label?: (surface: ShowcaseSurface<S, M, O>) => string;
155
+ className?: string;
156
+ }>;
157
+
158
+ /**
159
+ * An interactive illustration: tabs pick a surface, a segmented control picks
160
+ * a mode, and an optional second control picks an option. Tabs follow the
161
+ * WAI-ARIA tabs pattern with automatic activation; mode and option buttons are
162
+ * toggle buttons in labelled groups.
163
+ */
164
+ export function ModeShowcase<S extends string, M extends string, O extends string = string>({
165
+ caption,
166
+ className,
167
+ height = 440,
168
+ initial,
169
+ label,
170
+ minWidth = 400,
171
+ modeLabel = "Mode",
172
+ modes,
173
+ optionInactiveModes = [],
174
+ optionLabel = "Option",
175
+ options,
176
+ status,
177
+ surfaces,
178
+ theme,
179
+ }: ModeShowcaseProps<S, M, O>) {
180
+ assertUniqueIds(surfaces, "ModeShowcase surface");
181
+ assertUniqueIds(modes, "ModeShowcase mode");
182
+ if (options !== undefined) assertUniqueIds(options, "ModeShowcase option");
183
+ assertCaption(caption, "ModeShowcase");
184
+ if (!(height > 0)) throw new RangeError("ModeShowcase height must be positive.");
185
+
186
+ const id = useId();
187
+ const firstSurface = surfaces.find((surface) => surface.id === initial?.surface) ?? itemAt(surfaces, 0, "ModeShowcase surfaces");
188
+ const firstMode = modes.find((mode) => mode.id === initial?.mode) ?? itemAt(modes, 0, "ModeShowcase modes");
189
+ const firstOption = options === undefined ? undefined : (options.find((option) => option.id === initial?.option) ?? itemAt(options, 0, "ModeShowcase options"));
190
+ const [surfaceId, setSurfaceId] = useState<S>(firstSurface.id);
191
+ const [mode, setMode] = useState<M>(firstMode.id);
192
+ const [previousMode, setPreviousMode] = useState<M>(firstMode.id);
193
+ const [option, setOption] = useState<O | undefined>(firstOption?.id);
194
+ const [animated, setAnimated] = useState(false);
195
+ const tabs = useRef(new Map<S, HTMLButtonElement>());
196
+
197
+ const surface = surfaces.find((entry) => entry.id === surfaceId) ?? itemAt(surfaces, 0, "ModeShowcase surfaces");
198
+ const modeChoice = modes.find((entry) => entry.id === mode) ?? itemAt(modes, 0, "ModeShowcase modes");
199
+ const optionChoice = options?.find((entry) => entry.id === option);
200
+ const optionInactive = optionInactiveModes.includes(mode);
201
+
202
+ const chooseMode = (next: M) => {
203
+ if (next === mode) return;
204
+ setPreviousMode(mode);
205
+ setMode(next);
206
+ setAnimated(true);
207
+ };
208
+ const chooseOption = (next: O) => {
209
+ if (next === option) return;
210
+ setOption(next);
211
+ setAnimated(true);
212
+ };
213
+ const onTabKey = (event: KeyboardEvent<HTMLButtonElement>) => {
214
+ const index = surfaces.findIndex((entry) => entry.id === surface.id);
215
+ const next = nextTabIndex(event.key, index, surfaces.length);
216
+ if (next === undefined) return;
217
+ event.preventDefault();
218
+ const target = itemAt(surfaces, next, "ModeShowcase surfaces").id;
219
+ setSurfaceId(target);
220
+ tabs.current.get(target)?.focus();
221
+ };
222
+
223
+ const statusNode = status?.({ mode, option, surface: surface.id });
224
+ return (
225
+ <figure
226
+ aria-label={label?.(surface) ?? `Illustration of ${surface.label.toLowerCase()}`}
227
+ className={joinMockupClasses("hkm-showcase", className)}
228
+ data-hkm-theme={theme}
229
+ data-nosnippet=""
230
+ >
231
+ <div className="hkm-showcase-controls">
232
+ {surfaces.length > 1 ? (
233
+ <div aria-label="Surface" className="hkm-tabs" role="tablist">
234
+ {surfaces.map((entry) => (
235
+ <button
236
+ aria-controls={`${id}-panel`}
237
+ aria-selected={entry.id === surface.id}
238
+ className="hkm-tab"
239
+ id={`${id}-tab-${entry.id}`}
240
+ key={entry.id}
241
+ onClick={() => setSurfaceId(entry.id)}
242
+ onKeyDown={onTabKey}
243
+ ref={(element) => {
244
+ if (element === null) tabs.current.delete(entry.id);
245
+ else tabs.current.set(entry.id, element);
246
+ }}
247
+ role="tab"
248
+ tabIndex={entry.id === surface.id ? 0 : -1}
249
+ type="button"
250
+ >
251
+ {entry.label}
252
+ </button>
253
+ ))}
254
+ </div>
255
+ ) : null}
256
+ <div className="hkm-showcase-settings">
257
+ <div className="hkm-showcase-row">
258
+ <span className="hkm-showcase-label" id={`${id}-mode`}>{modeLabel}</span>
259
+ <div aria-labelledby={`${id}-mode`} className="hkm-segmented" role="group">
260
+ {modes.map((entry) => (
261
+ <button aria-pressed={entry.id === mode} key={entry.id} onClick={() => chooseMode(entry.id)} type="button">
262
+ {entry.label}
263
+ </button>
264
+ ))}
265
+ </div>
266
+ </div>
267
+ {options === undefined ? null : (
268
+ <div className="hkm-showcase-row">
269
+ <span className="hkm-showcase-label" id={`${id}-option`}>{optionLabel}</span>
270
+ <div aria-labelledby={`${id}-option`} className="hkm-segmented" role="group">
271
+ {options.map((entry) => (
272
+ <button aria-pressed={entry.id === option} disabled={optionInactive} key={entry.id} onClick={() => chooseOption(entry.id)} type="button">
273
+ {entry.label}
274
+ </button>
275
+ ))}
276
+ </div>
277
+ </div>
278
+ )}
279
+ </div>
280
+ <p className="hkm-showcase-hint">
281
+ {modeChoice.hint}
282
+ {optionChoice === undefined || optionInactive ? null : ` ${optionChoice.hint}`}
283
+ </p>
284
+ </div>
285
+ <div
286
+ aria-labelledby={surfaces.length > 1 ? `${id}-tab-${surface.id}` : undefined}
287
+ className="hkm-showcase-stage"
288
+ data-hkm-animated={animated ? "" : undefined}
289
+ data-hkm-from={animated ? previousMode : undefined}
290
+ id={`${id}-panel`}
291
+ role={surfaces.length > 1 ? "tabpanel" : undefined}
292
+ style={{ "--hkm-page-height": `${height}px` } as CSSProperties}
293
+ >
294
+ <FitToWidth minWidth={minWidth}>
295
+ {surface.render({ animated, mode, option, previousMode, theme })}
296
+ </FitToWidth>
297
+ </div>
298
+ <figcaption className="hkm-showcase-caption">
299
+ {statusNode === undefined ? null : <span aria-live="polite" className="hkm-showcase-status">{statusNode}</span>}
300
+ <span>{caption}</span>
301
+ </figcaption>
302
+ </figure>
303
+ );
304
+ }
305
+
306
+ /* ------------------------------------------------------------------ */
307
+ /* StepThrough */
308
+ /* ------------------------------------------------------------------ */
309
+
310
+ /** One step in a step-through. `render` gets whether transitions may run. */
311
+ export type ThroughStep = Readonly<{
312
+ id: string;
313
+ label: string;
314
+ /** One sentence under the stage that says what this step shows. */
315
+ hint?: string;
316
+ render: (state: Readonly<{ animated: boolean; theme: MockupTheme | undefined }>) => ReactNode;
317
+ }>;
318
+
319
+ /**
320
+ * Walks a flow one step at a time: numbered step tabs, Back and Next buttons,
321
+ * and a live hint. The keyboard model matches `ModeShowcase`.
322
+ */
323
+ export function StepThrough({
324
+ caption,
325
+ className,
326
+ initial,
327
+ label = "Steps",
328
+ minWidth = 400,
329
+ steps,
330
+ theme,
331
+ }: Readonly<{
332
+ steps: readonly ThroughStep[];
333
+ caption: string;
334
+ initial?: string;
335
+ label?: string;
336
+ minWidth?: number;
337
+ theme?: MockupTheme;
338
+ className?: string;
339
+ }>) {
340
+ assertUniqueIds(steps, "StepThrough step");
341
+ assertCaption(caption, "StepThrough");
342
+ const id = useId();
343
+ const [index, setIndex] = useState(() => Math.max(0, steps.findIndex((step) => step.id === initial)));
344
+ const [animated, setAnimated] = useState(false);
345
+ const tabs = useRef(new Map<number, HTMLButtonElement>());
346
+ const current = Math.min(index, steps.length - 1);
347
+ const step = itemAt(steps, current, "StepThrough steps");
348
+
349
+ const go = (next: number, focus = false) => {
350
+ if (next < 0 || next >= steps.length || next === current) return;
351
+ setIndex(next);
352
+ setAnimated(true);
353
+ if (focus) tabs.current.get(next)?.focus();
354
+ };
355
+ const onTabKey = (event: KeyboardEvent<HTMLButtonElement>) => {
356
+ const next = nextTabIndex(event.key, current, steps.length);
357
+ if (next === undefined) return;
358
+ event.preventDefault();
359
+ go(next, true);
360
+ };
361
+
362
+ return (
363
+ <figure aria-label={`Illustration: ${label.toLowerCase()}`} className={joinMockupClasses("hkm-showcase", "hkm-steps", className)} data-hkm-theme={theme} data-nosnippet="">
364
+ <div className="hkm-showcase-controls">
365
+ <div aria-label={label} className="hkm-tabs hkm-step-tabs" role="tablist">
366
+ {steps.map((entry, position) => (
367
+ <button
368
+ aria-controls={`${id}-panel`}
369
+ aria-selected={position === current}
370
+ className="hkm-tab"
371
+ data-hkm-done={position < current ? "" : undefined}
372
+ id={`${id}-tab-${entry.id}`}
373
+ key={entry.id}
374
+ onClick={() => go(position)}
375
+ onKeyDown={onTabKey}
376
+ ref={(element) => {
377
+ if (element === null) tabs.current.delete(position);
378
+ else tabs.current.set(position, element);
379
+ }}
380
+ role="tab"
381
+ tabIndex={position === current ? 0 : -1}
382
+ type="button"
383
+ >
384
+ <span aria-hidden="true" className="hkm-step-number">{position + 1}</span>
385
+ {entry.label}
386
+ </button>
387
+ ))}
388
+ </div>
389
+ </div>
390
+ <div
391
+ aria-labelledby={`${id}-tab-${step.id}`}
392
+ className="hkm-showcase-stage"
393
+ data-hkm-animated={animated ? "" : undefined}
394
+ id={`${id}-panel`}
395
+ role="tabpanel"
396
+ >
397
+ <FitToWidth minWidth={minWidth}>{step.render({ animated, theme })}</FitToWidth>
398
+ </div>
399
+ <div className="hkm-step-nav">
400
+ <button className="hkm-step-button" disabled={current === 0} onClick={() => go(current - 1)} type="button">
401
+ Back
402
+ </button>
403
+ <span aria-live="polite" className="hkm-showcase-status">
404
+ Step {current + 1} of {steps.length}{step.hint === undefined ? "" : `. ${step.hint}`}
405
+ </span>
406
+ <button className="hkm-step-button" data-hkm-primary="" disabled={current === steps.length - 1} onClick={() => go(current + 1)} type="button">
407
+ Next
408
+ </button>
409
+ </div>
410
+ <figcaption className="hkm-showcase-caption">
411
+ <span>{caption}</span>
412
+ </figcaption>
413
+ </figure>
414
+ );
415
+ }