@ciderpress/ui 1.0.0-rc.12 → 1.0.0-rc.14

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 (52) hide show
  1. package/dist/head/css/themes/honeycrisp.css +1 -1
  2. package/dist/node.mjs +17 -13
  3. package/dist/plugins/mermaid/MermaidRenderer.tsx +2 -2
  4. package/dist/theme/components/home/hero-banner.tsx +225 -0
  5. package/dist/theme/components/home/hero-demo.css +492 -61
  6. package/dist/theme/components/home/hero-demo.test.tsx +159 -0
  7. package/dist/theme/components/home/hero-demo.tsx +335 -32
  8. package/dist/theme/components/home/hero.css +142 -9
  9. package/dist/theme/components/home/hero.test.tsx +105 -0
  10. package/dist/theme/components/home/hero.tsx +201 -4
  11. package/dist/theme/components/home/home-visual.css +98 -0
  12. package/dist/theme/components/home/home-visual.test.tsx +27 -0
  13. package/dist/theme/components/home/home-visual.tsx +20 -13
  14. package/dist/theme/components/home/layout.tsx +125 -1
  15. package/dist/theme/components/home/split.css +2 -2
  16. package/dist/theme/components/home/tabs.css +1 -1
  17. package/dist/theme/components/home/trust-strip.css +12 -0
  18. package/dist/theme/components/home/trust-strip.test.tsx +33 -0
  19. package/dist/theme/components/home/trust-strip.tsx +76 -15
  20. package/dist/theme/components/nav/layout.tsx +10 -14
  21. package/dist/theme/components/nav/nav-logo.tsx +2 -3
  22. package/dist/theme/components/sidebar/sidebar-filter.test.ts +49 -9
  23. package/dist/theme/components/sidebar/sidebar-filter.ts +24 -4
  24. package/dist/theme/index.tsx +8 -1
  25. package/dist/theme/lib/rich-text-parse.ts +0 -2
  26. package/dist/theme/lib/theme-favicon.ts +0 -3
  27. package/dist/theme/styles/themes/honeycrisp.css +20 -20
  28. package/package.json +20 -19
  29. package/src/theme/components/home/hero-banner.tsx +225 -0
  30. package/src/theme/components/home/hero-demo.css +492 -61
  31. package/src/theme/components/home/hero-demo.test.tsx +159 -0
  32. package/src/theme/components/home/hero-demo.tsx +335 -32
  33. package/src/theme/components/home/hero.css +142 -9
  34. package/src/theme/components/home/hero.test.tsx +105 -0
  35. package/src/theme/components/home/hero.tsx +201 -4
  36. package/src/theme/components/home/home-visual.css +98 -0
  37. package/src/theme/components/home/home-visual.test.tsx +27 -0
  38. package/src/theme/components/home/home-visual.tsx +20 -13
  39. package/src/theme/components/home/layout.tsx +125 -1
  40. package/src/theme/components/home/split.css +2 -2
  41. package/src/theme/components/home/tabs.css +1 -1
  42. package/src/theme/components/home/trust-strip.css +12 -0
  43. package/src/theme/components/home/trust-strip.test.tsx +33 -0
  44. package/src/theme/components/home/trust-strip.tsx +76 -15
  45. package/src/theme/components/nav/layout.tsx +10 -14
  46. package/src/theme/components/nav/nav-logo.tsx +2 -3
  47. package/src/theme/components/sidebar/sidebar-filter.test.ts +49 -9
  48. package/src/theme/components/sidebar/sidebar-filter.ts +24 -4
  49. package/src/theme/index.tsx +8 -1
  50. package/src/theme/lib/rich-text-parse.ts +0 -2
  51. package/src/theme/lib/theme-favicon.ts +0 -3
  52. package/src/theme/styles/themes/honeycrisp.css +20 -20
@@ -23,9 +23,7 @@ export function syncThemeFavicon(html: HTMLElement): void {
23
23
  const surface = resolveSurfaceColor(html)
24
24
  const dataUri = buildIconDataUri(brand, shadow, surface)
25
25
  const link = ensureFaviconLink()
26
- // oxlint-disable-next-line functional/immutable-data -- boundary mutation: updating link element href
27
26
  link.type = 'image/svg+xml'
28
- // oxlint-disable-next-line functional/immutable-data -- boundary mutation: updating link element href
29
27
  link.href = dataUri
30
28
  }
31
29
 
@@ -118,7 +116,6 @@ function ensureFaviconLink(): HTMLLinkElement {
118
116
  return existing
119
117
  }
120
118
  const link = globalThis.document.createElement('link')
121
- // oxlint-disable-next-line functional/immutable-data -- boundary mutation: initialising newly created link element
122
119
  link.rel = 'icon'
123
120
  globalThis.document.head.append(link)
124
121
  return link
@@ -288,7 +288,7 @@ html[data-cp-theme='honeycrisp'][data-cp-variant='dark'] {
288
288
  }
289
289
 
290
290
  html[data-cp-theme='honeycrisp'][data-cp-variant='light'] {
291
- --cp-c-brand-1: #dc2626;
291
+ --cp-c-brand-1: #d02020;
292
292
  --cp-c-brand-2: #b91c1c;
293
293
  --cp-c-brand-3: #7f1d1d;
294
294
  --cp-c-brand-fg: #ffffff;
@@ -301,15 +301,15 @@ html[data-cp-theme='honeycrisp'][data-cp-variant='light'] {
301
301
  --cp-c-warn: #d97706;
302
302
  --cp-c-info: #2563eb;
303
303
  --cp-c-muted: #888888;
304
- --cp-c-bg: #ffffff;
305
- --cp-c-bg-alt: #f9f9f9;
306
- --cp-c-bg-elv: #f5f5f5;
307
- --cp-c-bg-soft: #f0f0f0;
308
- --cp-c-bg-icon: #cccccc;
309
- --cp-c-home-bg: #ffffff;
304
+ --cp-c-bg: #f7f5f0;
305
+ --cp-c-bg-alt: #f1eee7;
306
+ --cp-c-bg-elv: #fffdf8;
307
+ --cp-c-bg-soft: #ebe7dc;
308
+ --cp-c-bg-icon: #d5cebe;
309
+ --cp-c-home-bg: #f7f5f0;
310
310
  --cp-c-overlay-faint: rgba(0, 0, 0, 0.1);
311
- --cp-c-gutter: #f5f5f5;
312
- --cp-code-block-bg: #f5f5f5;
311
+ --cp-c-gutter: #f1eee7;
312
+ --cp-code-block-bg: #f1eee7;
313
313
  --cp-c-text-1: #1a1a1a;
314
314
  --cp-c-text-2: rgba(26, 26, 26, 0.72);
315
315
  --cp-c-text-3: rgba(26, 26, 26, 0.48);
@@ -384,7 +384,7 @@ html[data-cp-theme='honeycrisp'][data-cp-variant='light'] {
384
384
  --cp-oas-deprecated: var(--cp-c-text-3);
385
385
  --cp-oas-required: #dc2626;
386
386
  --cp-button-brand-bg: #b91c1c;
387
- --cp-button-brand-hover-bg: #dc2626;
387
+ --cp-button-brand-hover-bg: #d02020;
388
388
  --cp-button-brand-active-bg: #7f1d1d;
389
389
  --cp-button-brand-text: #ffffff;
390
390
  --cp-s-1: 1px;
@@ -535,12 +535,12 @@ html[data-cp-theme='honeycrisp'][data-cp-variant='light'] {
535
535
  --cp-blur-base: 8px;
536
536
  --cp-gradient-brand: linear-gradient(135deg, var(--cp-c-brand-1), var(--cp-c-brand-3));
537
537
  --cp-gradient-hero-title: linear-gradient(135deg, var(--cp-c-brand-1), var(--cp-c-brand-light));
538
- --rp-c-bg: #ffffff;
539
- --rp-c-bg-alt: #f9f9f9;
540
- --rp-c-bg-dark: #f5f5f5;
541
- --rp-c-bg-mute: #ffffff;
542
- --rp-c-bg-soft: #f0f0f0;
543
- --rp-c-brand: #dc2626;
538
+ --rp-c-bg: #f7f5f0;
539
+ --rp-c-bg-alt: #f1eee7;
540
+ --rp-c-bg-dark: #f1eee7;
541
+ --rp-c-bg-mute: #f7f5f0;
542
+ --rp-c-bg-soft: #ebe7dc;
543
+ --rp-c-brand: #d02020;
544
544
  --rp-c-brand-light: #ef4444;
545
545
  --rp-c-brand-lighter: #fca5a5;
546
546
  --rp-c-brand-dark: #b91c1c;
@@ -549,15 +549,15 @@ html[data-cp-theme='honeycrisp'][data-cp-variant='light'] {
549
549
  --rp-c-divider: #e2e2e2;
550
550
  --rp-c-divider-dark: #d0d0d0;
551
551
  --rp-c-divider-light: #e2e2e2;
552
- --rp-c-link: #dc2626;
552
+ --rp-c-link: #d02020;
553
553
  --rp-c-text-1: #1a1a1a;
554
554
  --rp-c-text-2: rgba(26, 26, 26, 0.72);
555
555
  --rp-c-text-3: rgba(26, 26, 26, 0.48);
556
556
  --rp-c-text-4: rgba(26, 26, 26, 0.48);
557
557
  --rp-c-text-code: #1a1a1a;
558
- --rp-code-block-bg: #f5f5f5;
559
- --rp-home-background-bg: #ffffff;
560
- --rp-home-feature-bg: #f0f0f0;
558
+ --rp-code-block-bg: #f1eee7;
559
+ --rp-home-background-bg: #f7f5f0;
560
+ --rp-home-feature-bg: #ebe7dc;
561
561
  --rp-home-hero-secondary-color: rgba(26, 26, 26, 0.72);
562
562
  --rp-home-hero-title-color: #1a1a1a;
563
563
  --rp-font-family-base:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciderpress/ui",
3
- "version": "1.0.0-rc.12",
3
+ "version": "1.0.0-rc.14",
4
4
  "description": "Rspress plugin, theme components, and styles for ciderpress",
5
5
  "keywords": [
6
6
  "ciderpress",
@@ -47,43 +47,44 @@
47
47
  "dependencies": {
48
48
  "@iconify-json/catppuccin": "^1.2.17",
49
49
  "@iconify-json/devicon": "^1.2.62",
50
- "@iconify-json/logos": "^1.2.11",
51
- "@iconify-json/material-icon-theme": "^1.2.69",
50
+ "@iconify-json/logos": "^1.2.13",
51
+ "@iconify-json/material-icon-theme": "^1.2.70",
52
52
  "@iconify-json/mdi": "^1.2.3",
53
53
  "@iconify-json/pixel": "^1.2.2",
54
- "@iconify-json/pixelarticons": "^1.2.7",
55
- "@iconify-json/simple-icons": "^1.2.89",
54
+ "@iconify-json/pixelarticons": "^1.2.8",
55
+ "@iconify-json/simple-icons": "^1.2.93",
56
56
  "@iconify-json/skill-icons": "^1.2.4",
57
- "@iconify-json/vscode-icons": "^1.2.65",
57
+ "@iconify-json/vscode-icons": "^1.2.73",
58
58
  "@iconify/react": "^6.0.2",
59
59
  "@mermaid-js/layout-elk": "^0.2.2",
60
60
  "@rspress/plugin-sitemap": "^2.0.19",
61
61
  "clsx": "^2.1.1",
62
- "katex": "^0.17.0",
63
- "massaman": "^0.3.0",
64
- "mermaid": "^11.16.0",
62
+ "katex": "^0.18.4",
63
+ "massaman": "^0.5.0",
64
+ "mermaid": "^11.16.1",
65
65
  "openapi-sampler": "^1.7.4",
66
- "react-aria-components": "^1.19.0",
66
+ "react-aria-components": "^1.20.0",
67
67
  "ts-morph": "^28.0.0",
68
68
  "unist-util-visit": "^5.1.0",
69
- "@ciderpress/config": "1.0.0-rc.9",
70
- "@ciderpress/theme": "1.0.0-rc.5"
69
+ "@ciderpress/config": "1.0.0-rc.11",
70
+ "@ciderpress/theme": "1.0.0-rc.6"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@ladle/react": "^5.1.1",
74
- "@rslib/core": "0.23.1",
74
+ "@rslib/core": "^0.23.2",
75
75
  "@rspress/core": "^2.0.19",
76
- "@types/react": "^19.2.17",
77
- "@types/react-dom": "^19.2.3",
78
- "esbuild": "^0.28.1",
76
+ "@types/react": "^19.2.18",
77
+ "@types/react-dom": "^19.2.4",
78
+ "esbuild": "^0.28.2",
79
79
  "geist": "^1.7.2",
80
- "react": "^19.2.7",
81
- "react-dom": "^19.2.7",
80
+ "happy-dom": "^20.12.0",
81
+ "react": "^19.2.8",
82
+ "react-dom": "^19.2.8",
82
83
  "rspress-plugin-devkit": "^1.0.0",
83
84
  "rspress-plugin-file-tree": "^1.0.5",
84
85
  "rspress-plugin-katex": "^1.0.1",
85
86
  "rspress-plugin-supersub": "^1.0.0",
86
- "typescript": "^6.0.3",
87
+ "typescript": "^7.0.2",
87
88
  "vitest": "^4.1.10"
88
89
  },
89
90
  "peerDependencies": {
@@ -0,0 +1,225 @@
1
+ import type { BannerFn, CiderpressConfig, ImageSource, LogoContext } from '@ciderpress/config'
2
+ // oxlint-disable-next-line import/no-unresolved -- alias provided by createRspressConfig's resolve.alias
3
+ import userConfigModule from '@ciderpress/internal/user-config'
4
+ import { BRAND_COLORS, DEFAULT_THEME_NAME } from '@ciderpress/theme'
5
+ import React, { useEffect, useState } from 'react'
6
+
7
+ import { withMountBase } from '../../lib/with-mount-base.ts'
8
+
9
+ const COLOR_VARS = Object.freeze({
10
+ brand: '--rp-c-brand',
11
+ brandHover: '--rp-c-brand-dark',
12
+ brandSoft: '--rp-c-brand-tint',
13
+ bg: '--rp-c-bg',
14
+ text: '--rp-c-text-1',
15
+ })
16
+
17
+ const DEFAULT_BRAND = BRAND_COLORS[DEFAULT_THEME_NAME]
18
+ const FALLBACK_COLORS = Object.freeze({
19
+ brand: DEFAULT_BRAND.primary,
20
+ brandHover: DEFAULT_BRAND.hover,
21
+ brandSoft: DEFAULT_BRAND.soft,
22
+ bg: '#ffffff',
23
+ text: '#1f2937',
24
+ })
25
+ const FALLBACK_THEME: LogoContext = Object.freeze({
26
+ name: DEFAULT_THEME_NAME,
27
+ variant: 'dark',
28
+ isDark: true,
29
+ colors: FALLBACK_COLORS,
30
+ })
31
+
32
+ /**
33
+ * Resolve and render a function-valued `brand.banner` with the live theme context.
34
+ *
35
+ * @returns Theme-aware banner content, an image, or null.
36
+ */
37
+ export function HeroBanner(): React.ReactElement | null {
38
+ const banner = readBannerConfig(userConfigModule)
39
+ const theme = useThemeContext(typeof banner === 'function')
40
+
41
+ if (typeof banner !== 'function') {
42
+ return null
43
+ }
44
+
45
+ return <>{renderBanner({ banner, theme })}</>
46
+ }
47
+
48
+ interface RenderBannerParams {
49
+ readonly banner: BannerFn
50
+ readonly theme: LogoContext
51
+ }
52
+
53
+ /**
54
+ * Invoke a banner function and normalize image returns into elements.
55
+ *
56
+ * @private
57
+ * @param params - Banner function and active theme context.
58
+ * @returns Renderable banner content.
59
+ */
60
+ function renderBanner(params: RenderBannerParams): React.ReactNode {
61
+ const result = params.banner({ theme: params.theme })
62
+ if (typeof result === 'string') {
63
+ return <img src={withMountBase(result)} alt="" />
64
+ }
65
+ if (isImageSource(result)) {
66
+ return (
67
+ <img
68
+ src={withMountBase(result.src)}
69
+ alt={result.alt ?? ''}
70
+ width={result.width}
71
+ height={result.height}
72
+ />
73
+ )
74
+ }
75
+ return result as React.ReactNode
76
+ }
77
+
78
+ /**
79
+ * Read a function-valued banner from the bundled user config.
80
+ *
81
+ * @private
82
+ * @param mod - User config module or its default export.
83
+ * @returns Banner function when configured.
84
+ */
85
+ function readBannerConfig(mod: unknown): BannerFn | undefined {
86
+ if (mod === null || mod === undefined) {
87
+ return undefined
88
+ }
89
+ const record = mod as Record<string, unknown>
90
+ const candidate = readConfigExport({ mod, record })
91
+ const brand = candidate.brand
92
+ if (brand === undefined || typeof brand.banner !== 'function') {
93
+ return undefined
94
+ }
95
+ return brand.banner
96
+ }
97
+
98
+ interface ReadConfigExportParams {
99
+ readonly mod: unknown
100
+ readonly record: Record<string, unknown>
101
+ }
102
+
103
+ /**
104
+ * Normalize default and direct user-config module exports.
105
+ *
106
+ * @private
107
+ * @param params - Raw module and record view.
108
+ * @returns Partial Ciderpress config.
109
+ */
110
+ function readConfigExport(params: ReadConfigExportParams): Partial<CiderpressConfig> {
111
+ if (params.record.default !== null && params.record.default !== undefined) {
112
+ return params.record.default as Partial<CiderpressConfig>
113
+ }
114
+ return params.mod as Partial<CiderpressConfig>
115
+ }
116
+
117
+ /**
118
+ * Subscribe to theme changes only when a runtime banner needs them.
119
+ *
120
+ * @private
121
+ * @param enabled - Whether the banner is function-valued.
122
+ * @returns Server-safe default context followed by the live browser context.
123
+ */
124
+ function useThemeContext(enabled: boolean): LogoContext {
125
+ const [theme, setTheme] = useState<LogoContext>(FALLBACK_THEME)
126
+
127
+ useEffect(() => {
128
+ if (!enabled) {
129
+ return
130
+ }
131
+ const html = globalThis.document.documentElement
132
+ function update(): void {
133
+ return setTheme(readThemeContext(html))
134
+ }
135
+ update()
136
+
137
+ const observer = new MutationObserver(update)
138
+ observer.observe(html, {
139
+ attributes: true,
140
+ attributeFilter: ['data-cp-theme', 'data-cp-variant', 'class', 'style'],
141
+ })
142
+ return () => observer.disconnect()
143
+ }, [enabled])
144
+
145
+ return theme
146
+ }
147
+
148
+ /**
149
+ * Distinguish a structured image source from a React element.
150
+ *
151
+ * @private
152
+ * @param value - Banner return value.
153
+ * @returns Whether the value is an image source object.
154
+ */
155
+ function isImageSource(value: unknown): value is Exclude<ImageSource, string> {
156
+ if (value === null || typeof value !== 'object') {
157
+ return false
158
+ }
159
+ const record = value as Record<string, unknown>
160
+ if ('$$typeof' in record) {
161
+ return false
162
+ }
163
+ return typeof record.src === 'string'
164
+ }
165
+
166
+ /**
167
+ * Build the live theme context passed to `brand.banner`.
168
+ *
169
+ * @private
170
+ * @param html - Document root element.
171
+ * @returns Current theme snapshot.
172
+ */
173
+ function readThemeContext(html: HTMLElement): LogoContext {
174
+ const variant = readVariant(html)
175
+ const name = readThemeName(html)
176
+ const styles = globalThis.window.getComputedStyle(html)
177
+ function read(cssVar: string, fallback: string): string {
178
+ const raw = styles.getPropertyValue(cssVar).trim()
179
+ if (raw.length === 0) {
180
+ return fallback
181
+ }
182
+ return raw
183
+ }
184
+
185
+ return {
186
+ name,
187
+ variant,
188
+ isDark: variant === 'dark',
189
+ colors: {
190
+ brand: read(COLOR_VARS.brand, FALLBACK_COLORS.brand),
191
+ brandHover: read(COLOR_VARS.brandHover, FALLBACK_COLORS.brandHover),
192
+ brandSoft: read(COLOR_VARS.brandSoft, FALLBACK_COLORS.brandSoft),
193
+ bg: read(COLOR_VARS.bg, FALLBACK_COLORS.bg),
194
+ text: read(COLOR_VARS.text, FALLBACK_COLORS.text),
195
+ },
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Read the active color variant from the document root.
201
+ *
202
+ * @private
203
+ * @param html - Document root element.
204
+ * @returns Active variant.
205
+ */
206
+ function readVariant(html: HTMLElement): 'light' | 'dark' {
207
+ if (html.dataset.cpVariant === 'light') {
208
+ return 'light'
209
+ }
210
+ return 'dark'
211
+ }
212
+
213
+ /**
214
+ * Read the active theme name from the document root.
215
+ *
216
+ * @private
217
+ * @param html - Document root element.
218
+ * @returns Active theme name.
219
+ */
220
+ function readThemeName(html: HTMLElement): string {
221
+ if (typeof html.dataset.cpTheme === 'string') {
222
+ return html.dataset.cpTheme
223
+ }
224
+ return DEFAULT_THEME_NAME
225
+ }