@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,604 @@
1
+ import {
2
+ ComputerIcon,
3
+ Moon02Icon,
4
+ Sun03Icon,
5
+ } from "@hugeicons/core-free-icons";
6
+
7
+ import {
8
+ defaultDesignTheme,
9
+ designThemeStorageKey,
10
+ designThemeLabel,
11
+ designThemes,
12
+ type DesignTheme,
13
+ isDesignTheme,
14
+ resolveDesignTheme,
15
+ } from "../appearance.js";
16
+ import {
17
+ acquireThemeColorMeta,
18
+ type ThemeColorMetaRegistration,
19
+ } from "./theme-color-sync.js";
20
+
21
+ const menuSelector = "[data-hraness-appearance-menu]";
22
+ const triggerSelector = ".hraness-design-theme-toggle__trigger";
23
+ const popoverSelector = ".hraness-design-theme-toggle__popover";
24
+ const menuElementSelector = ".hraness-design-theme-toggle__menu";
25
+ const itemSelector = ".hraness-design-theme-toggle__item[data-theme-value]";
26
+ const systemDarkQuery = "(prefers-color-scheme: dark)";
27
+ const svgNamespace = "http://www.w3.org/2000/svg";
28
+
29
+ type IconSvgElement = readonly (
30
+ readonly [string, Readonly<Record<string, string | number>>]
31
+ )[];
32
+
33
+ const appearanceIcons: Readonly<Record<DesignTheme, IconSvgElement>> = {
34
+ dark: Moon02Icon,
35
+ light: Sun03Icon,
36
+ system: ComputerIcon,
37
+ };
38
+
39
+ export interface AppearanceStorage {
40
+ readonly getItem: (key: string) => unknown;
41
+ readonly setItem: (key: string, value: string) => void;
42
+ }
43
+
44
+ export interface AppearanceMenuOptions {
45
+ readonly darkThemeColor: string;
46
+ readonly document?: Document;
47
+ readonly lightThemeColor: string;
48
+ readonly metaName?: string;
49
+ readonly storage?: AppearanceStorage | null;
50
+ readonly storageKey?: string;
51
+ }
52
+
53
+ export interface AppearanceMenuInstallation {
54
+ readonly dispose: () => void;
55
+ readonly preference: () => DesignTheme;
56
+ }
57
+
58
+ interface AppearanceMenuElements {
59
+ readonly items: ReadonlyMap<DesignTheme, HTMLElement>;
60
+ readonly menu: HTMLElement;
61
+ readonly popover: HTMLElement;
62
+ readonly root: HTMLElement;
63
+ readonly trigger: HTMLButtonElement;
64
+ typeahead: string;
65
+ typeaheadTimer: ReturnType<typeof setTimeout> | null;
66
+ }
67
+
68
+ interface InstallationState {
69
+ disposed: boolean;
70
+ readonly publicInstallation: AppearanceMenuInstallation;
71
+ }
72
+
73
+ const installationByDocument = new WeakMap<Document, InstallationState>();
74
+
75
+ function requireNonBlank(value: string, name: string): string {
76
+ if (value.trim().length === 0) throw new Error(`${name} must not be blank.`);
77
+ return value;
78
+ }
79
+
80
+ function resolveDocument(candidate: Document | undefined): Document {
81
+ const ownedDocument = candidate
82
+ ?? (typeof globalThis.document === "undefined" ? undefined : globalThis.document);
83
+ if (ownedDocument === undefined) {
84
+ throw new Error("installAppearanceMenus requires a browser Document.");
85
+ }
86
+ if (ownedDocument.documentElement === null || ownedDocument.head === null) {
87
+ throw new Error("installAppearanceMenus requires a complete HTML document.");
88
+ }
89
+ return ownedDocument;
90
+ }
91
+
92
+ function defaultStorage(document: Document): AppearanceStorage | null {
93
+ try {
94
+ return document.defaultView?.localStorage ?? null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function storedPreference(storage: AppearanceStorage | null, storageKey: string): DesignTheme {
101
+ if (storage === null) return defaultDesignTheme;
102
+ try {
103
+ const value = storage.getItem(storageKey);
104
+ if (value === null || value === undefined) return defaultDesignTheme;
105
+ if (isDesignTheme(value)) return value;
106
+ storage.setItem(storageKey, defaultDesignTheme);
107
+ } catch {
108
+ // Appearance persistence is optional in privacy-restricted browsers.
109
+ }
110
+ return defaultDesignTheme;
111
+ }
112
+
113
+ function persistPreference(
114
+ storage: AppearanceStorage | null,
115
+ storageKey: string,
116
+ preference: DesignTheme,
117
+ ): void {
118
+ if (storage === null) return;
119
+ try {
120
+ storage.setItem(storageKey, preference);
121
+ } catch {
122
+ // The current page still changes when persistence is unavailable.
123
+ }
124
+ }
125
+
126
+ function isElement(value: EventTarget | null): value is Element {
127
+ return value !== null
128
+ && typeof value === "object"
129
+ && "nodeType" in value
130
+ && (value as Node).nodeType === 1;
131
+ }
132
+
133
+ function attributeName(name: string): string {
134
+ return name.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);
135
+ }
136
+
137
+ function appearanceIcon(document: Document, theme: DesignTheme): SVGSVGElement {
138
+ const svg = document.createElementNS(svgNamespace, "svg");
139
+ svg.setAttribute("aria-hidden", "true");
140
+ svg.setAttribute("class", "hraness-icon");
141
+ svg.setAttribute("color", "currentColor");
142
+ svg.setAttribute("data-slot", "icon");
143
+ svg.setAttribute("fill", "none");
144
+ svg.setAttribute("height", "18");
145
+ svg.setAttribute("stroke", "currentColor");
146
+ svg.setAttribute("stroke-width", "1.5");
147
+ svg.setAttribute("viewBox", "0 0 24 24");
148
+ svg.setAttribute("width", "18");
149
+
150
+ for (const [tag, attributes] of appearanceIcons[theme]) {
151
+ const child = document.createElementNS(svgNamespace, tag);
152
+ for (const [name, value] of Object.entries(attributes)) {
153
+ if (name !== "key") child.setAttribute(attributeName(name), String(value));
154
+ }
155
+ child.setAttribute("stroke", "currentColor");
156
+ child.setAttribute("stroke-width", "1.5");
157
+ svg.append(child);
158
+ }
159
+ return svg;
160
+ }
161
+
162
+ function renderIconHost(
163
+ document: Document,
164
+ host: HTMLElement,
165
+ theme: DesignTheme,
166
+ ): void {
167
+ host.classList.add("hraness-appearance-icon");
168
+ host.setAttribute("aria-hidden", "true");
169
+ host.setAttribute("data-appearance-icon", theme);
170
+ host.setAttribute("data-slot", "appearance-icon");
171
+ host.replaceChildren(appearanceIcon(document, theme));
172
+ }
173
+
174
+ function themeValue(element: Element): DesignTheme | null {
175
+ const value = element.getAttribute("data-theme-value");
176
+ return isDesignTheme(value) ? value : null;
177
+ }
178
+
179
+ function requireMenuElements(root: HTMLElement): AppearanceMenuElements {
180
+ const triggers = root.querySelectorAll<HTMLButtonElement>(triggerSelector);
181
+ const popovers = root.querySelectorAll<HTMLElement>(popoverSelector);
182
+ const menus = root.querySelectorAll<HTMLElement>(menuElementSelector);
183
+ const trigger = triggers[0];
184
+ const popover = popovers[0];
185
+ const menu = menus[0];
186
+ if (
187
+ triggers.length !== 1
188
+ || popovers.length !== 1
189
+ || menus.length !== 1
190
+ || trigger?.tagName !== "BUTTON"
191
+ || popover === undefined
192
+ || menu === undefined
193
+ ) {
194
+ throw new Error(
195
+ "Appearance menu markup requires one button trigger, popover, and menu.",
196
+ );
197
+ }
198
+ if (
199
+ menu.id.trim().length === 0
200
+ || trigger.getAttribute("aria-controls") !== menu.id
201
+ || menu.getAttribute("role") !== "menu"
202
+ ) {
203
+ throw new Error("Appearance menu trigger and menu ownership is invalid.");
204
+ }
205
+
206
+ const items = new Map<DesignTheme, HTMLElement>();
207
+ for (const element of menu.querySelectorAll<HTMLElement>(itemSelector)) {
208
+ const theme = themeValue(element);
209
+ if (
210
+ theme === null
211
+ || items.has(theme)
212
+ || element.getAttribute("role") !== "menuitemradio"
213
+ ) {
214
+ throw new Error("Appearance menu items must be unique Light, Dark, and System radios.");
215
+ }
216
+ items.set(theme, element);
217
+ }
218
+ if (
219
+ items.size !== designThemes.length
220
+ || designThemes.some((theme) => !items.has(theme))
221
+ ) {
222
+ throw new Error("Appearance menu must contain Light, Dark, and System exactly once.");
223
+ }
224
+
225
+ return {
226
+ items,
227
+ menu,
228
+ popover,
229
+ root,
230
+ trigger,
231
+ typeahead: "",
232
+ typeaheadTimer: null,
233
+ };
234
+ }
235
+
236
+ function menuItemFromEvent(
237
+ elements: AppearanceMenuElements,
238
+ event: Event,
239
+ ): HTMLElement | null {
240
+ if (!isElement(event.target)) return null;
241
+ const item = event.target.closest<HTMLElement>(itemSelector);
242
+ return item !== null && elements.menu.contains(item) ? item : null;
243
+ }
244
+
245
+ function focusItem(elements: AppearanceMenuElements, theme: DesignTheme): void {
246
+ elements.items.get(theme)?.focus();
247
+ }
248
+
249
+ function orderedItems(elements: AppearanceMenuElements): readonly HTMLElement[] {
250
+ return designThemes.flatMap((theme) => {
251
+ const item = elements.items.get(theme);
252
+ return item === undefined ? [] : [item];
253
+ });
254
+ }
255
+
256
+ function adjacentItem(
257
+ elements: AppearanceMenuElements,
258
+ current: HTMLElement | null,
259
+ offset: -1 | 1,
260
+ ): HTMLElement {
261
+ const items = orderedItems(elements);
262
+ if (items.length === 0) throw new Error("Appearance menu has no focusable items.");
263
+ const currentIndex = current === null ? -1 : items.indexOf(current);
264
+ const nextIndex = currentIndex < 0
265
+ ? (offset === 1 ? 0 : items.length - 1)
266
+ : (currentIndex + offset + items.length) % items.length;
267
+ const next = items[nextIndex];
268
+ if (next === undefined) throw new Error("Appearance menu focus order is invalid.");
269
+ return next;
270
+ }
271
+
272
+ function storageEventValue(event: Event, storageKey: string): unknown {
273
+ const candidate = event as Event & Readonly<{ key?: unknown; newValue?: unknown }>;
274
+ return candidate.key === storageKey ? candidate.newValue : undefined;
275
+ }
276
+
277
+ /**
278
+ * Installs the standards-only Light/Dark/System controller for static pages.
279
+ * Each `[data-hraness-appearance-menu]` root owns one button trigger, one
280
+ * popover, and one `role="menu"` with unique Light/Dark/System radio items.
281
+ * The call applies the stored preference synchronously; menu enhancement waits
282
+ * for parsed markup when the installer runs from a blocking head script.
283
+ */
284
+ export function installAppearanceMenus(
285
+ options: AppearanceMenuOptions,
286
+ ): AppearanceMenuInstallation {
287
+ const document = resolveDocument(options.document);
288
+ const active = installationByDocument.get(document);
289
+ if (active !== undefined && !active.disposed) {
290
+ throw new Error("Appearance menus are already installed for this document.");
291
+ }
292
+
293
+ const darkThemeColor = requireNonBlank(options.darkThemeColor, "darkThemeColor");
294
+ const lightThemeColor = requireNonBlank(options.lightThemeColor, "lightThemeColor");
295
+ const metaName = requireNonBlank(options.metaName ?? "theme-color", "metaName");
296
+ const storageKey = requireNonBlank(options.storageKey ?? designThemeStorageKey, "storageKey");
297
+ const storage = options.storage === undefined ? defaultStorage(document) : options.storage;
298
+ const view = document.defaultView;
299
+ const systemMedia = typeof view?.matchMedia === "function"
300
+ ? view.matchMedia(systemDarkQuery)
301
+ : null;
302
+ let preference = storedPreference(storage, storageKey);
303
+ let disposed = false;
304
+ let mounted = false;
305
+ let openMenu: AppearanceMenuElements | null = null;
306
+ let metaRegistration: ThemeColorMetaRegistration | null = null;
307
+ const menus = new Set<AppearanceMenuElements>();
308
+ const menuIds = new Set<string>();
309
+ const cleanups: Array<() => void> = [];
310
+
311
+ const resolvedTheme = () => resolveDesignTheme(preference, systemMedia?.matches ?? false);
312
+ const currentThemeColor = () => resolvedTheme() === "dark"
313
+ ? darkThemeColor
314
+ : lightThemeColor;
315
+
316
+ const closeMenu = (
317
+ elements: AppearanceMenuElements,
318
+ returnFocus: boolean,
319
+ ): void => {
320
+ elements.popover.hidden = true;
321
+ elements.root.removeAttribute("data-open");
322
+ elements.trigger.setAttribute("aria-expanded", "false");
323
+ if (openMenu === elements) openMenu = null;
324
+ if (returnFocus) elements.trigger.focus();
325
+ };
326
+
327
+ const syncMenu = (elements: AppearanceMenuElements): void => {
328
+ elements.root.setAttribute("data-theme-value", preference);
329
+ const currentLabel = designThemeLabel(preference);
330
+ const menuLabel = elements.menu.getAttribute("aria-label")?.trim() || "Appearance";
331
+ elements.trigger.setAttribute("aria-label", `${menuLabel}: ${currentLabel}`);
332
+ elements.trigger.setAttribute("title", `${menuLabel}: ${currentLabel}`);
333
+ const currentHost = elements.trigger.querySelector<HTMLElement>(
334
+ "[data-current-appearance-icon]",
335
+ );
336
+ if (currentHost === null) {
337
+ throw new Error("Appearance menu trigger is missing its current icon host.");
338
+ }
339
+ currentHost.setAttribute("data-current-appearance-icon", preference);
340
+ renderIconHost(document, currentHost, preference);
341
+
342
+ for (const theme of designThemes) {
343
+ const item = elements.items.get(theme);
344
+ if (item === undefined) continue;
345
+ const selected = theme === preference;
346
+ item.setAttribute("aria-checked", String(selected));
347
+ item.setAttribute("tabindex", "-1");
348
+ item.toggleAttribute("data-selected", selected);
349
+ const iconHost = item.querySelector<HTMLElement>("[data-appearance-icon]");
350
+ if (iconHost === null) {
351
+ throw new Error(`Appearance menu ${theme} item is missing its icon host.`);
352
+ }
353
+ renderIconHost(document, iconHost, theme);
354
+ }
355
+ };
356
+
357
+ const syncAppearance = (): void => {
358
+ document.documentElement.setAttribute("data-theme", resolvedTheme());
359
+ metaRegistration?.update(currentThemeColor());
360
+ for (const menu of menus) syncMenu(menu);
361
+ };
362
+
363
+ const setPreference = (nextPreference: DesignTheme, persist: boolean): void => {
364
+ preference = nextPreference;
365
+ if (persist) persistPreference(storage, storageKey, preference);
366
+ syncAppearance();
367
+ };
368
+
369
+ const focusForOpen = (
370
+ elements: AppearanceMenuElements,
371
+ target: "first" | "last" | "selected",
372
+ ): void => {
373
+ if (target === "selected") focusItem(elements, preference);
374
+ else {
375
+ const items = orderedItems(elements);
376
+ const item = target === "first" ? items[0] : items.at(-1);
377
+ item?.focus();
378
+ }
379
+ };
380
+
381
+ const showMenu = (
382
+ elements: AppearanceMenuElements,
383
+ focus: "first" | "last" | "selected",
384
+ ): void => {
385
+ if (openMenu !== null && openMenu !== elements) closeMenu(openMenu, false);
386
+ openMenu = elements;
387
+ elements.popover.hidden = false;
388
+ elements.root.setAttribute("data-open", "true");
389
+ elements.trigger.setAttribute("aria-expanded", "true");
390
+ focusForOpen(elements, focus);
391
+ };
392
+
393
+ const selectItem = (elements: AppearanceMenuElements, item: HTMLElement): void => {
394
+ const theme = themeValue(item);
395
+ if (theme === null) return;
396
+ setPreference(theme, true);
397
+ closeMenu(elements, true);
398
+ };
399
+
400
+ const clearTypeahead = (elements: AppearanceMenuElements): void => {
401
+ elements.typeahead = "";
402
+ if (elements.typeaheadTimer !== null) clearTimeout(elements.typeaheadTimer);
403
+ elements.typeaheadTimer = null;
404
+ };
405
+
406
+ const typeahead = (
407
+ elements: AppearanceMenuElements,
408
+ key: string,
409
+ ): void => {
410
+ if (elements.typeaheadTimer !== null) clearTimeout(elements.typeaheadTimer);
411
+ elements.typeahead += key.toLocaleLowerCase();
412
+ const find = (query: string) => orderedItems(elements).find((item) =>
413
+ (item.textContent ?? "").trim().toLocaleLowerCase().startsWith(query));
414
+ const match = find(elements.typeahead) ?? find(key.toLocaleLowerCase());
415
+ match?.focus();
416
+ elements.typeaheadTimer = setTimeout(() => clearTypeahead(elements), 500);
417
+ };
418
+
419
+ const mount = (): void => {
420
+ if (disposed || mounted) return;
421
+ mounted = true;
422
+ for (const root of document.querySelectorAll<HTMLElement>(menuSelector)) {
423
+ const elements = requireMenuElements(root);
424
+ if (menuIds.has(elements.menu.id)) {
425
+ throw new Error("Appearance menu ids must be unique within the document.");
426
+ }
427
+ menuIds.add(elements.menu.id);
428
+ menus.add(elements);
429
+ syncMenu(elements);
430
+ root.setAttribute("data-display", "icons");
431
+ root.setAttribute("data-presentation", "menu");
432
+ root.setAttribute("data-ready", "true");
433
+ root.removeAttribute("aria-busy");
434
+ elements.trigger.disabled = false;
435
+ elements.trigger.setAttribute("aria-expanded", "false");
436
+ elements.trigger.setAttribute("aria-haspopup", "menu");
437
+ elements.popover.hidden = true;
438
+
439
+ const onTriggerClick = (): void => {
440
+ if (openMenu === elements) closeMenu(elements, false);
441
+ else showMenu(elements, "selected");
442
+ };
443
+ const onTriggerKeyDown = (event: KeyboardEvent): void => {
444
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
445
+ event.preventDefault();
446
+ showMenu(elements, event.key === "ArrowDown" ? "first" : "last");
447
+ } else if (event.key === "Escape" && openMenu === elements) {
448
+ event.preventDefault();
449
+ closeMenu(elements, true);
450
+ }
451
+ };
452
+ const onMenuClick = (event: MouseEvent): void => {
453
+ const item = menuItemFromEvent(elements, event);
454
+ if (item !== null) selectItem(elements, item);
455
+ };
456
+ const onMenuKeyDown = (event: KeyboardEvent): void => {
457
+ const item = menuItemFromEvent(elements, event);
458
+ if (event.key === "Escape") {
459
+ event.preventDefault();
460
+ event.stopPropagation();
461
+ closeMenu(elements, true);
462
+ return;
463
+ }
464
+ if (event.key === "Tab") {
465
+ closeMenu(elements, false);
466
+ return;
467
+ }
468
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
469
+ event.preventDefault();
470
+ adjacentItem(elements, item, event.key === "ArrowDown" ? 1 : -1).focus();
471
+ return;
472
+ }
473
+ if (event.key === "Home" || event.key === "End") {
474
+ event.preventDefault();
475
+ const items = orderedItems(elements);
476
+ (event.key === "Home" ? items[0] : items.at(-1))?.focus();
477
+ return;
478
+ }
479
+ if ((event.key === "Enter" || event.key === " ") && item !== null) {
480
+ event.preventDefault();
481
+ selectItem(elements, item);
482
+ return;
483
+ }
484
+ if (
485
+ event.key.length === 1
486
+ && !event.altKey
487
+ && !event.ctrlKey
488
+ && !event.metaKey
489
+ ) {
490
+ typeahead(elements, event.key);
491
+ }
492
+ };
493
+ elements.trigger.addEventListener("click", onTriggerClick);
494
+ elements.trigger.addEventListener("keydown", onTriggerKeyDown);
495
+ elements.menu.addEventListener("click", onMenuClick);
496
+ elements.menu.addEventListener("keydown", onMenuKeyDown);
497
+ cleanups.push(() => {
498
+ clearTypeahead(elements);
499
+ elements.trigger.removeEventListener("click", onTriggerClick);
500
+ elements.trigger.removeEventListener("keydown", onTriggerKeyDown);
501
+ elements.menu.removeEventListener("click", onMenuClick);
502
+ elements.menu.removeEventListener("keydown", onMenuKeyDown);
503
+ closeMenu(elements, false);
504
+ elements.root.setAttribute("aria-busy", "true");
505
+ elements.root.setAttribute("data-ready", "false");
506
+ elements.trigger.disabled = true;
507
+ });
508
+ }
509
+ };
510
+
511
+ const state: InstallationState = {
512
+ disposed: false,
513
+ publicInstallation: {
514
+ dispose: () => {
515
+ if (disposed) return;
516
+ disposed = true;
517
+ state.disposed = true;
518
+ for (const cleanup of cleanups.splice(0).reverse()) cleanup();
519
+ metaRegistration?.release();
520
+ metaRegistration = null;
521
+ menus.clear();
522
+ if (installationByDocument.get(document) === state) {
523
+ installationByDocument.delete(document);
524
+ }
525
+ },
526
+ preference: () => preference,
527
+ },
528
+ };
529
+ const { publicInstallation } = state;
530
+ installationByDocument.set(document, state);
531
+
532
+ try {
533
+ metaRegistration = acquireThemeColorMeta(
534
+ document,
535
+ metaName,
536
+ Symbol("hraness-static-appearance"),
537
+ currentThemeColor(),
538
+ );
539
+ document.documentElement.setAttribute("data-theme", resolvedTheme());
540
+
541
+ const onSystemChange = (): void => {
542
+ if (preference === "system") syncAppearance();
543
+ };
544
+ systemMedia?.addEventListener("change", onSystemChange);
545
+ if (systemMedia !== null) {
546
+ cleanups.push(() => systemMedia.removeEventListener("change", onSystemChange));
547
+ }
548
+
549
+ const onStorage = (event: Event): void => {
550
+ const value = storageEventValue(event, storageKey);
551
+ if (value === undefined) return;
552
+ if (isDesignTheme(value)) {
553
+ setPreference(value, false);
554
+ return;
555
+ }
556
+ setPreference(defaultDesignTheme, value !== null);
557
+ };
558
+ view?.addEventListener("storage", onStorage);
559
+ if (view !== null) {
560
+ cleanups.push(() => view.removeEventListener("storage", onStorage));
561
+ }
562
+
563
+ const closeForOutsideTarget = (target: EventTarget | null): void => {
564
+ if (
565
+ openMenu !== null
566
+ && (!isElement(target) || !openMenu.root.contains(target))
567
+ ) {
568
+ closeMenu(openMenu, false);
569
+ }
570
+ };
571
+ const onDocumentPointerDown = (event: Event): void => {
572
+ closeForOutsideTarget(event.target);
573
+ };
574
+ const onDocumentFocusIn = (event: Event): void => {
575
+ closeForOutsideTarget(event.target);
576
+ };
577
+ document.addEventListener("pointerdown", onDocumentPointerDown, true);
578
+ document.addEventListener("focusin", onDocumentFocusIn);
579
+ cleanups.push(() => {
580
+ document.removeEventListener("pointerdown", onDocumentPointerDown, true);
581
+ document.removeEventListener("focusin", onDocumentFocusIn);
582
+ });
583
+
584
+ if (document.readyState === "loading") {
585
+ const mountAfterParse = (): void => {
586
+ try {
587
+ mount();
588
+ } catch (error) {
589
+ publicInstallation.dispose();
590
+ throw error;
591
+ }
592
+ };
593
+ document.addEventListener("DOMContentLoaded", mountAfterParse, { once: true });
594
+ cleanups.push(() => document.removeEventListener("DOMContentLoaded", mountAfterParse));
595
+ } else {
596
+ mount();
597
+ }
598
+ } catch (error) {
599
+ publicInstallation.dispose();
600
+ throw error;
601
+ }
602
+
603
+ return publicInstallation;
604
+ }