@ciderpress/ui 1.0.0-rc.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 (428) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/dist/0~142.js +2 -0
  4. package/dist/0~164.js +2 -0
  5. package/dist/0~2.js +2 -0
  6. package/dist/0~225.js +2 -0
  7. package/dist/0~288.js +2 -0
  8. package/dist/0~299.js +2 -0
  9. package/dist/0~3.js +2 -0
  10. package/dist/0~301.js +2 -0
  11. package/dist/0~311.js +2 -0
  12. package/dist/0~326.js +2 -0
  13. package/dist/0~40.js +2 -0
  14. package/dist/0~43.js +2 -0
  15. package/dist/0~433.js +2 -0
  16. package/dist/0~453.js +2 -0
  17. package/dist/0~457.js +2 -0
  18. package/dist/0~497.js +2 -0
  19. package/dist/0~511.js +2 -0
  20. package/dist/0~603.js +2 -0
  21. package/dist/0~616.js +2 -0
  22. package/dist/0~659.js +2 -0
  23. package/dist/0~670.js +2 -0
  24. package/dist/0~687.js +2 -0
  25. package/dist/0~706.js +2 -0
  26. package/dist/0~729.js +2 -0
  27. package/dist/0~768.js +2 -0
  28. package/dist/0~819.js +2 -0
  29. package/dist/0~830.js +2 -0
  30. package/dist/0~854.js +2 -0
  31. package/dist/0~863.js +2 -0
  32. package/dist/0~879.js +2 -0
  33. package/dist/0~96.js +2 -0
  34. package/dist/0~97.js +2 -0
  35. package/dist/components/FileTree/FileTree.css +90 -0
  36. package/dist/components/FileTree/FileTree.d.ts +7 -0
  37. package/dist/components/FileTree/FileTree.js +531 -0
  38. package/dist/components/FileTree/FileTreeItem.d.ts +8 -0
  39. package/dist/components/FileTree/RemoteSvgIcon.d.ts +11 -0
  40. package/dist/components/FileTree/index.d.ts +4 -0
  41. package/dist/components/folder-icons.d.ts +32 -0
  42. package/dist/components/languages.d.ts +7 -0
  43. package/dist/components/tree-parser/tree-parser.d.ts +12 -0
  44. package/dist/components/tree-parser/types.d.ts +11 -0
  45. package/dist/head/css/loader-apple.css +1 -0
  46. package/dist/head/css/loader-backdrop.css +1 -0
  47. package/dist/head/css/loader-dots.css +1 -0
  48. package/dist/head/css/themes/amber.css +1 -0
  49. package/dist/head/css/themes/arcade.css +1 -0
  50. package/dist/head/css/themes/grannysmith.css +1 -0
  51. package/dist/head/css/themes/honeycrisp.css +1 -0
  52. package/dist/head/css/themes/midnight.css +1 -0
  53. package/dist/head/css/themes/mulled.css +1 -0
  54. package/dist/head/js/loader-dots.js +1 -0
  55. package/dist/head/js/vscode-nav.js +1 -0
  56. package/dist/index.d.ts +43 -0
  57. package/dist/index.mjs +40 -0
  58. package/dist/node.d.ts +81 -0
  59. package/dist/node.mjs +17762 -0
  60. package/dist/plugins/mermaid/MermaidRenderer.tsx +480 -0
  61. package/dist/plugins/mermaid/mermaid.css +199 -0
  62. package/dist/theme/components/announcement/announcement-bar.css +103 -0
  63. package/dist/theme/components/announcement/announcement-bar.tsx +173 -0
  64. package/dist/theme/components/ask-ai/ask-ai-button.css +52 -0
  65. package/dist/theme/components/ask-ai/ask-ai-button.tsx +56 -0
  66. package/dist/theme/components/content-footer/content-footer-portal.tsx +70 -0
  67. package/dist/theme/components/content-footer/feedback.css +47 -0
  68. package/dist/theme/components/content-footer/feedback.tsx +96 -0
  69. package/dist/theme/components/content-footer/meta-actions.css +25 -0
  70. package/dist/theme/components/content-footer/meta-actions.tsx +57 -0
  71. package/dist/theme/components/content-footer/page-pager.css +61 -0
  72. package/dist/theme/components/content-footer/page-pager.tsx +110 -0
  73. package/dist/theme/components/edit-source-button.tsx +97 -0
  74. package/dist/theme/components/footer/site-footer.css +179 -0
  75. package/dist/theme/components/footer/site-footer.tsx +115 -0
  76. package/dist/theme/components/home/cta.css +67 -0
  77. package/dist/theme/components/home/cta.tsx +77 -0
  78. package/dist/theme/components/home/feature-card.css +52 -0
  79. package/dist/theme/components/home/feature-card.tsx +131 -0
  80. package/dist/theme/components/home/feature.tsx +79 -0
  81. package/dist/theme/components/home/hero-demo.css +66 -0
  82. package/dist/theme/components/home/hero-demo.tsx +46 -0
  83. package/dist/theme/components/home/hero.css +96 -0
  84. package/dist/theme/components/home/hero.tsx +111 -0
  85. package/dist/theme/components/home/layout.tsx +183 -0
  86. package/dist/theme/components/home/page-rail.css +21 -0
  87. package/dist/theme/components/home/page-rail.tsx +19 -0
  88. package/dist/theme/components/home/split.css +148 -0
  89. package/dist/theme/components/home/split.tsx +120 -0
  90. package/dist/theme/components/home/trust-strip.css +33 -0
  91. package/dist/theme/components/home/trust-strip.tsx +43 -0
  92. package/dist/theme/components/home/workspaces.tsx +71 -0
  93. package/dist/theme/components/nav/branch-tag.css +54 -0
  94. package/dist/theme/components/nav/branch-tag.tsx +101 -0
  95. package/dist/theme/components/nav/ciderpress-doc-layout.tsx +247 -0
  96. package/dist/theme/components/nav/ciderpress-docs-bar.css +505 -0
  97. package/dist/theme/components/nav/ciderpress-docs-bar.tsx +426 -0
  98. package/dist/theme/components/nav/ciderpress-header.css +172 -0
  99. package/dist/theme/components/nav/ciderpress-header.tsx +113 -0
  100. package/dist/theme/components/nav/ciderpress-layout-context.tsx +49 -0
  101. package/dist/theme/components/nav/ciderpress-nav-hamburger.css +193 -0
  102. package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +112 -0
  103. package/dist/theme/components/nav/ciderpress-nav-menu.css +130 -0
  104. package/dist/theme/components/nav/ciderpress-nav-menu.tsx +372 -0
  105. package/dist/theme/components/nav/ciderpress-nav-search.css +73 -0
  106. package/dist/theme/components/nav/ciderpress-nav-search.tsx +55 -0
  107. package/dist/theme/components/nav/ciderpress-nav-social-links.css +35 -0
  108. package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +87 -0
  109. package/dist/theme/components/nav/floating-branch-indicator.css +106 -0
  110. package/dist/theme/components/nav/floating-branch-indicator.tsx +109 -0
  111. package/dist/theme/components/nav/layout.tsx +268 -0
  112. package/dist/theme/components/nav/mobile-nav-cta.tsx +79 -0
  113. package/dist/theme/components/nav/nav-divider.css +15 -0
  114. package/dist/theme/components/nav/nav-divider.tsx +19 -0
  115. package/dist/theme/components/nav/nav-logo.css +42 -0
  116. package/dist/theme/components/nav/nav-logo.tsx +262 -0
  117. package/dist/theme/components/nav/theme-switcher.css +92 -0
  118. package/dist/theme/components/nav/theme-switcher.tsx +298 -0
  119. package/dist/theme/components/nav/topbar-cta.css +45 -0
  120. package/dist/theme/components/nav/topbar-cta.tsx +39 -0
  121. package/dist/theme/components/nav/variant-toggle.css +70 -0
  122. package/dist/theme/components/nav/variant-toggle.tsx +83 -0
  123. package/dist/theme/components/nav/version-chip.css +31 -0
  124. package/dist/theme/components/nav/version-chip.tsx +45 -0
  125. package/dist/theme/components/nav/vscode-tag.css +32 -0
  126. package/dist/theme/components/nav/vscode-tag.tsx +20 -0
  127. package/dist/theme/components/openapi/code-sample.tsx +341 -0
  128. package/dist/theme/components/openapi/copy-markdown-button.tsx +41 -0
  129. package/dist/theme/components/openapi/icons.tsx +45 -0
  130. package/dist/theme/components/openapi/index.ts +6 -0
  131. package/dist/theme/components/openapi/method-badge.tsx +24 -0
  132. package/dist/theme/components/openapi/openapi.css +665 -0
  133. package/dist/theme/components/openapi/operation.tsx +200 -0
  134. package/dist/theme/components/openapi/overview.tsx +207 -0
  135. package/dist/theme/components/openapi/parameters-table.tsx +149 -0
  136. package/dist/theme/components/openapi/request-body.tsx +90 -0
  137. package/dist/theme/components/openapi/response-list.tsx +112 -0
  138. package/dist/theme/components/openapi/schema-viewer.tsx +277 -0
  139. package/dist/theme/components/openapi/security-badges.tsx +63 -0
  140. package/dist/theme/components/openapi/spec-utils.ts +122 -0
  141. package/dist/theme/components/shared/accordion.css +118 -0
  142. package/dist/theme/components/shared/accordion.stories.tsx +96 -0
  143. package/dist/theme/components/shared/accordion.tsx +191 -0
  144. package/dist/theme/components/shared/brand-logo.stories.tsx +49 -0
  145. package/dist/theme/components/shared/brand-mark.stories.tsx +53 -0
  146. package/dist/theme/components/shared/browser-window.css +1 -0
  147. package/dist/theme/components/shared/browser-window.stories.tsx +100 -0
  148. package/dist/theme/components/shared/browser-window.tsx +2 -0
  149. package/dist/theme/components/shared/card.stories.tsx +50 -0
  150. package/dist/theme/components/shared/card.tsx +35 -0
  151. package/dist/theme/components/shared/ciderpress-logo.tsx +93 -0
  152. package/dist/theme/components/shared/ciderpress-mark.tsx +77 -0
  153. package/dist/theme/components/shared/color.css +56 -0
  154. package/dist/theme/components/shared/color.stories.tsx +47 -0
  155. package/dist/theme/components/shared/color.tsx +69 -0
  156. package/dist/theme/components/shared/columns.css +37 -0
  157. package/dist/theme/components/shared/columns.stories.tsx +91 -0
  158. package/dist/theme/components/shared/columns.tsx +50 -0
  159. package/dist/theme/components/shared/desktop-window.css +339 -0
  160. package/dist/theme/components/shared/desktop-window.tsx +535 -0
  161. package/dist/theme/components/shared/field.css +158 -0
  162. package/dist/theme/components/shared/field.stories.tsx +100 -0
  163. package/dist/theme/components/shared/field.tsx +186 -0
  164. package/dist/theme/components/shared/frame.css +39 -0
  165. package/dist/theme/components/shared/frame.stories.tsx +49 -0
  166. package/dist/theme/components/shared/frame.tsx +76 -0
  167. package/dist/theme/components/shared/icon.stories.tsx +90 -0
  168. package/dist/theme/components/shared/icon.tsx +41 -0
  169. package/dist/theme/components/shared/prompt.css +178 -0
  170. package/dist/theme/components/shared/prompt.stories.tsx +57 -0
  171. package/dist/theme/components/shared/prompt.tsx +369 -0
  172. package/dist/theme/components/shared/resolve-card-icon.ts +28 -0
  173. package/dist/theme/components/shared/section-card.stories.tsx +51 -0
  174. package/dist/theme/components/shared/section-card.tsx +44 -0
  175. package/dist/theme/components/shared/section-grid.stories.tsx +46 -0
  176. package/dist/theme/components/shared/section-grid.tsx +15 -0
  177. package/dist/theme/components/shared/status-badge.css +37 -0
  178. package/dist/theme/components/shared/status-badge.stories.tsx +61 -0
  179. package/dist/theme/components/shared/status-badge.tsx +74 -0
  180. package/dist/theme/components/shared/steps.css +88 -0
  181. package/dist/theme/components/shared/steps.stories.tsx +68 -0
  182. package/dist/theme/components/shared/steps.tsx +100 -0
  183. package/dist/theme/components/shared/tech-icon-table.stories.tsx +29 -0
  184. package/dist/theme/components/shared/tech-icon-table.tsx +71 -0
  185. package/dist/theme/components/shared/tech-tag.stories.tsx +40 -0
  186. package/dist/theme/components/shared/tech-tag.tsx +31 -0
  187. package/dist/theme/components/shared/theme-color-palette.css +80 -0
  188. package/dist/theme/components/shared/theme-color-palette.stories.tsx +52 -0
  189. package/dist/theme/components/shared/theme-color-palette.tsx +275 -0
  190. package/dist/theme/components/shared/tooltip.css +69 -0
  191. package/dist/theme/components/shared/tooltip.stories.tsx +72 -0
  192. package/dist/theme/components/shared/tooltip.tsx +77 -0
  193. package/dist/theme/components/sidebar/framework-picker.css +77 -0
  194. package/dist/theme/components/sidebar/framework-picker.tsx +78 -0
  195. package/dist/theme/components/sidebar/sidebar-filter.test.ts +140 -0
  196. package/dist/theme/components/sidebar/sidebar-filter.ts +79 -0
  197. package/dist/theme/components/sidebar/sidebar-links.css +102 -0
  198. package/dist/theme/components/sidebar/sidebar-links.tsx +122 -0
  199. package/dist/theme/components/sidebar/sidebar-promo.css +42 -0
  200. package/dist/theme/components/sidebar/sidebar-promo.tsx +52 -0
  201. package/dist/theme/components/sidebar/sidebar-scope.tsx +151 -0
  202. package/dist/theme/components/sidebar/sidebar-toggle.css +181 -0
  203. package/dist/theme/components/sidebar/sidebar-toggle.tsx +127 -0
  204. package/dist/theme/components/theme-provider.tsx +637 -0
  205. package/dist/theme/components/workspaces/card.css +151 -0
  206. package/dist/theme/components/workspaces/card.tsx +166 -0
  207. package/dist/theme/components/workspaces/grid.tsx +62 -0
  208. package/dist/theme/css.d.ts +1 -0
  209. package/dist/theme/env.d.ts +12 -0
  210. package/dist/theme/fonts/GeistMono-Variable.woff2 +0 -0
  211. package/dist/theme/fonts/GeistPixel-Square.woff2 +0 -0
  212. package/dist/theme/fonts/GeistSans-Variable.woff2 +0 -0
  213. package/dist/theme/hooks/use-ciderpress.ts +61 -0
  214. package/dist/theme/hooks/use-nav-items.ts +76 -0
  215. package/dist/theme/icons/index.ts +2 -0
  216. package/dist/theme/icons/tech-map.ts +228 -0
  217. package/dist/theme/index.tsx +180 -0
  218. package/dist/theme/lib/read-social-links.ts +27 -0
  219. package/dist/theme/lib/route-link.tsx +126 -0
  220. package/dist/theme/lib/safe-url.test.ts +0 -0
  221. package/dist/theme/lib/safe-url.ts +68 -0
  222. package/dist/theme/lib/theme-favicon.ts +125 -0
  223. package/dist/theme/lib/user-config-stub.ts +12 -0
  224. package/dist/theme/styles/layers.css +32 -0
  225. package/dist/theme/styles/overrides/badge.css +26 -0
  226. package/dist/theme/styles/overrides/content-footer.css +80 -0
  227. package/dist/theme/styles/overrides/details.css +61 -0
  228. package/dist/theme/styles/overrides/fonts.css +32 -0
  229. package/dist/theme/styles/overrides/home-card.css +133 -0
  230. package/dist/theme/styles/overrides/home.css +243 -0
  231. package/dist/theme/styles/overrides/rail.css +337 -0
  232. package/dist/theme/styles/overrides/rspress.css +62 -0
  233. package/dist/theme/styles/overrides/scrollbar.css +25 -0
  234. package/dist/theme/styles/overrides/section-card.css +114 -0
  235. package/dist/theme/styles/overrides/sidebar.css +144 -0
  236. package/dist/theme/styles/overrides/tokens.css +288 -0
  237. package/dist/theme/styles/overrides/vscode.css +88 -0
  238. package/dist/theme/styles/themes/amber.css +545 -0
  239. package/dist/theme/styles/themes/arcade-fx.css +366 -0
  240. package/dist/theme/styles/themes/arcade.css +273 -0
  241. package/dist/theme/styles/themes/grannysmith.css +545 -0
  242. package/dist/theme/styles/themes/honeycrisp.css +545 -0
  243. package/dist/theme/styles/themes/midnight.css +273 -0
  244. package/dist/theme/styles/themes/mulled.css +817 -0
  245. package/package.json +103 -0
  246. package/src/theme/components/announcement/announcement-bar.css +103 -0
  247. package/src/theme/components/announcement/announcement-bar.tsx +173 -0
  248. package/src/theme/components/ask-ai/ask-ai-button.css +52 -0
  249. package/src/theme/components/ask-ai/ask-ai-button.tsx +56 -0
  250. package/src/theme/components/content-footer/content-footer-portal.tsx +70 -0
  251. package/src/theme/components/content-footer/feedback.css +47 -0
  252. package/src/theme/components/content-footer/feedback.tsx +96 -0
  253. package/src/theme/components/content-footer/meta-actions.css +25 -0
  254. package/src/theme/components/content-footer/meta-actions.tsx +57 -0
  255. package/src/theme/components/content-footer/page-pager.css +61 -0
  256. package/src/theme/components/content-footer/page-pager.tsx +110 -0
  257. package/src/theme/components/edit-source-button.tsx +97 -0
  258. package/src/theme/components/footer/site-footer.css +179 -0
  259. package/src/theme/components/footer/site-footer.tsx +115 -0
  260. package/src/theme/components/home/cta.css +67 -0
  261. package/src/theme/components/home/cta.tsx +77 -0
  262. package/src/theme/components/home/feature-card.css +52 -0
  263. package/src/theme/components/home/feature-card.tsx +131 -0
  264. package/src/theme/components/home/feature.tsx +79 -0
  265. package/src/theme/components/home/hero-demo.css +66 -0
  266. package/src/theme/components/home/hero-demo.tsx +46 -0
  267. package/src/theme/components/home/hero.css +96 -0
  268. package/src/theme/components/home/hero.tsx +111 -0
  269. package/src/theme/components/home/layout.tsx +183 -0
  270. package/src/theme/components/home/page-rail.css +21 -0
  271. package/src/theme/components/home/page-rail.tsx +19 -0
  272. package/src/theme/components/home/split.css +148 -0
  273. package/src/theme/components/home/split.tsx +120 -0
  274. package/src/theme/components/home/trust-strip.css +33 -0
  275. package/src/theme/components/home/trust-strip.tsx +43 -0
  276. package/src/theme/components/home/workspaces.tsx +71 -0
  277. package/src/theme/components/nav/branch-tag.css +54 -0
  278. package/src/theme/components/nav/branch-tag.tsx +101 -0
  279. package/src/theme/components/nav/ciderpress-doc-layout.tsx +247 -0
  280. package/src/theme/components/nav/ciderpress-docs-bar.css +505 -0
  281. package/src/theme/components/nav/ciderpress-docs-bar.tsx +426 -0
  282. package/src/theme/components/nav/ciderpress-header.css +172 -0
  283. package/src/theme/components/nav/ciderpress-header.tsx +113 -0
  284. package/src/theme/components/nav/ciderpress-layout-context.tsx +49 -0
  285. package/src/theme/components/nav/ciderpress-nav-hamburger.css +193 -0
  286. package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +112 -0
  287. package/src/theme/components/nav/ciderpress-nav-menu.css +130 -0
  288. package/src/theme/components/nav/ciderpress-nav-menu.tsx +372 -0
  289. package/src/theme/components/nav/ciderpress-nav-search.css +73 -0
  290. package/src/theme/components/nav/ciderpress-nav-search.tsx +55 -0
  291. package/src/theme/components/nav/ciderpress-nav-social-links.css +35 -0
  292. package/src/theme/components/nav/ciderpress-nav-social-links.tsx +87 -0
  293. package/src/theme/components/nav/floating-branch-indicator.css +106 -0
  294. package/src/theme/components/nav/floating-branch-indicator.tsx +109 -0
  295. package/src/theme/components/nav/layout.tsx +268 -0
  296. package/src/theme/components/nav/mobile-nav-cta.tsx +79 -0
  297. package/src/theme/components/nav/nav-divider.css +15 -0
  298. package/src/theme/components/nav/nav-divider.tsx +19 -0
  299. package/src/theme/components/nav/nav-logo.css +42 -0
  300. package/src/theme/components/nav/nav-logo.tsx +262 -0
  301. package/src/theme/components/nav/theme-switcher.css +92 -0
  302. package/src/theme/components/nav/theme-switcher.tsx +298 -0
  303. package/src/theme/components/nav/topbar-cta.css +45 -0
  304. package/src/theme/components/nav/topbar-cta.tsx +39 -0
  305. package/src/theme/components/nav/variant-toggle.css +70 -0
  306. package/src/theme/components/nav/variant-toggle.tsx +83 -0
  307. package/src/theme/components/nav/version-chip.css +31 -0
  308. package/src/theme/components/nav/version-chip.tsx +45 -0
  309. package/src/theme/components/nav/vscode-tag.css +32 -0
  310. package/src/theme/components/nav/vscode-tag.tsx +20 -0
  311. package/src/theme/components/openapi/code-sample.tsx +341 -0
  312. package/src/theme/components/openapi/copy-markdown-button.tsx +41 -0
  313. package/src/theme/components/openapi/icons.tsx +45 -0
  314. package/src/theme/components/openapi/index.ts +6 -0
  315. package/src/theme/components/openapi/method-badge.tsx +24 -0
  316. package/src/theme/components/openapi/openapi.css +665 -0
  317. package/src/theme/components/openapi/operation.tsx +200 -0
  318. package/src/theme/components/openapi/overview.tsx +207 -0
  319. package/src/theme/components/openapi/parameters-table.tsx +149 -0
  320. package/src/theme/components/openapi/request-body.tsx +90 -0
  321. package/src/theme/components/openapi/response-list.tsx +112 -0
  322. package/src/theme/components/openapi/schema-viewer.tsx +277 -0
  323. package/src/theme/components/openapi/security-badges.tsx +63 -0
  324. package/src/theme/components/openapi/spec-utils.ts +122 -0
  325. package/src/theme/components/shared/accordion.css +118 -0
  326. package/src/theme/components/shared/accordion.stories.tsx +96 -0
  327. package/src/theme/components/shared/accordion.tsx +191 -0
  328. package/src/theme/components/shared/brand-logo.stories.tsx +49 -0
  329. package/src/theme/components/shared/brand-mark.stories.tsx +53 -0
  330. package/src/theme/components/shared/browser-window.css +1 -0
  331. package/src/theme/components/shared/browser-window.stories.tsx +100 -0
  332. package/src/theme/components/shared/browser-window.tsx +2 -0
  333. package/src/theme/components/shared/card.stories.tsx +50 -0
  334. package/src/theme/components/shared/card.tsx +35 -0
  335. package/src/theme/components/shared/ciderpress-logo.tsx +93 -0
  336. package/src/theme/components/shared/ciderpress-mark.tsx +77 -0
  337. package/src/theme/components/shared/color.css +56 -0
  338. package/src/theme/components/shared/color.stories.tsx +47 -0
  339. package/src/theme/components/shared/color.tsx +69 -0
  340. package/src/theme/components/shared/columns.css +37 -0
  341. package/src/theme/components/shared/columns.stories.tsx +91 -0
  342. package/src/theme/components/shared/columns.tsx +50 -0
  343. package/src/theme/components/shared/desktop-window.css +339 -0
  344. package/src/theme/components/shared/desktop-window.tsx +535 -0
  345. package/src/theme/components/shared/field.css +158 -0
  346. package/src/theme/components/shared/field.stories.tsx +100 -0
  347. package/src/theme/components/shared/field.tsx +186 -0
  348. package/src/theme/components/shared/frame.css +39 -0
  349. package/src/theme/components/shared/frame.stories.tsx +49 -0
  350. package/src/theme/components/shared/frame.tsx +76 -0
  351. package/src/theme/components/shared/icon.stories.tsx +90 -0
  352. package/src/theme/components/shared/icon.tsx +41 -0
  353. package/src/theme/components/shared/prompt.css +178 -0
  354. package/src/theme/components/shared/prompt.stories.tsx +57 -0
  355. package/src/theme/components/shared/prompt.tsx +369 -0
  356. package/src/theme/components/shared/resolve-card-icon.ts +28 -0
  357. package/src/theme/components/shared/section-card.stories.tsx +51 -0
  358. package/src/theme/components/shared/section-card.tsx +44 -0
  359. package/src/theme/components/shared/section-grid.stories.tsx +46 -0
  360. package/src/theme/components/shared/section-grid.tsx +15 -0
  361. package/src/theme/components/shared/status-badge.css +37 -0
  362. package/src/theme/components/shared/status-badge.stories.tsx +61 -0
  363. package/src/theme/components/shared/status-badge.tsx +74 -0
  364. package/src/theme/components/shared/steps.css +88 -0
  365. package/src/theme/components/shared/steps.stories.tsx +68 -0
  366. package/src/theme/components/shared/steps.tsx +100 -0
  367. package/src/theme/components/shared/tech-icon-table.stories.tsx +29 -0
  368. package/src/theme/components/shared/tech-icon-table.tsx +71 -0
  369. package/src/theme/components/shared/tech-tag.stories.tsx +40 -0
  370. package/src/theme/components/shared/tech-tag.tsx +31 -0
  371. package/src/theme/components/shared/theme-color-palette.css +80 -0
  372. package/src/theme/components/shared/theme-color-palette.stories.tsx +52 -0
  373. package/src/theme/components/shared/theme-color-palette.tsx +275 -0
  374. package/src/theme/components/shared/tooltip.css +69 -0
  375. package/src/theme/components/shared/tooltip.stories.tsx +72 -0
  376. package/src/theme/components/shared/tooltip.tsx +77 -0
  377. package/src/theme/components/sidebar/framework-picker.css +77 -0
  378. package/src/theme/components/sidebar/framework-picker.tsx +78 -0
  379. package/src/theme/components/sidebar/sidebar-filter.test.ts +140 -0
  380. package/src/theme/components/sidebar/sidebar-filter.ts +79 -0
  381. package/src/theme/components/sidebar/sidebar-links.css +102 -0
  382. package/src/theme/components/sidebar/sidebar-links.tsx +122 -0
  383. package/src/theme/components/sidebar/sidebar-promo.css +42 -0
  384. package/src/theme/components/sidebar/sidebar-promo.tsx +52 -0
  385. package/src/theme/components/sidebar/sidebar-scope.tsx +151 -0
  386. package/src/theme/components/sidebar/sidebar-toggle.css +181 -0
  387. package/src/theme/components/sidebar/sidebar-toggle.tsx +127 -0
  388. package/src/theme/components/theme-provider.tsx +637 -0
  389. package/src/theme/components/workspaces/card.css +151 -0
  390. package/src/theme/components/workspaces/card.tsx +166 -0
  391. package/src/theme/components/workspaces/grid.tsx +62 -0
  392. package/src/theme/css.d.ts +1 -0
  393. package/src/theme/env.d.ts +12 -0
  394. package/src/theme/fonts/GeistMono-Variable.woff2 +0 -0
  395. package/src/theme/fonts/GeistPixel-Square.woff2 +0 -0
  396. package/src/theme/fonts/GeistSans-Variable.woff2 +0 -0
  397. package/src/theme/hooks/use-ciderpress.ts +61 -0
  398. package/src/theme/hooks/use-nav-items.ts +76 -0
  399. package/src/theme/icons/index.ts +2 -0
  400. package/src/theme/icons/tech-map.ts +228 -0
  401. package/src/theme/index.tsx +180 -0
  402. package/src/theme/lib/read-social-links.ts +27 -0
  403. package/src/theme/lib/route-link.tsx +126 -0
  404. package/src/theme/lib/safe-url.test.ts +0 -0
  405. package/src/theme/lib/safe-url.ts +68 -0
  406. package/src/theme/lib/theme-favicon.ts +125 -0
  407. package/src/theme/lib/user-config-stub.ts +12 -0
  408. package/src/theme/styles/layers.css +32 -0
  409. package/src/theme/styles/overrides/badge.css +26 -0
  410. package/src/theme/styles/overrides/content-footer.css +80 -0
  411. package/src/theme/styles/overrides/details.css +61 -0
  412. package/src/theme/styles/overrides/fonts.css +32 -0
  413. package/src/theme/styles/overrides/home-card.css +133 -0
  414. package/src/theme/styles/overrides/home.css +243 -0
  415. package/src/theme/styles/overrides/rail.css +337 -0
  416. package/src/theme/styles/overrides/rspress.css +62 -0
  417. package/src/theme/styles/overrides/scrollbar.css +25 -0
  418. package/src/theme/styles/overrides/section-card.css +114 -0
  419. package/src/theme/styles/overrides/sidebar.css +144 -0
  420. package/src/theme/styles/overrides/tokens.css +288 -0
  421. package/src/theme/styles/overrides/vscode.css +88 -0
  422. package/src/theme/styles/themes/amber.css +545 -0
  423. package/src/theme/styles/themes/arcade-fx.css +366 -0
  424. package/src/theme/styles/themes/arcade.css +273 -0
  425. package/src/theme/styles/themes/grannysmith.css +545 -0
  426. package/src/theme/styles/themes/honeycrisp.css +545 -0
  427. package/src/theme/styles/themes/midnight.css +273 -0
  428. package/src/theme/styles/themes/mulled.css +817 -0
@@ -0,0 +1,637 @@
1
+ import { TOKEN_TO_CSS_VAR } from '@ciderpress/theme'
2
+ import type { ThemeColors, ThemeVariant, TokenPath } from '@ciderpress/theme'
3
+ import { match, P } from 'massaman/match'
4
+ import { useEffect, useLayoutEffect } from 'react'
5
+ import type React from 'react'
6
+
7
+ import { syncThemeFavicon } from '../lib/theme-favicon.ts'
8
+
9
+ declare const __CIDERPRESS_THEME_NAME__: string
10
+ declare const __CIDERPRESS_DEFAULT_VARIANT__: string
11
+ declare const __CIDERPRESS_THEME_COLORS__: string
12
+ declare const __CIDERPRESS_THEME_DARK_COLORS__: string
13
+ declare const __CIDERPRESS_THEME_REGISTRY__: string
14
+
15
+ interface RegistryEntry {
16
+ readonly name: string
17
+ readonly variants: readonly ThemeVariant[]
18
+ readonly defaultVariant: ThemeVariant
19
+ }
20
+
21
+ /**
22
+ * `ThemeColors` field names whose values are applied to the DOM as
23
+ * CSS custom-property overrides.
24
+ */
25
+ type ThemeColorKey = keyof ThemeColors
26
+
27
+ /**
28
+ * Parsed theme registry — built-in themes plus any user themes from
29
+ * `config.themes`. Read from the build-time define so the client bundle
30
+ * does not pull `@ciderpress/theme`'s factory + Zod into the runtime path.
31
+ */
32
+ const REGISTRY_ENTRIES: readonly RegistryEntry[] = parseRegistry(__CIDERPRESS_THEME_REGISTRY__)
33
+
34
+ /**
35
+ * Variants supported by each registered theme, keyed by theme name. Used
36
+ * to set `data-cp-variants` on `<html>` so CSS can hide the appearance
37
+ * toggle for themes that only declare one variant.
38
+ */
39
+ const VARIANTS_BY_THEME: Readonly<Record<string, string>> = Object.freeze(
40
+ Object.fromEntries(REGISTRY_ENTRIES.map((entry) => [entry.name, entry.variants.join(' ')]))
41
+ )
42
+
43
+ /**
44
+ * Default variant for each theme — used when no `ciderpress-variant` is
45
+ * persisted in `localStorage` or when the persisted value is stale.
46
+ */
47
+ const DEFAULT_VARIANT_BY_THEME: Readonly<Record<string, ThemeVariant>> = Object.freeze(
48
+ Object.fromEntries(REGISTRY_ENTRIES.map((entry) => [entry.name, entry.defaultVariant]))
49
+ )
50
+
51
+ /**
52
+ * Set of theme names known at build time. Used to validate a persisted
53
+ * theme name from `localStorage` before applying it.
54
+ */
55
+ const REGISTERED_THEME_NAMES: ReadonlySet<string> = new Set(REGISTRY_ENTRIES.map((e) => e.name))
56
+
57
+ /**
58
+ * Mapping from each `ThemeColors` field to the canonical `CiderpressTokens`
59
+ * leaf path that backs it. Fields whose only target is a legacy `--rp-*`
60
+ * var have `null` here.
61
+ */
62
+ const THEME_COLOR_TOKEN_PATHS: Readonly<Record<ThemeColorKey, TokenPath | null>> = Object.freeze({
63
+ brand: 'colors.brand.primary',
64
+ brandLight: null,
65
+ brandDark: 'colors.brand.hover',
66
+ brandSoft: 'colors.brand.soft',
67
+ bg: 'colors.surface.bg',
68
+ bgAlt: 'colors.surface.bgAlt',
69
+ bgElv: 'colors.surface.bgElv',
70
+ bgSoft: 'colors.surface.bgSoft',
71
+ text1: 'colors.text.text1',
72
+ text2: 'colors.text.text2',
73
+ text3: 'colors.text.text3',
74
+ divider: 'colors.border.divider',
75
+ border: 'colors.border.border',
76
+ homeBg: null,
77
+ })
78
+
79
+ /**
80
+ * Legacy `--rp-*` CSS variable names still emitted alongside the
81
+ * canonical `--cp-*` vars so Rspress's built-in styles continue to
82
+ * pick up theme color overrides without modification.
83
+ */
84
+ const LEGACY_RP_VARS: Readonly<Record<ThemeColorKey, readonly string[]>> = Object.freeze({
85
+ brand: ['--rp-c-brand'],
86
+ brandLight: ['--rp-c-brand-light'],
87
+ brandDark: ['--rp-c-brand-dark'],
88
+ brandSoft: ['--rp-c-brand-tint'],
89
+ bg: ['--rp-c-bg'],
90
+ bgAlt: [],
91
+ bgElv: [],
92
+ bgSoft: ['--rp-c-bg-soft'],
93
+ text1: ['--rp-c-text-1'],
94
+ text2: ['--rp-c-text-2'],
95
+ text3: ['--rp-c-text-3'],
96
+ divider: ['--rp-c-divider'],
97
+ border: [],
98
+ homeBg: ['--rp-home-background-bg'],
99
+ })
100
+
101
+ /**
102
+ * Mapping from each `ThemeColors` field name to the ordered list of CSS
103
+ * variables it sets on `<html>`. The canonical `--cp-*` var (resolved
104
+ * from `TOKEN_TO_CSS_VAR`) is emitted first, followed by any legacy
105
+ * `--rp-*` vars from `LEGACY_RP_VARS`.
106
+ */
107
+ const COLOR_VAR_MAP: Readonly<Record<ThemeColorKey, readonly string[]>> = Object.freeze(
108
+ Object.fromEntries(
109
+ (Object.keys(THEME_COLOR_TOKEN_PATHS) as readonly ThemeColorKey[]).map((key) => {
110
+ const tokenPath = THEME_COLOR_TOKEN_PATHS[key]
111
+ const legacy = LEGACY_RP_VARS[key]
112
+ const cpVar = match(tokenPath)
113
+ .with(P.nullish, () => [] as readonly string[])
114
+ .otherwise((path) => [TOKEN_TO_CSS_VAR[path]] as readonly string[])
115
+ return [key, [...cpVar, ...legacy]] as const
116
+ })
117
+ ) as Record<ThemeColorKey, readonly string[]>
118
+ )
119
+
120
+ /**
121
+ * Collect all CSS variable names from the color map.
122
+ */
123
+ const ALL_CSS_VARS: readonly string[] = Object.values(COLOR_VAR_MAP).flat()
124
+
125
+ /**
126
+ * Minimum time (ms) the loading overlay stays visible before fading out.
127
+ */
128
+ const LOADER_MIN_DISPLAY_MS = 150
129
+
130
+ /**
131
+ * Duration (ms) of the CSS fade-out transition. Must match the
132
+ * `transition: opacity` value in loader-backdrop.css / loader-dots.css.
133
+ */
134
+ const LOADER_FADE_MS = 200
135
+
136
+ const useIsomorphicLayoutEffect = getIsomorphicEffect()
137
+
138
+ /**
139
+ * ThemeProvider — global UI component that configures the active theme
140
+ * and variant.
141
+ *
142
+ * Sets `data-cp-theme`, `data-cp-variant`, and `data-cp-variants` on
143
+ * `<html>` so theme CSS resolves correctly. Mirrors variant changes to
144
+ * Rspress's `.rp-dark` class so Rspress's built-in components stay in
145
+ * sync with our variant state, and observes Rspress's class to persist
146
+ * variant changes triggered by Rspress's own dark-mode toggle.
147
+ *
148
+ * Sets `data-cp-ready` on `<html>` to dismiss the loading overlay
149
+ * injected by critical CSS.
150
+ *
151
+ * @returns Null element (side-effect only component)
152
+ */
153
+ export function ThemeProvider(): React.ReactElement | null {
154
+ useIsomorphicLayoutEffect(() => {
155
+ const html = document.documentElement
156
+ const persistedTheme = safeGetItem('ciderpress-theme')
157
+ const themeName = resolveActiveThemeName(persistedTheme)
158
+ if (persistedTheme !== null && persistedTheme !== themeName) {
159
+ safeRemoveItem('ciderpress-theme')
160
+ }
161
+
162
+ const persistedVariant = safeGetVariant('ciderpress-variant')
163
+ const buildTimeDefault = parseVariant(__CIDERPRESS_DEFAULT_VARIANT__)
164
+ const variant = resolveActiveVariant({
165
+ persisted: persistedVariant,
166
+ buildTimeDefault,
167
+ themeName,
168
+ })
169
+ if (persistedVariant !== null && persistedVariant !== variant) {
170
+ safeRemoveItem('ciderpress-variant')
171
+ }
172
+
173
+ const colors = parseColors(__CIDERPRESS_THEME_COLORS__)
174
+ const darkColors = parseColors(__CIDERPRESS_THEME_DARK_COLORS__)
175
+ const hasColors = Object.keys(colors).length > 0
176
+ const hasDarkColors = Object.keys(darkColors).length > 0
177
+
178
+ html.dataset.cpTheme = themeName
179
+
180
+ // Set supported variants — CSS hides the appearance toggle for themes
181
+ // that only declare one variant.
182
+ const variants = VARIANTS_BY_THEME[themeName]
183
+ if (variants) {
184
+ html.dataset.cpVariants = variants
185
+ } else {
186
+ html.dataset.cpVariants = 'dark'
187
+ }
188
+
189
+ // Apply the resolved variant — both as `data-cp-variant` (our canonical
190
+ // attribute used by emitted theme CSS) and by mirroring to Rspress's
191
+ // `.rp-dark` class so its components stay aligned.
192
+ applyVariant(html, variant)
193
+
194
+ if (hasColors) {
195
+ applyColorOverrides(html, colors)
196
+ }
197
+
198
+ if (variant === 'dark' && hasDarkColors) {
199
+ applyColorOverrides(html, darkColors)
200
+ }
201
+
202
+ // Sync the document favicon with the active theme's brand colour.
203
+ // Browsers cache static icon assets and ignore CSS, so the only way to
204
+ // keep the tab mark in lockstep with the chosen theme is to swap
205
+ // `<link rel="icon">` to a data-URI SVG carrying the resolved
206
+ // `--cp-c-brand-1`.
207
+ syncThemeFavicon(html)
208
+
209
+ // Observe `.rp-dark` class changes so Rspress's built-in dark toggle
210
+ // stays the single source of truth for variant flips. The new variant
211
+ // must be present in the active theme's supported set; if it isn't
212
+ // (e.g. a devtools tinkerer flips `.rp-dark` off on `midnight`, which
213
+ // is dark-only), snap the class back rather than persisting an
214
+ // unsupported variant.
215
+ const supportedSet = new Set((VARIANTS_BY_THEME[themeName] ?? 'dark').split(' '))
216
+ const observer = new MutationObserver((mutations) => {
217
+ const classChanged = mutations.some((m) => m.attributeName === 'class')
218
+ if (!classChanged) {
219
+ return
220
+ }
221
+ const nowDark = html.classList.contains('rp-dark')
222
+ const nextVariant = match(nowDark)
223
+ .with(true, (): ThemeVariant => 'dark')
224
+ .otherwise((): ThemeVariant => 'light')
225
+ if (html.dataset.cpVariant === nextVariant) {
226
+ return
227
+ }
228
+ if (!supportedSet.has(nextVariant)) {
229
+ // Unsupported variant for the active theme — snap Rspress's
230
+ // class state back to match the current `data-cp-variant` and
231
+ // do NOT persist the illegal flip.
232
+ //
233
+ // The classList mutation below re-fires this observer. The
234
+ // re-entrant pass hits the `dataset.cpVariant === nextVariant`
235
+ // early return above — safe, just one extra callback.
236
+ if (html.dataset.cpVariant === 'dark') {
237
+ html.classList.add('rp-dark', 'dark')
238
+ html.dataset.dark = 'true'
239
+ } else {
240
+ html.classList.remove('rp-dark', 'dark')
241
+ html.dataset.dark = 'false'
242
+ }
243
+ return
244
+ }
245
+ html.dataset.cpVariant = nextVariant
246
+ persistVariant(nextVariant)
247
+ clearColorOverrides(html)
248
+ if (hasColors) {
249
+ applyColorOverrides(html, colors)
250
+ }
251
+ if (nextVariant === 'dark' && hasDarkColors) {
252
+ applyColorOverrides(html, darkColors)
253
+ }
254
+ // Re-sync the favicon — brand colour is constant across variants for
255
+ // built-in themes, but surface overrides on the dark variant can
256
+ // affect the chip background colour we bake into the SVG.
257
+ syncThemeFavicon(html)
258
+ })
259
+ observer.observe(html, { attributes: true, attributeFilter: ['class'] })
260
+
261
+ const cancelLoader = dismissLoader(html)
262
+
263
+ return () => {
264
+ observer.disconnect()
265
+ cancelLoader()
266
+ }
267
+ }, [])
268
+
269
+ return null
270
+ }
271
+
272
+ export { ThemeProvider as default }
273
+
274
+ /**
275
+ * Resolve the theme name to apply for this render.
276
+ *
277
+ * Returns the persisted localStorage value when it points at a registered
278
+ * theme; falls back to the build-time default otherwise.
279
+ *
280
+ * @private
281
+ * @param persisted - Value read from `localStorage['ciderpress-theme']` (or `null`)
282
+ * @returns Resolved theme name
283
+ */
284
+ function resolveActiveThemeName(persisted: string | null): string {
285
+ if (persisted !== null && REGISTERED_THEME_NAMES.has(persisted)) {
286
+ return persisted
287
+ }
288
+ return __CIDERPRESS_THEME_NAME__
289
+ }
290
+
291
+ /**
292
+ * Resolve the variant to apply for this render. Honours persisted state
293
+ * when it's still valid for the active theme; otherwise uses the theme's
294
+ * own default variant; otherwise the build-time default; otherwise dark.
295
+ *
296
+ * Keep the resolution ladder in sync with:
297
+ * - `packages/ui/src/config.ts` → `buildHeadScriptBody` (head IIFE)
298
+ * - `theme-switcher.tsx` → `applyTheme` (user-driven theme change)
299
+ *
300
+ * @private
301
+ * @param params - Persisted variant, build-time default, active theme name
302
+ * @returns Resolved variant for this render
303
+ */
304
+ function resolveActiveVariant(params: {
305
+ readonly persisted: ThemeVariant | null
306
+ readonly buildTimeDefault: ThemeVariant | null
307
+ readonly themeName: string
308
+ }): ThemeVariant {
309
+ const supported = VARIANTS_BY_THEME[params.themeName]
310
+ const supportedVariants = match(supported)
311
+ .with(P.nullish, () => ['dark'])
312
+ .otherwise((s) => s.split(' '))
313
+ const supportedSet = new Set(supportedVariants)
314
+ if (params.persisted !== null && supportedSet.has(params.persisted)) {
315
+ return params.persisted
316
+ }
317
+ const themeDefault = DEFAULT_VARIANT_BY_THEME[params.themeName]
318
+ if (themeDefault !== undefined && supportedSet.has(themeDefault)) {
319
+ return themeDefault
320
+ }
321
+ if (params.buildTimeDefault !== null && supportedSet.has(params.buildTimeDefault)) {
322
+ return params.buildTimeDefault
323
+ }
324
+ if (supportedSet.has('dark')) {
325
+ return 'dark'
326
+ }
327
+ return 'light'
328
+ }
329
+
330
+ /**
331
+ * Apply the resolved variant to `<html>` — sets `data-cp-variant`,
332
+ * toggles Rspress's `.rp-dark` / `.dark` classes, and mirrors the value
333
+ * into Rspress's `localStorage['rspress-theme-appearance']` key so a
334
+ * server-rendered first paint matches.
335
+ *
336
+ * @private
337
+ * @param html - HTML document element
338
+ * @param variant - Variant to apply
339
+ */
340
+ function applyVariant(html: HTMLElement, variant: ThemeVariant): void {
341
+ html.dataset.cpVariant = variant
342
+ if (variant === 'dark') {
343
+ html.classList.add('rp-dark', 'dark')
344
+ html.dataset.dark = 'true'
345
+ } else {
346
+ html.classList.remove('rp-dark', 'dark')
347
+ html.dataset.dark = 'false'
348
+ }
349
+ try {
350
+ localStorage.setItem('rspress-theme-appearance', variant)
351
+ } catch {
352
+ // storage unavailable
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Persist the user's variant preference so it survives reloads.
358
+ *
359
+ * @private
360
+ * @param variant - Variant to remember
361
+ */
362
+ function persistVariant(variant: ThemeVariant): void {
363
+ try {
364
+ localStorage.setItem('ciderpress-variant', variant)
365
+ } catch {
366
+ // storage unavailable
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Build a flat list of [cssVar, value] pairs from a color overrides object.
372
+ *
373
+ * @private
374
+ * @param colors - Color key-value overrides
375
+ * @returns Array of CSS variable/value pairs
376
+ */
377
+ function resolveColorPairs(colors: Record<string, string>): readonly (readonly [string, string])[] {
378
+ return Object.entries(colors).flatMap(([key, value]) => {
379
+ const vars = (COLOR_VAR_MAP as Record<string, readonly string[] | undefined>)[key]
380
+ if (!Array.isArray(vars)) {
381
+ return []
382
+ }
383
+ return vars.map((cssVar) => [cssVar, value] as const)
384
+ })
385
+ }
386
+
387
+ /**
388
+ * Apply a ThemeColors object as inline CSS custom properties on <html>.
389
+ *
390
+ * @private
391
+ * @param html - HTML document element
392
+ * @param colors - Color key-value overrides to apply
393
+ */
394
+ function applyColorOverrides(html: HTMLElement, colors: Record<string, string>): void {
395
+ const pairs = resolveColorPairs(colors)
396
+ // oxlint-disable-next-line no-unused-expressions -- DOM side effect; for-loops and forEach also banned
397
+ pairs.map(([cssVar, value]) => html.style.setProperty(cssVar, value))
398
+ }
399
+
400
+ /**
401
+ * Remove all color overrides previously set as inline styles.
402
+ *
403
+ * @private
404
+ * @param html - HTML document element
405
+ */
406
+ function clearColorOverrides(html: HTMLElement): void {
407
+ // oxlint-disable-next-line no-unused-expressions -- DOM side effect; for-loops and forEach also banned
408
+ ALL_CSS_VARS.map((cssVar) => html.style.removeProperty(cssVar))
409
+ }
410
+
411
+ /**
412
+ * Safe localStorage read — returns null if storage is unavailable.
413
+ *
414
+ * @private
415
+ * @param key - localStorage key to read
416
+ * @returns Stored value or null
417
+ */
418
+ function safeGetItem(key: string): string | null {
419
+ try {
420
+ return localStorage.getItem(key)
421
+ } catch {
422
+ return null
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Safe localStorage delete — silently ignores storage exceptions.
428
+ *
429
+ * @private
430
+ * @param key - localStorage key to remove
431
+ */
432
+ function safeRemoveItem(key: string): void {
433
+ try {
434
+ localStorage.removeItem(key)
435
+ } catch {
436
+ // storage unavailable
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Read a localStorage entry and parse it as a `ThemeVariant`. Returns
442
+ * `null` when the entry is missing, unparseable, or not a known variant.
443
+ *
444
+ * @private
445
+ * @param key - localStorage key to read
446
+ * @returns Parsed variant or `null`
447
+ */
448
+ function safeGetVariant(key: string): ThemeVariant | null {
449
+ return parseVariant(safeGetItem(key))
450
+ }
451
+
452
+ /**
453
+ * Coerce a raw string into a `ThemeVariant`, or return `null` when the
454
+ * value is not a known variant.
455
+ *
456
+ * @private
457
+ * @param raw - Candidate variant value (may be `null` or arbitrary string)
458
+ * @returns Validated variant or `null`
459
+ */
460
+ function parseVariant(raw: string | null): ThemeVariant | null {
461
+ if (raw === 'dark' || raw === 'light') {
462
+ return raw
463
+ }
464
+ return null
465
+ }
466
+
467
+ /**
468
+ * Parse the build-time theme registry define and return the resolved
469
+ * `{ name, variants, defaultVariant }` entries. Returns an empty array
470
+ * on any malformed input so the caller can intersect against an empty
471
+ * set without crashing.
472
+ *
473
+ * @private
474
+ * @param raw - Raw JSON string from `__CIDERPRESS_THEME_REGISTRY__`
475
+ * @returns Resolved registry entries
476
+ */
477
+ function parseRegistry(raw: string): readonly RegistryEntry[] {
478
+ if (!raw || raw === '""' || raw === 'undefined') {
479
+ return []
480
+ }
481
+ try {
482
+ const parsed: unknown = JSON.parse(raw)
483
+ if (!Array.isArray(parsed)) {
484
+ return []
485
+ }
486
+ return parsed.flatMap(toRegistryEntry)
487
+ } catch {
488
+ return []
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Lift a raw registry entry into a typed `RegistryEntry`, returning an
494
+ * empty array for malformed entries so the caller can `flatMap` cleanly.
495
+ *
496
+ * @private
497
+ * @param entry - Raw entry from the parsed JSON array
498
+ * @returns Single-element array on success, empty array on shape mismatch
499
+ */
500
+ function toRegistryEntry(entry: unknown): readonly RegistryEntry[] {
501
+ if (entry === null || typeof entry !== 'object') {
502
+ return []
503
+ }
504
+ const { name, variants, defaultVariant } = entry as {
505
+ name?: unknown
506
+ variants?: unknown
507
+ defaultVariant?: unknown
508
+ }
509
+ if (typeof name !== 'string') {
510
+ return []
511
+ }
512
+ const resolvedVariants = parseVariantList(variants)
513
+ if (resolvedVariants.length === 0) {
514
+ return [{ name, variants: ['dark'], defaultVariant: 'dark' }]
515
+ }
516
+ const defaultVariantString = match(defaultVariant)
517
+ .with(P.string, (s) => s)
518
+ .otherwise(() => null)
519
+ const resolvedDefault = parseVariant(defaultVariantString)
520
+ const fallbackDefault = resolvedVariants[0] as ThemeVariant
521
+ return [
522
+ {
523
+ name,
524
+ variants: resolvedVariants,
525
+ defaultVariant: resolvedDefault ?? fallbackDefault,
526
+ },
527
+ ]
528
+ }
529
+
530
+ /**
531
+ * Coerce an unknown value into a `readonly ThemeVariant[]`, dropping any
532
+ * entries that don't parse cleanly.
533
+ *
534
+ * @private
535
+ * @param raw - Candidate variant list from the registry JSON
536
+ * @returns Filtered list of known variants
537
+ */
538
+ function parseVariantList(raw: unknown): readonly ThemeVariant[] {
539
+ if (!Array.isArray(raw)) {
540
+ return []
541
+ }
542
+ return raw.flatMap((v) => {
543
+ const candidate = match(v)
544
+ .with(P.string, (s) => s)
545
+ .otherwise(() => null)
546
+ const parsed = parseVariant(candidate)
547
+ if (parsed === null) {
548
+ return []
549
+ }
550
+ return [parsed]
551
+ })
552
+ }
553
+
554
+ /**
555
+ * Parse a JSON build-time define, returning an empty object on failure.
556
+ *
557
+ * @private
558
+ * @param raw - Raw JSON string from build-time define
559
+ * @returns Parsed color overrides object
560
+ */
561
+ function parseColors(raw: string): Record<string, string> {
562
+ if (!raw || raw === '""' || raw === 'undefined') {
563
+ return {}
564
+ }
565
+ try {
566
+ const parsed: unknown = JSON.parse(raw)
567
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
568
+ return {}
569
+ }
570
+ return Object.fromEntries(
571
+ Object.entries(parsed as Record<string, unknown>).filter(
572
+ ([, value]) => typeof value === 'string'
573
+ )
574
+ ) as Record<string, string>
575
+ } catch {
576
+ return {}
577
+ }
578
+ }
579
+
580
+ /**
581
+ * useLayoutEffect on the client, useEffect on the server (avoids SSR warning).
582
+ * Ensures DOM mutations happen synchronously before browser paint.
583
+ *
584
+ * @private
585
+ * @returns The appropriate effect hook for the current environment
586
+ */
587
+ function getIsomorphicEffect(): typeof useLayoutEffect {
588
+ return match(globalThis.window)
589
+ .with(P.nonNullable, () => useLayoutEffect)
590
+ .otherwise(() => useEffect)
591
+ }
592
+
593
+ /**
594
+ * Clear the JS-driven dots animation interval set by loader-dots.js.
595
+ *
596
+ * @private
597
+ */
598
+ function clearDotsInterval(): void {
599
+ const g = globalThis as Record<string, unknown>
600
+ const dotsInterval = g.__cpDotsInterval as ReturnType<typeof setInterval> | undefined
601
+ if (dotsInterval !== undefined) {
602
+ clearInterval(dotsInterval)
603
+ delete g.__cpDotsInterval
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Dismiss the loading overlay with a two-phase approach:
609
+ * 1. Add `cp-loader-fade` class — CSS transitions opacity to 0
610
+ * 2. After transition completes — set `data-cp-ready` attribute — CSS hard removes (display: none)
611
+ *
612
+ * Also clears the JS-driven dots animation interval set by loader-dots.js.
613
+ *
614
+ * Returns a cleanup function that cancels pending timers.
615
+ *
616
+ * @private
617
+ * @param html - HTML document element
618
+ * @returns Cleanup function that cancels pending timers
619
+ */
620
+ function dismissLoader(html: HTMLElement): () => void {
621
+ const fadeTimer = setTimeout(() => {
622
+ html.classList.add('cp-loader-fade')
623
+ }, LOADER_MIN_DISPLAY_MS)
624
+
625
+ const removeTimer = setTimeout(() => {
626
+ html.dataset.cpReady = 'true'
627
+ html.classList.remove('cp-loader-fade')
628
+ clearDotsInterval()
629
+ }, LOADER_MIN_DISPLAY_MS + LOADER_FADE_MS)
630
+
631
+ return () => {
632
+ clearTimeout(fadeTimer)
633
+ clearTimeout(removeTimer)
634
+ html.classList.remove('cp-loader-fade')
635
+ clearDotsInterval()
636
+ }
637
+ }