@ciderpress/ui 1.0.0-rc.11 → 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 (64) hide show
  1. package/dist/head/css/themes/honeycrisp.css +1 -1
  2. package/dist/node.mjs +719 -26
  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/seo-head-data.test.ts +196 -0
  23. package/dist/theme/components/seo-head-data.ts +490 -0
  24. package/dist/theme/components/seo-head.tsx +102 -0
  25. package/dist/theme/components/shared/IssueLinkIcon.tsx +42 -0
  26. package/dist/theme/components/shared/issue-link.css +74 -0
  27. package/dist/theme/components/sidebar/sidebar-filter.test.ts +49 -9
  28. package/dist/theme/components/sidebar/sidebar-filter.ts +24 -4
  29. package/dist/theme/index.tsx +9 -1
  30. package/dist/theme/lib/rich-text-parse.ts +0 -2
  31. package/dist/theme/lib/seo-url.ts +28 -0
  32. package/dist/theme/lib/theme-favicon.ts +0 -3
  33. package/dist/theme/styles/themes/honeycrisp.css +20 -20
  34. package/package.json +23 -21
  35. package/src/theme/components/home/hero-banner.tsx +225 -0
  36. package/src/theme/components/home/hero-demo.css +492 -61
  37. package/src/theme/components/home/hero-demo.test.tsx +159 -0
  38. package/src/theme/components/home/hero-demo.tsx +335 -32
  39. package/src/theme/components/home/hero.css +142 -9
  40. package/src/theme/components/home/hero.test.tsx +105 -0
  41. package/src/theme/components/home/hero.tsx +201 -4
  42. package/src/theme/components/home/home-visual.css +98 -0
  43. package/src/theme/components/home/home-visual.test.tsx +27 -0
  44. package/src/theme/components/home/home-visual.tsx +20 -13
  45. package/src/theme/components/home/layout.tsx +125 -1
  46. package/src/theme/components/home/split.css +2 -2
  47. package/src/theme/components/home/tabs.css +1 -1
  48. package/src/theme/components/home/trust-strip.css +12 -0
  49. package/src/theme/components/home/trust-strip.test.tsx +33 -0
  50. package/src/theme/components/home/trust-strip.tsx +76 -15
  51. package/src/theme/components/nav/layout.tsx +10 -14
  52. package/src/theme/components/nav/nav-logo.tsx +2 -3
  53. package/src/theme/components/seo-head-data.test.ts +196 -0
  54. package/src/theme/components/seo-head-data.ts +490 -0
  55. package/src/theme/components/seo-head.tsx +102 -0
  56. package/src/theme/components/shared/IssueLinkIcon.tsx +42 -0
  57. package/src/theme/components/shared/issue-link.css +74 -0
  58. package/src/theme/components/sidebar/sidebar-filter.test.ts +49 -9
  59. package/src/theme/components/sidebar/sidebar-filter.ts +24 -4
  60. package/src/theme/index.tsx +9 -1
  61. package/src/theme/lib/rich-text-parse.ts +0 -2
  62. package/src/theme/lib/seo-url.ts +28 -0
  63. package/src/theme/lib/theme-favicon.ts +0 -3
  64. package/src/theme/styles/themes/honeycrisp.css +20 -20
@@ -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
+ }