@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,228 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, type RefObject } from "react";
4
+
5
+ const interactiveTargetSelector = [
6
+ "a[href]",
7
+ "area[href]",
8
+ "button",
9
+ "input",
10
+ "select",
11
+ "summary",
12
+ "textarea",
13
+ "[contenteditable]:not([contenteditable='false'])",
14
+ "[role='button']",
15
+ "[role='checkbox']",
16
+ "[role='combobox']",
17
+ "[role='gridcell']",
18
+ "[role='link']",
19
+ "[role='menuitem']",
20
+ "[role='option']",
21
+ "[role='radio']",
22
+ "[role='slider']",
23
+ "[role='spinbutton']",
24
+ "[role='switch']",
25
+ "[role='tab']",
26
+ "[role='textbox']",
27
+ "[tabindex]:not([tabindex='-1'])",
28
+ ].join(",");
29
+
30
+ const textEntryTargetSelector = [
31
+ "input:not([type='button']):not([type='checkbox']):not([type='color']):not([type='file']):not([type='hidden']):not([type='image']):not([type='radio']):not([type='range']):not([type='reset']):not([type='submit'])",
32
+ "select",
33
+ "textarea",
34
+ "[contenteditable]:not([contenteditable='false'])",
35
+ "[role='combobox']",
36
+ "[role='textbox']",
37
+ ].join(",");
38
+
39
+ type ClosestEventTarget = EventTarget & {
40
+ closest(selectors: string): Element | null;
41
+ };
42
+
43
+ function hasClosest(target: EventTarget | null): target is ClosestEventTarget {
44
+ return target !== null && "closest" in target && typeof target.closest === "function";
45
+ }
46
+
47
+ export function isKeyboardInteractionTarget(target: EventTarget | null): boolean {
48
+ return hasClosest(target) && target.closest(interactiveTargetSelector) !== null;
49
+ }
50
+
51
+ export function isKeyboardTextEntryTarget(target: EventTarget | null): boolean {
52
+ return hasClosest(target) && target.closest(textEntryTargetSelector) !== null;
53
+ }
54
+
55
+ export type KeyboardShortcutEvent = Pick<
56
+ KeyboardEvent,
57
+ | "altKey"
58
+ | "ctrlKey"
59
+ | "defaultPrevented"
60
+ | "isComposing"
61
+ | "key"
62
+ | "metaKey"
63
+ | "repeat"
64
+ | "shiftKey"
65
+ >;
66
+
67
+ export interface KeyboardShortcutDefinition {
68
+ readonly allowRepeat?: boolean;
69
+ readonly allowWhenEditable?: boolean;
70
+ readonly allowWhenInteractive?: boolean;
71
+ /**
72
+ * Grants a matched shortcut to one explicitly recognized, non-editable
73
+ * interactive target without opening the shortcut to every control.
74
+ */
75
+ readonly allowWhenInteractiveTarget?: (
76
+ target: EventTarget | null,
77
+ ) => boolean;
78
+ readonly altKey?: boolean;
79
+ readonly ctrlKey?: boolean;
80
+ readonly id: string;
81
+ readonly isDisabled?: boolean;
82
+ readonly key: string;
83
+ readonly metaKey?: boolean;
84
+ readonly shiftKey?: boolean;
85
+ }
86
+
87
+ export interface KeyboardShortcutBinding extends KeyboardShortcutDefinition {
88
+ readonly onAction: (event: KeyboardEvent) => void;
89
+ }
90
+
91
+ export type KeyboardShortcutDecision =
92
+ | {
93
+ readonly bindingId: string;
94
+ readonly bindingIndex: number;
95
+ readonly kind: "handle";
96
+ }
97
+ | {
98
+ readonly kind: "ignore";
99
+ readonly reason:
100
+ | "composing"
101
+ | "default-prevented"
102
+ | "disabled"
103
+ | "editable-target"
104
+ | "interactive-target"
105
+ | "no-match"
106
+ | "repeat";
107
+ };
108
+
109
+ export interface KeyboardShortcutContext {
110
+ readonly isDisabled?: boolean;
111
+ readonly isEditableTarget?: boolean;
112
+ readonly isInteractiveTarget?: boolean;
113
+ readonly target?: EventTarget | null;
114
+ }
115
+
116
+ function normalizedKey(key: string): string {
117
+ switch (key) {
118
+ case "Esc":
119
+ return "Escape";
120
+ case "Left":
121
+ return "ArrowLeft";
122
+ case "Right":
123
+ return "ArrowRight";
124
+ case "Up":
125
+ return "ArrowUp";
126
+ case "Down":
127
+ return "ArrowDown";
128
+ case "Space":
129
+ case "Spacebar":
130
+ return " ";
131
+ default:
132
+ return key.length === 1 ? key.toLocaleLowerCase("en-US") : key;
133
+ }
134
+ }
135
+
136
+ export function matchesKeyboardShortcut(
137
+ event: KeyboardShortcutEvent,
138
+ shortcut: KeyboardShortcutDefinition,
139
+ ): boolean {
140
+ return (
141
+ normalizedKey(event.key) === normalizedKey(shortcut.key) &&
142
+ event.altKey === (shortcut.altKey ?? false) &&
143
+ event.ctrlKey === (shortcut.ctrlKey ?? false) &&
144
+ event.metaKey === (shortcut.metaKey ?? false) &&
145
+ event.shiftKey === (shortcut.shiftKey ?? false)
146
+ );
147
+ }
148
+
149
+ export function decideKeyboardShortcut(
150
+ shortcuts: readonly KeyboardShortcutDefinition[],
151
+ event: KeyboardShortcutEvent,
152
+ context: KeyboardShortcutContext = {},
153
+ ): KeyboardShortcutDecision {
154
+ if (context.isDisabled === true) return { kind: "ignore", reason: "disabled" };
155
+ if (event.defaultPrevented) return { kind: "ignore", reason: "default-prevented" };
156
+ if (event.isComposing) return { kind: "ignore", reason: "composing" };
157
+
158
+ let suppressedReason: Extract<KeyboardShortcutDecision, { readonly kind: "ignore" }>["reason"] | null = null;
159
+ for (const [bindingIndex, shortcut] of shortcuts.entries()) {
160
+ if (shortcut.isDisabled === true || !matchesKeyboardShortcut(event, shortcut)) continue;
161
+ if (event.repeat && shortcut.allowRepeat !== true) {
162
+ suppressedReason ??= "repeat";
163
+ continue;
164
+ }
165
+ if (
166
+ context.isEditableTarget === true &&
167
+ shortcut.allowWhenEditable !== true
168
+ ) {
169
+ suppressedReason ??= "editable-target";
170
+ continue;
171
+ }
172
+ if (
173
+ context.isInteractiveTarget === true &&
174
+ context.isEditableTarget !== true &&
175
+ shortcut.allowWhenInteractive !== true &&
176
+ shortcut.allowWhenInteractiveTarget?.(context.target ?? null) !== true
177
+ ) {
178
+ suppressedReason ??= "interactive-target";
179
+ continue;
180
+ }
181
+ return { bindingId: shortcut.id, bindingIndex, kind: "handle" };
182
+ }
183
+
184
+ return { kind: "ignore", reason: suppressedReason ?? "no-match" };
185
+ }
186
+
187
+ export interface UseKeyboardShortcutsOptions {
188
+ readonly isDisabled?: boolean;
189
+ readonly scopeRef?: RefObject<HTMLElement | null>;
190
+ }
191
+
192
+ function isNode(target: EventTarget | null): target is Node {
193
+ return target !== null && typeof Node !== "undefined" && target instanceof Node;
194
+ }
195
+
196
+ /** Installs guarded document shortcuts without stealing keys from focused widgets by default. */
197
+ export function useKeyboardShortcuts(
198
+ bindings: readonly KeyboardShortcutBinding[],
199
+ options: UseKeyboardShortcutsOptions = {},
200
+ ): void {
201
+ const latestRef = useRef({ bindings, isDisabled: options.isDisabled ?? false });
202
+ latestRef.current = { bindings, isDisabled: options.isDisabled ?? false };
203
+ const scopeRef = options.scopeRef;
204
+
205
+ useEffect(() => {
206
+ const onKeyDown = (event: KeyboardEvent): void => {
207
+ if (scopeRef !== undefined) {
208
+ const scope = scopeRef.current;
209
+ if (scope === null || !isNode(event.target) || !scope.contains(event.target)) return;
210
+ }
211
+
212
+ const current = latestRef.current;
213
+ const decision = decideKeyboardShortcut(current.bindings, event, {
214
+ isDisabled: current.isDisabled,
215
+ isEditableTarget: isKeyboardTextEntryTarget(event.target),
216
+ isInteractiveTarget: isKeyboardInteractionTarget(event.target),
217
+ target: event.target,
218
+ });
219
+ if (decision.kind === "ignore") return;
220
+
221
+ event.preventDefault();
222
+ current.bindings[decision.bindingIndex]?.onAction(event);
223
+ };
224
+
225
+ window.addEventListener("keydown", onKeyDown);
226
+ return () => window.removeEventListener("keydown", onKeyDown);
227
+ }, [scopeRef]);
228
+ }
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import { Button, TextField } from "@hraness/ui";
4
+ import { useId, useState } from "react";
5
+
6
+ import { getDesignPaletteTheme } from "../palette-themes.js";
7
+ import { lanternControlStyles } from "./lantern-material.stylex.js";
8
+ import { SyntaxCode } from "./syntax-code.js";
9
+ import { TopBar } from "./surfaces.js";
10
+
11
+ // Patterns are retired: every accepted value paints the flat palette plane.
12
+ // The samples keep the finite vocabulary visible so existing markup can be
13
+ // checked against it, and show that only the palette changes.
14
+ const materialPatterns = [
15
+ { pattern: "cells", palette: "paper", label: "cells", description: "Retired. Renders as a flat Paper plane." },
16
+ { pattern: "weave", palette: "gruvbox", label: "weave", description: "Retired. Renders as a flat Gruvbox plane." },
17
+ { pattern: "contour", palette: "rose-pine", label: "contour", description: "Retired. Renders as a flat Rosé Pine plane." },
18
+ { pattern: "mesh", palette: "tokyo-night", label: "mesh", description: "Retired. Renders as a flat Tokyo Night plane." },
19
+ { pattern: "none", palette: "paper", label: "none", description: "The default: an uninterrupted surface." },
20
+ ] as const;
21
+
22
+ const exampleNotes = [
23
+ { title: "A place for unfinished thoughts", detail: "Personal · edited today" },
24
+ { title: "What we learned on the walk", detail: "Shared · edited yesterday" },
25
+ { title: "Ideas for a slower morning", detail: "Personal · edited Monday" },
26
+ ] as const;
27
+
28
+ function MaterialWorkspace({ mode }: Readonly<{ mode: "light" | "dark" }>) {
29
+ const id = useId();
30
+ const [query, setQuery] = useState("");
31
+ const [personalOnly, setPersonalOnly] = useState(false);
32
+ const [saved, setSaved] = useState(false);
33
+ const notes = exampleNotes.filter((note) =>
34
+ note.title.toLowerCase().includes(query.toLowerCase())
35
+ && (!personalOnly || note.detail.startsWith("Personal")),
36
+ );
37
+
38
+ return (
39
+ <div
40
+ className={`design-gallery__lantern-wall hraness-material-wall ${getDesignPaletteTheme("paper", mode).className}`}
41
+ data-hraness-material="lantern"
42
+ data-hraness-theme="paper"
43
+ data-palette="paper"
44
+ data-theme={mode}
45
+ data-gallery-lantern={mode}
46
+ >
47
+ <div className="design-gallery__lantern-caption">
48
+ <h3>{mode === "light" ? "Daylight" : "Lamplight"}</h3>
49
+ <p>The same notebook in the Paper palette, light and dark.</p>
50
+ </div>
51
+ <div className="hraness-material-terminal">
52
+ <div className="hraness-material-terminal__bar">notebook · terminal</div>
53
+ <pre className="hraness-material-code" aria-label="Illustrative notebook command" tabIndex={0}><SyntaxCode code={'notebook search "a slower morning" --format markdown\n# One matching note, on your computer.'} language="shell" /></pre>
54
+ </div>
55
+ <div className="design-gallery__lantern-workspace hraness-material-pane" data-depth="raised">
56
+ <TopBar
57
+ className="hraness-material-chrome"
58
+ position="static"
59
+ surface="glass"
60
+ title="Your notebook"
61
+ actions={<span className="design-gallery__lantern-local">Local workspace</span>}
62
+ />
63
+ <div className="design-gallery__lantern-content">
64
+ <TextField
65
+ controlXstyle={lanternControlStyles.inset}
66
+ inputProps={{ id: `${id}-search` }}
67
+ label="Find a note"
68
+ onChange={setQuery}
69
+ placeholder="Search your notes"
70
+ value={query}
71
+ />
72
+ <div className="design-gallery__lantern-filter">
73
+ <Button
74
+ aria-pressed={personalOnly}
75
+ controlXstyle={[lanternControlStyles.edge, personalOnly && lanternControlStyles.selected]}
76
+ data-gallery-lantern-filter=""
77
+ onPress={() => setPersonalOnly(!personalOnly)}
78
+ size="compact"
79
+ >
80
+ Personal only
81
+ </Button>
82
+ <span aria-live="polite">{notes.length} {notes.length === 1 ? "note" : "notes"}</span>
83
+ </div>
84
+ <ul className="hraness-material-rows design-gallery__lantern-notes">
85
+ {notes.map((note) => (
86
+ <li key={note.title}>
87
+ <details className="hraness-material-disclosure">
88
+ <summary>{note.title}</summary>
89
+ <p>Illustrative note. Your words and decisions stay together here.</p>
90
+ </details>
91
+ <span>{note.detail}</span>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+ {notes.length === 0 ? <p role="status">No notes match. Try a shorter search or turn off the personal filter.</p> : null}
96
+ <div className="design-gallery__lantern-save">
97
+ <Button
98
+ controlXstyle={lanternControlStyles.edge}
99
+ onPress={() => setSaved(true)}
100
+ variant="primary"
101
+ >
102
+ Save this view
103
+ </Button>
104
+ <span aria-live="polite">{saved ? "View saved for this example." : "Example content. Nothing leaves this page."}</span>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ /** Executable reference, with product-neutral illustrative content. */
113
+ export function LanternMaterialGallery() {
114
+ const [plainSelected, setPlainSelected] = useState(true);
115
+ const [removed, setRemoved] = useState(false);
116
+ return (
117
+ <section className="design-gallery__section" id="lantern">
118
+ <h2>Lantern material</h2>
119
+ <p className="design-gallery__lantern-intro">Lantern gives reading surfaces a glowing edge and selected controls a warm fill.</p>
120
+ <div className="design-gallery__lantern-pair">
121
+ <MaterialWorkspace mode="light" />
122
+ <MaterialWorkspace mode="dark" />
123
+ </div>
124
+ <h3>Patterns render as flat palette planes</h3>
125
+ <div className="design-gallery__lantern-pair">
126
+ {materialPatterns.map(({ pattern, palette, label, description }) => (
127
+ <div
128
+ className={`design-gallery__lantern-wall hraness-material-wall ${getDesignPaletteTheme(palette, "light").className}`}
129
+ data-gallery-pattern={pattern}
130
+ data-hraness-material="lantern"
131
+ data-hraness-pattern={pattern}
132
+ data-palette={palette}
133
+ data-theme="light"
134
+ key={pattern}
135
+ >
136
+ <div className="design-gallery__lantern-caption"><h4>{label}</h4><p>{description}</p></div>
137
+ </div>
138
+ ))}
139
+ </div>
140
+ <div
141
+ className={`design-gallery__lantern-states hraness-material-pane ${getDesignPaletteTheme("paper", "light").className}`}
142
+ data-hraness-material="lantern"
143
+ data-hraness-theme="paper"
144
+ data-palette="paper"
145
+ data-theme="light"
146
+ >
147
+ <h3>Controls keep their meaning</h3>
148
+ <p>Keyboard focus, errors, and unavailable actions stay clearly marked under Lantern.</p>
149
+ <div className="design-gallery__lantern-state-actions">
150
+ <Button controlXstyle={lanternControlStyles.edge} isDisabled>Unavailable action</Button>
151
+ <Button controlXstyle={lanternControlStyles.edge} isPending>Saving changes</Button>
152
+ <Button controlXstyle={lanternControlStyles.edge} onPress={() => setRemoved(true)} variant="danger">Remove example</Button>
153
+ <button className="design-gallery__lantern-plain hraness-material-choice" aria-pressed={plainSelected} onClick={() => setPlainSelected(!plainSelected)} type="button">HTML selection</button>
154
+ </div>
155
+ <p aria-live="polite">{removed ? "Example removed for this visit." : ""}</p>
156
+ <TextField
157
+ controlXstyle={lanternControlStyles.inset}
158
+ defaultValue=""
159
+ errorMessage="Give the notebook a name before saving."
160
+ isInvalid
161
+ label="Notebook name"
162
+ />
163
+ </div>
164
+ </section>
165
+ );
166
+ }
@@ -0,0 +1,39 @@
1
+ import * as stylex from "@stylexjs/stylex";
2
+
3
+ const forcedColors = "@media (forced-colors: active)";
4
+ const disabled = ':is(:disabled, [data-disabled], [aria-disabled="true"])';
5
+ const disabledField = ':has(input:disabled, textarea:disabled)';
6
+
7
+ /** Paint adapters for the existing primitive's controlXstyle seam.
8
+ * Keep focus, geometry, error, pending and disabled behavior with @hraness/ui. */
9
+ export const lanternControlStyles = stylex.create({
10
+ edge: {
11
+ backgroundImage: {
12
+ default: "linear-gradient(to bottom, var(--hraness-material-edge), transparent 2px)",
13
+ [disabled]: "none",
14
+ [forcedColors]: "none",
15
+ },
16
+ },
17
+ inset: {
18
+ backgroundImage: {
19
+ default: "linear-gradient(to bottom, var(--hraness-material-shade), transparent 4px)",
20
+ [disabledField]: "none",
21
+ [forcedColors]: "none",
22
+ },
23
+ },
24
+ selected: {
25
+ backgroundColor: {
26
+ default: "var(--hraness-material-warm-plane)",
27
+ [forcedColors]: "Highlight",
28
+ },
29
+ backgroundImage: {
30
+ default: "linear-gradient(to top, var(--hraness-material-warm), var(--hraness-material-warm) 2px, transparent 2px)",
31
+ [disabled]: "none",
32
+ [forcedColors]: "none",
33
+ },
34
+ color: {
35
+ default: "var(--hraness-material-ink)",
36
+ [forcedColors]: "HighlightText",
37
+ },
38
+ },
39
+ });
@@ -0,0 +1,73 @@
1
+ import type { ReactNode } from "react";
2
+ import { assertLaunchBeats, type LaunchBeat } from "../launch.js";
3
+ import { ArticleFigure, type ArticleFigureKind } from "./article.js";
4
+
5
+ /*
6
+ * Server-safe launch post body: one section per beat, each with its own
7
+ * anchor, heading, paragraph, and visual. Styled by plain-publication.css
8
+ * inside a MarketingArticle.
9
+ */
10
+
11
+ const HEADING_TAGS = { 2: "h2", 3: "h3", 4: "h4" } as const;
12
+
13
+ function figureKind(beat: LaunchBeat): ArticleFigureKind {
14
+ switch (beat.visual.kind) {
15
+ case "mockup":
16
+ return "illustration";
17
+ case "clip":
18
+ return "recording";
19
+ case "diagram":
20
+ return "diagram";
21
+ }
22
+ }
23
+
24
+ /** The anchor id a beat renders, such as `beat-status`. Social posts can deep-link to it. */
25
+ export function launchBeatAnchor(beat: Pick<LaunchBeat, "id">): string {
26
+ return `beat-${beat.id}`;
27
+ }
28
+
29
+ /**
30
+ * The beats of an "Introducing a product" post, in order. Each beat renders
31
+ * as a section with a `#beat-<id>` anchor, its headline, its post as the
32
+ * paragraph, and exactly one visual from `renderVisual`, captioned with the
33
+ * beat's alt text and labelled by kind. Pass beats already resolved against
34
+ * the launch facts.
35
+ */
36
+ export function LaunchBeats({
37
+ beats,
38
+ className,
39
+ detailLabel = "More on this",
40
+ headingLevel = 2,
41
+ renderVisual,
42
+ }: Readonly<{
43
+ beats: readonly LaunchBeat[];
44
+ className?: string;
45
+ /** Link text for a beat's `detailHref`. */
46
+ detailLabel?: string;
47
+ headingLevel?: 2 | 3 | 4;
48
+ /** Returns the one visual for a beat: a mockup, a video, or a diagram image. */
49
+ renderVisual: (beat: LaunchBeat) => ReactNode;
50
+ }>) {
51
+ assertLaunchBeats(beats);
52
+ const Heading = HEADING_TAGS[headingLevel];
53
+ if (Heading === undefined) throw new RangeError("Launch beat heading level must be 2 to 4.");
54
+ return (
55
+ <div className={["plain-publication__beats", className].filter(Boolean).join(" ")} data-hraness-launch-beats="">
56
+ {beats.map((beat) => {
57
+ const anchor = launchBeatAnchor(beat);
58
+ const visual = renderVisual(beat);
59
+ if (visual === null || visual === undefined || visual === false) throw new RangeError(`Launch beat ${JSON.stringify(beat.id)} needs a visual.`);
60
+ return (
61
+ <section aria-labelledby={`${anchor}-heading`} className="plain-publication__beat" data-part={beat.part} id={anchor} key={beat.id}>
62
+ <Heading id={`${anchor}-heading`}>{beat.headline}</Heading>
63
+ <p>{beat.post}</p>
64
+ <ArticleFigure caption={beat.alt} kind={figureKind(beat)}>{visual}</ArticleFigure>
65
+ {beat.detailHref === undefined ? null : (
66
+ <p className="plain-publication__beat-detail"><a href={beat.detailHref}>{detailLabel}</a></p>
67
+ )}
68
+ </section>
69
+ );
70
+ })}
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,130 @@
1
+ import * as stylex from "@stylexjs/stylex";
2
+
3
+ const hovered = ":is([data-hovered], :hover)";
4
+ const focusVisible = ":is([data-focus-visible], :focus-visible)";
5
+
6
+ export const navigationRailStyles = stylex.create({
7
+ item: {
8
+ alignItems: "center",
9
+ borderRadius: "var(--control-radius)",
10
+ color: "var(--muted)",
11
+ display: "grid",
12
+ gap: "var(--space-2)",
13
+ gridTemplateColumns: "auto minmax(0, 1fr) auto",
14
+ "min-block-size": "var(--interactive-target-min)",
15
+ padding: "var(--space-2) var(--space-3)",
16
+ textDecoration: "none",
17
+ },
18
+ itemActive: {
19
+ backgroundAttachment: { default: "scroll", [hovered]: "scroll" },
20
+ backgroundClip: { default: "border-box", [hovered]: "border-box" },
21
+ backgroundImage: { default: "none", [hovered]: "none" },
22
+ backgroundOrigin: { default: "padding-box", [hovered]: "padding-box" },
23
+ backgroundPosition: { default: "0% 0%", [hovered]: "0% 0%" },
24
+ backgroundRepeat: { default: "repeat", [hovered]: "repeat" },
25
+ backgroundSize: { default: "auto auto", [hovered]: "auto auto" },
26
+ backgroundColor: {
27
+ default: "var(--secondary)",
28
+ [hovered]: "var(--secondary)",
29
+ },
30
+ color: {
31
+ default: "var(--secondary-foreground)",
32
+ [hovered]: "var(--secondary-foreground)",
33
+ },
34
+ },
35
+ itemCopy: {
36
+ display: "grid",
37
+ gap: "0.125rem",
38
+ "min-inline-size": 0,
39
+ },
40
+ itemDescription: {
41
+ color: "var(--muted)",
42
+ fontSize: "var(--text-caption)",
43
+ overflowWrap: "anywhere",
44
+ },
45
+ itemIcon: {
46
+ display: "inline-grid",
47
+ placeItems: "center",
48
+ },
49
+ itemLabel: {
50
+ fontWeight: "var(--font-weight-medium)",
51
+ overflowWrap: "anywhere",
52
+ },
53
+ itemNativeInteractionFallbacks: {
54
+ backgroundAttachment: { default: null, [hovered]: "scroll" },
55
+ backgroundClip: { default: null, [hovered]: "border-box" },
56
+ backgroundImage: { default: null, [hovered]: "none" },
57
+ backgroundOrigin: { default: null, [hovered]: "padding-box" },
58
+ backgroundPosition: { default: null, [hovered]: "0% 0%" },
59
+ backgroundRepeat: { default: null, [hovered]: "repeat" },
60
+ backgroundSize: { default: null, [hovered]: "auto auto" },
61
+ backgroundColor: {
62
+ default: null,
63
+ [hovered]: "var(--surface-hover)",
64
+ },
65
+ color: {
66
+ default: null,
67
+ [hovered]: "var(--foreground)",
68
+ },
69
+ outlineColor: {
70
+ default: null,
71
+ [focusVisible]: "var(--focus)",
72
+ },
73
+ outlineOffset: {
74
+ default: null,
75
+ [focusVisible]: 2,
76
+ },
77
+ outlineStyle: {
78
+ default: null,
79
+ [focusVisible]: "solid",
80
+ },
81
+ outlineWidth: {
82
+ default: null,
83
+ [focusVisible]: 3,
84
+ },
85
+ },
86
+ navigation: {
87
+ alignContent: "start",
88
+ display: "grid",
89
+ flex: "1 1 auto",
90
+ gap: "var(--space-4)",
91
+ "min-block-size": 0,
92
+ overflow: "auto",
93
+ padding: "var(--space-3)",
94
+ },
95
+ rail: {
96
+ backgroundAttachment: "scroll",
97
+ backgroundClip: "border-box",
98
+ backgroundImage: "none",
99
+ backgroundOrigin: "padding-box",
100
+ backgroundPosition: "0% 0%",
101
+ backgroundRepeat: "repeat",
102
+ backgroundSize: "auto auto",
103
+ backgroundColor: "var(--surface)",
104
+ "block-size": "100%",
105
+ display: "flex",
106
+ flexDirection: "column",
107
+ "min-block-size": 0,
108
+ },
109
+ railEdge: {
110
+ flex: "0 0 auto",
111
+ padding: "var(--space-4)",
112
+ },
113
+ section: {
114
+ display: "grid",
115
+ gap: "var(--space-2)",
116
+ },
117
+ sectionItems: {
118
+ display: "grid",
119
+ gap: "var(--space-1)",
120
+ },
121
+ sectionTitle: {
122
+ color: "var(--muted)",
123
+ fontSize: "var(--text-caption)",
124
+ fontWeight: "var(--font-weight-medium)",
125
+ letterSpacing: "0.04em",
126
+ margin: 0,
127
+ "padding-inline": "var(--space-2)",
128
+ textTransform: "uppercase",
129
+ },
130
+ });