@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,262 @@
1
+ /* oxlint-disable no-ternary -- raw-copied file; relaxed rules per packages/ui/CLAUDE.md */
2
+ import type { LogoContext, LogoFn, LogoImage, CiderpressConfig } from '@ciderpress/config'
3
+ // oxlint-disable-next-line import/no-unresolved -- alias provided by createRspressConfig's resolve.alias
4
+ import userConfigModule from '@ciderpress/internal/user-config'
5
+ import { BRAND_COLORS, DEFAULT_THEME_NAME } from '@ciderpress/theme'
6
+ import React, { useEffect, useState } from 'react'
7
+ import { createPortal } from 'react-dom'
8
+
9
+ import { CiderpressLogo } from '../shared/ciderpress-logo'
10
+
11
+ import './nav-logo.css'
12
+
13
+ /**
14
+ * CSS custom property names read from `<html>` to populate `LogoContext.colors`.
15
+ * Kept in sync with the `--rp-c-*` variables emitted by the active theme's
16
+ * stylesheet.
17
+ */
18
+ const COLOR_VARS = Object.freeze({
19
+ brand: '--rp-c-brand',
20
+ brandHover: '--rp-c-brand-dark',
21
+ brandSoft: '--rp-c-brand-tint',
22
+ bg: '--rp-c-bg',
23
+ text: '--rp-c-text-1',
24
+ })
25
+
26
+ /**
27
+ * Fallback colors used during SSR / before the first paint, where
28
+ * `getComputedStyle` is unavailable or returns empty strings.
29
+ */
30
+ const DEFAULT_BRAND = BRAND_COLORS[DEFAULT_THEME_NAME]
31
+ const FALLBACK_COLORS = Object.freeze({
32
+ brand: DEFAULT_BRAND.primary,
33
+ brandHover: DEFAULT_BRAND.hover,
34
+ brandSoft: DEFAULT_BRAND.soft,
35
+ bg: '#ffffff',
36
+ text: '#1f2937',
37
+ })
38
+
39
+ /**
40
+ * `NavLogo` — Rspress-aware logo slot that handles all three logo configs.
41
+ *
42
+ * - Reads `userConfig.logo` from the bundled user config via the
43
+ * `@ciderpress/internal/user-config` alias.
44
+ * - Subscribes to `data-cp-theme` / `data-cp-variant` mutations on `<html>`
45
+ * so a function-form `logo` re-runs when the theme switches.
46
+ * - Portals into Rspress's `.rp-nav__title__link` element so the rendered
47
+ * logo sits inside the same wrapper as Rspress's stock title/logo.
48
+ *
49
+ * Branching:
50
+ * - `logo` is a `string` → Rspress already renders an `<img>` for it via
51
+ * its native `logo` config field; this slot renders `null`.
52
+ * - `logo` is a function → call with the live `LogoContext`. If the return
53
+ * value is a `LogoImage`-shaped object, spread onto `<img>`. Otherwise
54
+ * render as a React node.
55
+ * - `logo` is missing → render the default themed `<CiderpressLogo />`.
56
+ *
57
+ * @returns Portaled logo element or null
58
+ */
59
+ export function NavLogo(): React.ReactElement | null {
60
+ const [target, setTarget] = useState<HTMLElement | null>(null)
61
+ const [themeContext, setThemeContext] = useState<LogoContext | null>(null)
62
+
63
+ useEffect(() => {
64
+ const html = globalThis.document.documentElement
65
+ setThemeContext(readThemeContext(html))
66
+
67
+ function findTarget() {
68
+ return globalThis.document.querySelector('.rp-nav__title__link') as HTMLElement | null
69
+ }
70
+
71
+ // Try immediately, then poll on requestAnimationFrame ticks until the
72
+ // nav renders. Rspress's CSR nav can mount AFTER this effect fires,
73
+ // so a single querySelector at effect time misses the element. We
74
+ // belt-and-suspenders with both rAF polling AND MutationObserver in
75
+ // case the polling stops before the nav appears (e.g. tab inactive).
76
+ // oxlint-disable-next-line functional/no-let -- raf id retained for cleanup
77
+ let rafId: number | null = null
78
+ // oxlint-disable-next-line functional/no-let -- captured below for closure equality
79
+ let resolved = false
80
+
81
+ function tryResolve() {
82
+ if (resolved) {
83
+ return
84
+ }
85
+ const link = findTarget()
86
+ if (link !== null) {
87
+ // oxlint-disable-next-line functional/immutable-data -- flag flip to stop polling
88
+ resolved = true
89
+ setTarget(link)
90
+ return
91
+ }
92
+ rafId = globalThis.requestAnimationFrame(tryResolve)
93
+ }
94
+ tryResolve()
95
+
96
+ const observer = new MutationObserver(() => {
97
+ setThemeContext(readThemeContext(html))
98
+ if (!resolved) {
99
+ tryResolve()
100
+ }
101
+ })
102
+ observer.observe(html, {
103
+ attributes: true,
104
+ attributeFilter: ['data-cp-theme', 'data-cp-variant', 'class', 'style'],
105
+ childList: true,
106
+ subtree: true,
107
+ })
108
+
109
+ return () => {
110
+ observer.disconnect()
111
+ if (rafId !== null) {
112
+ globalThis.cancelAnimationFrame(rafId)
113
+ }
114
+ }
115
+ }, [])
116
+
117
+ const logoConfig = readLogoConfig(userConfigModule)
118
+
119
+ // String-form user logos are handled by Rspress's native <img>. NavLogo
120
+ // takes over for both the default case (theme-aware CiderpressLogo SVG) and
121
+ // function-form logos (user-defined render). The static `/logo.svg` from
122
+ // Rspress's <img> renders immediately on first paint; once NavLogo's
123
+ // portal mounts, CSS hides the static img so only the theme-aware version
124
+ // shows. Theme color flips via `currentColor` reading from `--rp-c-brand`.
125
+ if (typeof logoConfig === 'string') {
126
+ return null
127
+ }
128
+
129
+ if (target === null || themeContext === null) {
130
+ return null
131
+ }
132
+
133
+ // Raw-copied file — plain conditional, no ts-pattern (see packages/ui/CLAUDE.md).
134
+ const rendered =
135
+ typeof logoConfig === 'function' ? (
136
+ renderLogoFn({ fn: logoConfig, theme: themeContext })
137
+ ) : (
138
+ <CiderpressLogo />
139
+ )
140
+
141
+ return createPortal(<span className="cp-nav-logo">{rendered}</span>, target)
142
+ }
143
+
144
+ export { NavLogo as default }
145
+
146
+ /**
147
+ * Extract the `logo` field from the bundled user config module. Tolerates
148
+ * default-exports, named-exports, and the empty stub fallback.
149
+ *
150
+ * @private
151
+ * @param mod - Module imported from `@ciderpress/internal/user-config`
152
+ * @returns The `logo` value or `undefined` when none is configured
153
+ */
154
+ function readLogoConfig(mod: unknown): string | LogoFn | undefined {
155
+ if (mod === null || mod === undefined) {
156
+ return undefined
157
+ }
158
+ const asRecord = mod as Record<string, unknown>
159
+ const candidate =
160
+ asRecord.default !== null && asRecord.default !== undefined
161
+ ? (asRecord.default as Partial<CiderpressConfig>)
162
+ : (mod as Partial<CiderpressConfig>)
163
+ const { logo } = candidate
164
+ if (typeof logo === 'string') {
165
+ return logo
166
+ }
167
+ if (typeof logo === 'function') {
168
+ return logo
169
+ }
170
+ return undefined
171
+ }
172
+
173
+ interface RenderLogoFnParams {
174
+ readonly fn: LogoFn
175
+ readonly theme: LogoContext
176
+ }
177
+
178
+ /**
179
+ * Invoke the user's logo function and turn its return value into a
180
+ * renderable React node.
181
+ *
182
+ * `LogoImage` returns are detected structurally (object with a `src` key
183
+ * that isn't already a React element) and spread onto an `<img>` element
184
+ * matching Rspress's logo classes so styling stays consistent. Anything
185
+ * else is rendered as a React node directly.
186
+ *
187
+ * @private
188
+ * @param params - User function + live theme context
189
+ * @returns React node or null
190
+ */
191
+ function renderLogoFn(params: RenderLogoFnParams): React.ReactNode {
192
+ const result = params.fn({ theme: params.theme })
193
+ if (isLogoImage(result)) {
194
+ return (
195
+ <img
196
+ src={result.src}
197
+ alt={result.alt ?? 'logo'}
198
+ width={result.width}
199
+ height={result.height}
200
+ className="rspress-logo rp-nav__title__logo-image"
201
+ />
202
+ )
203
+ }
204
+ return result as React.ReactNode
205
+ }
206
+
207
+ /**
208
+ * Type guard distinguishing a `LogoImage` object from a React element.
209
+ * React elements are objects too, but they carry a `$$typeof` symbol key —
210
+ * any plain object with a string `src` and without `$$typeof` is treated
211
+ * as a `LogoImage`.
212
+ *
213
+ * @private
214
+ * @param value - Return value from a `LogoFn`
215
+ * @returns True when `value` should be spread onto an `<img>` element
216
+ */
217
+ function isLogoImage(value: unknown): value is LogoImage {
218
+ if (value === null || typeof value !== 'object') {
219
+ return false
220
+ }
221
+ const obj = value as Record<string, unknown>
222
+ if ('$$typeof' in obj) {
223
+ return false
224
+ }
225
+ return typeof obj.src === 'string'
226
+ }
227
+
228
+ /**
229
+ * Build the `LogoContext` passed to the user's logo function. Reads the
230
+ * active theme name and variant from `<html>` data attributes and the
231
+ * resolved brand colors from CSS custom properties.
232
+ *
233
+ * @private
234
+ * @param html - Document root element
235
+ * @returns Live theme context snapshot
236
+ */
237
+ function readThemeContext(html: HTMLElement): LogoContext {
238
+ const variant: 'light' | 'dark' = html.dataset.cpVariant === 'light' ? 'light' : 'dark'
239
+ const name = typeof html.dataset.cpTheme === 'string' ? html.dataset.cpTheme : DEFAULT_THEME_NAME
240
+
241
+ const styles = globalThis.window.getComputedStyle(html)
242
+ function read(cssVar: string, fallback: string): string {
243
+ const raw = styles.getPropertyValue(cssVar).trim()
244
+ if (raw.length === 0) {
245
+ return fallback
246
+ }
247
+ return raw
248
+ }
249
+
250
+ return {
251
+ name,
252
+ variant,
253
+ isDark: variant === 'dark',
254
+ colors: {
255
+ brand: read(COLOR_VARS.brand, FALLBACK_COLORS.brand),
256
+ brandHover: read(COLOR_VARS.brandHover, FALLBACK_COLORS.brandHover),
257
+ brandSoft: read(COLOR_VARS.brandSoft, FALLBACK_COLORS.brandSoft),
258
+ bg: read(COLOR_VARS.bg, FALLBACK_COLORS.bg),
259
+ text: read(COLOR_VARS.text, FALLBACK_COLORS.text),
260
+ },
261
+ }
262
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Theme switcher dropdown styles.
3
+ */
4
+
5
+ .cp-theme-switcher {
6
+ position: relative;
7
+ display: inline-flex;
8
+ align-items: center;
9
+ margin-left: 8px;
10
+ }
11
+
12
+ .cp-theme-switcher__btn {
13
+ display: inline-flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ width: 32px;
17
+ height: 32px;
18
+ padding: 0;
19
+ border: 1px solid var(--cp-c-border);
20
+ border-radius: var(--rp-radius);
21
+ background: var(--cp-c-bg);
22
+ color: var(--cp-c-text-2);
23
+ cursor: pointer;
24
+ transition:
25
+ color 0.2s,
26
+ border-color 0.2s;
27
+ }
28
+
29
+ .cp-theme-switcher__btn:hover {
30
+ color: var(--cp-c-text-1);
31
+ border-color: var(--cp-c-brand-1);
32
+ }
33
+
34
+ .cp-theme-switcher__dropdown {
35
+ position: absolute;
36
+ bottom: calc(100% + 8px);
37
+ right: 0;
38
+ z-index: 100;
39
+ min-width: 160px;
40
+ padding: 4px;
41
+ border: 1px solid var(--cp-c-border);
42
+ border-radius: var(--rp-radius);
43
+ background: var(--cp-c-bg-elv);
44
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
45
+ }
46
+
47
+ .cp-theme-switcher__option {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 8px;
51
+ width: 100%;
52
+ padding: 8px 12px;
53
+ border: none;
54
+ border-radius: var(--rp-radius-small);
55
+ background: none;
56
+ color: var(--cp-c-text-2);
57
+ font-size: 13px;
58
+ font-family: var(--cp-font-family-base);
59
+ cursor: pointer;
60
+ transition:
61
+ background 0.15s,
62
+ color 0.15s;
63
+ }
64
+
65
+ .cp-theme-switcher__option:hover {
66
+ background: var(--cp-c-bg-soft);
67
+ color: var(--cp-c-text-1);
68
+ }
69
+
70
+ .cp-theme-switcher__option--active {
71
+ color: var(--cp-c-brand-1);
72
+ }
73
+
74
+ .cp-theme-switcher__swatch {
75
+ display: inline-block;
76
+ width: 12px;
77
+ height: 12px;
78
+ border-radius: 50%;
79
+ border: 1px solid var(--cp-c-border);
80
+ flex-shrink: 0;
81
+ }
82
+
83
+ .cp-theme-switcher__check {
84
+ margin-left: auto;
85
+ font-size: 11px;
86
+ opacity: 0.7;
87
+ }
88
+
89
+ .cp-theme-switcher__name {
90
+ flex: 1;
91
+ text-align: left;
92
+ }
@@ -0,0 +1,298 @@
1
+ import { DEFAULT_THEME_NAME } from '@ciderpress/theme'
2
+ import { match, P } from 'massaman/match'
3
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
4
+
5
+ import { Icon } from '../shared/icon.tsx'
6
+
7
+ import './theme-switcher.css'
8
+
9
+ declare const __CIDERPRESS_THEME_SWITCHER__: boolean
10
+ declare const __CIDERPRESS_THEME_REGISTRY__: string
11
+
12
+ type Variant = 'dark' | 'light'
13
+
14
+ interface ThemeOption {
15
+ readonly name: string
16
+ readonly label: string
17
+ readonly swatch: string
18
+ readonly variants: readonly Variant[]
19
+ readonly defaultVariant: Variant
20
+ }
21
+
22
+ const THEME_OPTIONS: readonly ThemeOption[] = parseThemeRegistry(__CIDERPRESS_THEME_REGISTRY__)
23
+
24
+ const VALID_THEME_NAMES = new Set(THEME_OPTIONS.map((t) => t.name))
25
+
26
+ const FALLBACK_THEME = match(THEME_OPTIONS[0])
27
+ .with(P.nullish, () => DEFAULT_THEME_NAME as string)
28
+ .otherwise((entry) => entry.name)
29
+
30
+ /**
31
+ * ThemeSwitcher — dropdown button for switching between registered themes.
32
+ * Only renders when `__CIDERPRESS_THEME_SWITCHER__` build-time define is true.
33
+ *
34
+ * Switching a theme updates `data-cp-theme` and `data-cp-variants` on
35
+ * `<html>`. When the newly active theme does not declare the user's
36
+ * previously selected variant, falls back to the new theme's
37
+ * `defaultVariant` and mirrors that to Rspress's `.rp-dark` class.
38
+ *
39
+ * @returns React element or null when theme switching is disabled
40
+ */
41
+ export function ThemeSwitcher(): React.ReactElement | null {
42
+ const [isOpen, setIsOpen] = useState(false)
43
+ const [activeTheme, setActiveTheme] = useState(initialThemeName)
44
+ const containerRef = useRef<HTMLDivElement>(null)
45
+
46
+ const handleToggle = useCallback(() => {
47
+ setIsOpen((prev) => !prev)
48
+ }, [])
49
+
50
+ const handleSelect = useCallback((theme: ThemeOption) => {
51
+ setActiveTheme(theme.name)
52
+ applyTheme(theme)
53
+ setIsOpen(false)
54
+ }, [])
55
+
56
+ useEffect(() => {
57
+ function handleClickOutside(event: MouseEvent): void {
58
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
59
+ setIsOpen(false)
60
+ }
61
+ }
62
+
63
+ function handleEscape(event: KeyboardEvent): void {
64
+ if (event.key === 'Escape') {
65
+ setIsOpen(false)
66
+ }
67
+ }
68
+
69
+ document.addEventListener('mousedown', handleClickOutside)
70
+ document.addEventListener('keydown', handleEscape)
71
+
72
+ return () => {
73
+ document.removeEventListener('mousedown', handleClickOutside)
74
+ document.removeEventListener('keydown', handleEscape)
75
+ }
76
+ }, [])
77
+
78
+ if (!__CIDERPRESS_THEME_SWITCHER__) {
79
+ return null
80
+ }
81
+
82
+ return (
83
+ <div className="cp-theme-switcher" ref={containerRef}>
84
+ <button
85
+ className="cp-theme-switcher__btn"
86
+ onClick={handleToggle}
87
+ aria-label="Switch theme"
88
+ type="button"
89
+ >
90
+ <Icon icon="mdi:palette-outline" width={16} height={16} />
91
+ </button>
92
+ {isOpen && (
93
+ <div className="cp-theme-switcher__dropdown">
94
+ {THEME_OPTIONS.map((theme) => (
95
+ <button
96
+ key={theme.name}
97
+ className={optionClassName(activeTheme === theme.name)}
98
+ onClick={() => handleSelect(theme)}
99
+ type="button"
100
+ >
101
+ <span
102
+ className="cp-theme-switcher__swatch"
103
+ style={{ backgroundColor: theme.swatch }}
104
+ />
105
+ <span className="cp-theme-switcher__name">{theme.label}</span>
106
+ {activeTheme === theme.name && (
107
+ <span className="cp-theme-switcher__check">{'✓'}</span>
108
+ )}
109
+ </button>
110
+ ))}
111
+ </div>
112
+ )}
113
+ </div>
114
+ )
115
+ }
116
+
117
+ /**
118
+ * Resolve the active theme name for first render. Reads
119
+ * `localStorage['ciderpress-theme']` when available; falls back to the first
120
+ * registered theme when the value is missing or stale.
121
+ *
122
+ * @private
123
+ * @returns Resolved theme name
124
+ */
125
+ function initialThemeName(): string {
126
+ if (globalThis.window === undefined) {
127
+ return FALLBACK_THEME
128
+ }
129
+ try {
130
+ return sanitizeThemeName(globalThis.localStorage.getItem('ciderpress-theme'))
131
+ } catch {
132
+ return FALLBACK_THEME
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Validate a stored theme name, rejecting unknown values.
138
+ *
139
+ * @private
140
+ * @param raw - Raw theme name from localStorage
141
+ * @returns Sanitized theme name, defaulting to the first registered theme
142
+ */
143
+ function sanitizeThemeName(raw: string | null): string {
144
+ if (!raw) {
145
+ return FALLBACK_THEME
146
+ }
147
+ if (VALID_THEME_NAMES.has(raw)) {
148
+ return raw
149
+ }
150
+ return FALLBACK_THEME
151
+ }
152
+
153
+ /**
154
+ * Build the className string for a theme option button.
155
+ *
156
+ * @private
157
+ * @param isActive - Whether this option is the currently active theme
158
+ * @returns CSS class name string
159
+ */
160
+ function optionClassName(isActive: boolean): string {
161
+ if (isActive) {
162
+ return 'cp-theme-switcher__option cp-theme-switcher__option--active'
163
+ }
164
+ return 'cp-theme-switcher__option'
165
+ }
166
+
167
+ /**
168
+ * Apply a theme by updating the DOM and persisting to localStorage.
169
+ *
170
+ * When the user's current variant is supported by the new theme, that
171
+ * variant is preserved. Otherwise the new theme's `defaultVariant` is
172
+ * applied — and Rspress's `.rp-dark` class is mirrored accordingly so
173
+ * its built-in components stay aligned.
174
+ *
175
+ * Keep the persistence keys (`ciderpress-theme`, `ciderpress-variant`,
176
+ * `rspress-theme-appearance`) and the `.rp-dark` / `data-cp-*` mirror
177
+ * logic in sync with:
178
+ * - `packages/ui/src/config.ts` → `buildHeadScriptBody` (head IIFE)
179
+ * - `theme-provider.tsx` → `resolveActiveVariant` / `applyVariant`
180
+ *
181
+ * @private
182
+ * @param theme - Theme option to apply
183
+ */
184
+ function applyTheme(theme: ThemeOption): void {
185
+ const html = document.documentElement
186
+ html.dataset.cpTheme = theme.name
187
+ html.dataset.cpVariants = theme.variants.join(' ')
188
+ try {
189
+ localStorage.setItem('ciderpress-theme', theme.name)
190
+ } catch {
191
+ // storage unavailable
192
+ }
193
+
194
+ const currentVariant = readCurrentVariant(html)
195
+ const nextVariant = resolveNextVariant(currentVariant, theme)
196
+
197
+ html.dataset.cpVariant = nextVariant
198
+ if (nextVariant === 'dark') {
199
+ html.classList.add('rp-dark', 'dark')
200
+ html.dataset.dark = 'true'
201
+ } else {
202
+ html.classList.remove('rp-dark', 'dark')
203
+ html.dataset.dark = 'false'
204
+ }
205
+ try {
206
+ localStorage.setItem('rspress-theme-appearance', nextVariant)
207
+ localStorage.setItem('ciderpress-variant', nextVariant)
208
+ } catch {
209
+ // storage unavailable
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Pick the variant to apply after a theme switch — preserves the user's
215
+ * current variant when the new theme supports it, otherwise falls back
216
+ * to the new theme's `defaultVariant`.
217
+ *
218
+ * @private
219
+ * @param current - Currently active variant (or `null` when none is set)
220
+ * @param theme - Theme being switched to
221
+ * @returns Resolved variant for the new theme
222
+ */
223
+ function resolveNextVariant(current: Variant | null, theme: ThemeOption): Variant {
224
+ if (current === null) {
225
+ return theme.defaultVariant
226
+ }
227
+ if (theme.variants.includes(current)) {
228
+ return current
229
+ }
230
+ return theme.defaultVariant
231
+ }
232
+
233
+ /**
234
+ * Read the current `data-cp-variant` from `<html>` as a typed value.
235
+ * Returns `null` when no attribute is present or the value isn't a
236
+ * known variant.
237
+ *
238
+ * @private
239
+ * @param html - HTML document element
240
+ * @returns Current variant or `null`
241
+ */
242
+ function readCurrentVariant(html: HTMLElement): Variant | null {
243
+ const raw = html.dataset.cpVariant
244
+ if (raw === 'dark' || raw === 'light') {
245
+ return raw
246
+ }
247
+ return null
248
+ }
249
+
250
+ /**
251
+ * Parse the `__CIDERPRESS_THEME_REGISTRY__` build-time define into a typed
252
+ * list of theme options. Falls back to an empty list when the define is
253
+ * missing or malformed — the switcher then renders no options.
254
+ *
255
+ * @private
256
+ * @param raw - Raw JSON string from the build-time define
257
+ * @returns Validated list of theme options
258
+ */
259
+ function parseThemeRegistry(raw: string): readonly ThemeOption[] {
260
+ if (!raw || raw === '""' || raw === 'undefined') {
261
+ return []
262
+ }
263
+ try {
264
+ const parsed: unknown = JSON.parse(raw)
265
+ if (!Array.isArray(parsed)) {
266
+ return []
267
+ }
268
+ return parsed.filter(isThemeOption)
269
+ } catch {
270
+ return []
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Type-guard that validates a registry entry has the shape expected by
276
+ * the switcher. Any entry missing required fields or carrying the wrong
277
+ * runtime types is dropped silently.
278
+ *
279
+ * @private
280
+ * @param value - Candidate registry entry
281
+ * @returns True when the entry matches the `ThemeOption` shape
282
+ */
283
+ function isThemeOption(value: unknown): value is ThemeOption {
284
+ if (value === null || typeof value !== 'object') {
285
+ return false
286
+ }
287
+ const v = value as Record<string, unknown>
288
+ if (typeof v.name !== 'string' || typeof v.label !== 'string' || typeof v.swatch !== 'string') {
289
+ return false
290
+ }
291
+ if (v.defaultVariant !== 'dark' && v.defaultVariant !== 'light') {
292
+ return false
293
+ }
294
+ if (!Array.isArray(v.variants)) {
295
+ return false
296
+ }
297
+ return v.variants.every((m) => m === 'dark' || m === 'light')
298
+ }
@@ -0,0 +1,45 @@
1
+ .cp-topbar-cta {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: 6px;
6
+ padding: 6px 14px;
7
+ font-size: 12.5px;
8
+ font-weight: 600;
9
+ border-radius: var(--cp-radius-pill);
10
+ background: var(--cp-c-brand-1);
11
+ color: var(--cp-c-brand-fg);
12
+ border: 1px solid var(--cp-c-brand-1);
13
+ text-decoration: none;
14
+ white-space: nowrap;
15
+ transition: all 0.15s ease;
16
+ }
17
+
18
+ .cp-topbar-cta:hover {
19
+ background: var(--cp-c-brand-2);
20
+ border-color: var(--cp-c-brand-2);
21
+ color: var(--cp-c-brand-fg);
22
+ text-decoration: none;
23
+ }
24
+
25
+ /* Hide on narrow viewports — the navbar gets too tight and the CTA
26
+ gets clipped against the right edge. The mobile slide-over menu
27
+ surfaces the same action via `MobileNavCTA` (portalled into the
28
+ nav-screen). */
29
+ @media (max-width: 720px) {
30
+ html .rp-nav .cp-topbar-cta {
31
+ display: none;
32
+ }
33
+ }
34
+
35
+ /* Portalled CTA inside the mobile nav-screen — matches the slide-over's
36
+ padding and stretches full-width like other menu rows. */
37
+ html .cp-mobile-nav-cta {
38
+ padding: 16px 24px 8px;
39
+ }
40
+ html .cp-mobile-nav-cta .cp-topbar-cta {
41
+ display: flex;
42
+ width: 100%;
43
+ padding: 12px 16px;
44
+ font-size: 13px;
45
+ }