@ciderpress/ui 1.0.0-rc.1 → 1.0.0-rc.4
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.
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/node.d.ts +1 -1
- package/dist/node.mjs +316 -50
- package/dist/theme/components/footer/site-footer.tsx +68 -9
- package/dist/theme/components/home/feature-card.tsx +5 -8
- package/dist/theme/components/home/feature.tsx +31 -11
- package/dist/theme/components/home/hero-demo-custom.tsx +116 -0
- package/dist/theme/components/home/hero-demo.css +27 -0
- package/dist/theme/components/home/layout.tsx +149 -26
- package/dist/theme/components/home/split-visual-custom.tsx +26 -0
- package/dist/theme/components/home/workspaces.tsx +39 -3
- package/dist/theme/components/nav/branch-tag.tsx +7 -7
- package/dist/theme/components/nav/ciderpress-header.css +2 -1
- package/dist/theme/components/nav/ciderpress-header.tsx +26 -3
- package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
- package/dist/theme/components/nav/floating-branch-indicator.tsx +7 -7
- package/dist/theme/components/nav/header-icon.css +24 -0
- package/dist/theme/components/nav/header-icon.tsx +119 -0
- package/dist/theme/components/nav/header-logo.css +16 -0
- package/dist/theme/components/nav/header-logo.tsx +200 -0
- package/dist/theme/components/nav/layout.tsx +45 -7
- package/dist/theme/components/nav/nav-logo.tsx +16 -19
- package/dist/theme/components/shared/card-icon.tsx +37 -0
- package/dist/theme/components/shared/ciderpress-logo.tsx +5 -1
- package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
- package/dist/theme/components/shared/section-card.tsx +9 -6
- package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/dist/theme/components/theme-provider.tsx +27 -11
- package/dist/theme/components/workspaces/card.tsx +7 -9
- package/dist/theme/hooks/use-ciderpress.ts +60 -5
- package/dist/theme/index.tsx +0 -1
- package/dist/theme/lib/read-social-links.ts +4 -3
- package/dist/theme/lib/theme-favicon.ts +1 -1
- package/dist/theme/lib/with-mount-base.ts +31 -0
- package/package.json +14 -14
- package/src/theme/components/footer/site-footer.tsx +68 -9
- package/src/theme/components/home/feature-card.tsx +5 -8
- package/src/theme/components/home/feature.tsx +31 -11
- package/src/theme/components/home/hero-demo-custom.tsx +116 -0
- package/src/theme/components/home/hero-demo.css +27 -0
- package/src/theme/components/home/layout.tsx +149 -26
- package/src/theme/components/home/split-visual-custom.tsx +26 -0
- package/src/theme/components/home/workspaces.tsx +39 -3
- package/src/theme/components/nav/branch-tag.tsx +7 -7
- package/src/theme/components/nav/ciderpress-header.css +2 -1
- package/src/theme/components/nav/ciderpress-header.tsx +26 -3
- package/src/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
- package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
- package/src/theme/components/nav/header-icon.css +24 -0
- package/src/theme/components/nav/header-icon.tsx +119 -0
- package/src/theme/components/nav/header-logo.css +16 -0
- package/src/theme/components/nav/header-logo.tsx +200 -0
- package/src/theme/components/nav/layout.tsx +45 -7
- package/src/theme/components/nav/nav-logo.tsx +16 -19
- package/src/theme/components/shared/card-icon.tsx +37 -0
- package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
- package/src/theme/components/shared/resolve-card-icon.ts +53 -12
- package/src/theme/components/shared/section-card.tsx +9 -6
- package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/src/theme/components/theme-provider.tsx +27 -11
- package/src/theme/components/workspaces/card.tsx +7 -9
- package/src/theme/hooks/use-ciderpress.ts +60 -5
- package/src/theme/index.tsx +0 -1
- package/src/theme/lib/read-social-links.ts +4 -3
- package/src/theme/lib/theme-favicon.ts +1 -1
- package/src/theme/lib/with-mount-base.ts +31 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* oxlint-disable no-ternary -- raw-copied file; relaxed rules per packages/ui/CLAUDE.md */
|
|
2
|
+
import type { CiderpressConfig, IconConfig, IconImage } 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 { match } from 'massaman/match'
|
|
6
|
+
import type React from 'react'
|
|
7
|
+
|
|
8
|
+
import { Icon } from '../shared/icon'
|
|
9
|
+
|
|
10
|
+
import './header-icon.css'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Small brand chip rendered before `<HeaderLogo />` inside `cp-header-logo`.
|
|
14
|
+
*
|
|
15
|
+
* Reads `userConfig.brand.icon` from the bundled user config and routes:
|
|
16
|
+
* - Iconify id (`"devicon:react"`) → `<Icon>` component.
|
|
17
|
+
* - `{ id, color }` → `<Icon>` with inline color.
|
|
18
|
+
* - `{ src, alt }` → `<img>` with the user's asset.
|
|
19
|
+
* - missing → null (slot collapses, `<HeaderLogo />` stretches).
|
|
20
|
+
*
|
|
21
|
+
* Pairs with `<HeaderLogo />` for the canonical two-slot brand identity
|
|
22
|
+
* (icon chip + wordmark logo). Sites with only one of the two get the
|
|
23
|
+
* lone element rendered.
|
|
24
|
+
*
|
|
25
|
+
* @returns Branded icon chip ready to drop inside `cp-header-logo`, or null
|
|
26
|
+
*/
|
|
27
|
+
export function HeaderIcon(): React.ReactElement | null {
|
|
28
|
+
const icon = readIconConfig(userConfigModule)
|
|
29
|
+
if (icon === undefined) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
return match(resolveTopbar(icon))
|
|
33
|
+
.with({ kind: 'iconify' }, (r) => (
|
|
34
|
+
<span className="cp-header-icon">
|
|
35
|
+
<Icon icon={r.id} style={inlineColor(r.color)} />
|
|
36
|
+
</span>
|
|
37
|
+
))
|
|
38
|
+
.with({ kind: 'image' }, (r) => (
|
|
39
|
+
<span className="cp-header-icon">
|
|
40
|
+
<img src={r.src} alt={r.alt} className="cp-header-icon__img" />
|
|
41
|
+
</span>
|
|
42
|
+
))
|
|
43
|
+
.exhaustive()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { HeaderIcon as default }
|
|
47
|
+
|
|
48
|
+
type TopbarIcon =
|
|
49
|
+
| { readonly kind: 'iconify'; readonly id: string; readonly color: string | undefined }
|
|
50
|
+
| { readonly kind: 'image'; readonly src: string; readonly alt: string }
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract the `brand.icon` field from the bundled user config module.
|
|
54
|
+
*
|
|
55
|
+
* @private
|
|
56
|
+
* @param mod - Module imported from `@ciderpress/internal/user-config`
|
|
57
|
+
* @returns The `brand.icon` value or `undefined` when none is configured
|
|
58
|
+
*/
|
|
59
|
+
function readIconConfig(mod: unknown): IconConfig | undefined {
|
|
60
|
+
if (mod === null || mod === undefined) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
const asRecord = mod as Record<string, unknown>
|
|
64
|
+
const candidate =
|
|
65
|
+
asRecord.default !== null && asRecord.default !== undefined
|
|
66
|
+
? (asRecord.default as Partial<CiderpressConfig>)
|
|
67
|
+
: (mod as Partial<CiderpressConfig>)
|
|
68
|
+
const brand = candidate.brand
|
|
69
|
+
if (brand === undefined) {
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
return brand.icon
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Discriminate `IconConfig` into a render-ready shape. Iconify strings get
|
|
77
|
+
* `color: undefined` (driven by ambient CSS); image objects supply alt
|
|
78
|
+
* defaulted to empty string per the underlying type.
|
|
79
|
+
*
|
|
80
|
+
* @private
|
|
81
|
+
* @param icon - Validated `IconConfig` value
|
|
82
|
+
* @returns Discriminated render shape
|
|
83
|
+
*/
|
|
84
|
+
function resolveTopbar(icon: IconConfig): TopbarIcon {
|
|
85
|
+
if (typeof icon === 'string') {
|
|
86
|
+
return { kind: 'iconify', id: icon, color: undefined }
|
|
87
|
+
}
|
|
88
|
+
if (isImageIcon(icon)) {
|
|
89
|
+
return { kind: 'image', src: icon.src, alt: icon.alt ?? '' }
|
|
90
|
+
}
|
|
91
|
+
return { kind: 'iconify', id: icon.id, color: icon.color }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build an inline-style object that sets the chip's `color` when the user
|
|
96
|
+
* supplied an explicit palette colour. Iconify renders fills via
|
|
97
|
+
* `currentColor` for monochrome icons, so this is enough to retint them.
|
|
98
|
+
*
|
|
99
|
+
* @private
|
|
100
|
+
* @param color - Optional palette colour token
|
|
101
|
+
* @returns Inline style object or undefined
|
|
102
|
+
*/
|
|
103
|
+
function inlineColor(color: string | undefined): React.CSSProperties | undefined {
|
|
104
|
+
if (color === undefined) {
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
return { color }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Type guard for the image-form `IconConfig` object.
|
|
112
|
+
*
|
|
113
|
+
* @private
|
|
114
|
+
* @param icon - Non-string icon config value
|
|
115
|
+
* @returns True when the object carries `src` (image form)
|
|
116
|
+
*/
|
|
117
|
+
function isImageIcon(icon: Exclude<IconConfig, string>): icon is IconImage {
|
|
118
|
+
return 'src' in icon && typeof icon.src === 'string'
|
|
119
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sizing for the brand mark rendered inside `cp-header-logo` by
|
|
3
|
+
* `<HeaderLogo />`.
|
|
4
|
+
*
|
|
5
|
+
* The parent `.cp-header-logo` already sets `--cp-header-logo-height`
|
|
6
|
+
* (default 28px) and `width: auto` for inline SVGs. Apply matching
|
|
7
|
+
* rules to image marks so a user `logo: '/logo.svg'` lines up exactly
|
|
8
|
+
* with the SVG wordmark height — no extra config needed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.cp-header-logo__img,
|
|
12
|
+
.cp-header-logo__mark {
|
|
13
|
+
height: var(--cp-header-logo-height, 28px);
|
|
14
|
+
width: auto;
|
|
15
|
+
display: block;
|
|
16
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/* oxlint-disable no-ternary -- raw-copied file; relaxed rules per packages/ui/CLAUDE.md */
|
|
2
|
+
import type { CiderpressConfig, LogoFn, LogoImage } 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
|
+
|
|
8
|
+
import { withMountBase } from '../../lib/with-mount-base.ts'
|
|
9
|
+
|
|
10
|
+
import './header-logo.css'
|
|
11
|
+
|
|
12
|
+
interface ThemeContext {
|
|
13
|
+
readonly name: string
|
|
14
|
+
readonly variant: 'light' | 'dark'
|
|
15
|
+
readonly isDark: boolean
|
|
16
|
+
readonly colors: {
|
|
17
|
+
readonly brand: string
|
|
18
|
+
readonly brandHover: string
|
|
19
|
+
readonly brandSoft: string
|
|
20
|
+
readonly bg: string
|
|
21
|
+
readonly text: string
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const COLOR_VARS = Object.freeze({
|
|
26
|
+
brand: '--rp-c-brand',
|
|
27
|
+
brandHover: '--rp-c-brand-dark',
|
|
28
|
+
brandSoft: '--rp-c-brand-tint',
|
|
29
|
+
bg: '--rp-c-bg',
|
|
30
|
+
text: '--rp-c-text-1',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const DEFAULT_BRAND = BRAND_COLORS[DEFAULT_THEME_NAME]
|
|
34
|
+
const FALLBACK_COLORS = Object.freeze({
|
|
35
|
+
brand: DEFAULT_BRAND.primary,
|
|
36
|
+
brandHover: DEFAULT_BRAND.hover,
|
|
37
|
+
brandSoft: DEFAULT_BRAND.soft,
|
|
38
|
+
bg: '#ffffff',
|
|
39
|
+
text: '#1f2937',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Brand mark rendered inside `<RouteLink className="cp-header-logo">`
|
|
44
|
+
* by `<CiderpressHeader />`.
|
|
45
|
+
*
|
|
46
|
+
* Reads `userConfig.brand.logo` from the bundled user config (same
|
|
47
|
+
* `@ciderpress/internal/user-config` alias `NavLogo` uses) and renders:
|
|
48
|
+
*
|
|
49
|
+
* - `string` → `<img src={...}>` pointed at the user's asset path.
|
|
50
|
+
* - `LogoFn` → call with the live theme context. `LogoImage` returns
|
|
51
|
+
* render as `<img>`; React node returns render as-is. Re-renders
|
|
52
|
+
* when `data-cp-theme` / `data-cp-variant` flips so theme-aware
|
|
53
|
+
* logos retint without a reload.
|
|
54
|
+
* - missing → the framework's themed `<CiderpressLogo />` wordmark.
|
|
55
|
+
*
|
|
56
|
+
* Replaces the prior approach where the header was hardcoded to
|
|
57
|
+
* `<CiderpressLogo />` and the user's logo was forwarded only to
|
|
58
|
+
* Rspress's nav slot — which renders at 1×1 on landing pages.
|
|
59
|
+
*
|
|
60
|
+
* @returns Branded logo element ready to drop inside `cp-header-logo`
|
|
61
|
+
*/
|
|
62
|
+
export function HeaderLogo(): React.ReactElement | null {
|
|
63
|
+
const logoConfig = readLogoConfig(userConfigModule)
|
|
64
|
+
const [themeContext, setThemeContext] = useState<ThemeContext | null>(null)
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (typeof logoConfig !== 'function') {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const html = globalThis.document.documentElement
|
|
71
|
+
setThemeContext(readThemeContext(html))
|
|
72
|
+
|
|
73
|
+
const observer = new MutationObserver(() => {
|
|
74
|
+
setThemeContext(readThemeContext(html))
|
|
75
|
+
})
|
|
76
|
+
observer.observe(html, {
|
|
77
|
+
attributes: true,
|
|
78
|
+
attributeFilter: ['data-cp-theme', 'data-cp-variant', 'class', 'style'],
|
|
79
|
+
})
|
|
80
|
+
return () => observer.disconnect()
|
|
81
|
+
}, [logoConfig])
|
|
82
|
+
|
|
83
|
+
if (logoConfig === undefined) {
|
|
84
|
+
// Default: the auto-generated `/logo.svg` written to the public dir
|
|
85
|
+
// by the banner module at sync time (derived from `config.title`).
|
|
86
|
+
// Sites that committed their own `public/logo.svg` already win here.
|
|
87
|
+
// The `<CiderpressLogo />` framework wordmark is opt-in via
|
|
88
|
+
// `logo: ({ theme }) => <CiderpressLogo />`.
|
|
89
|
+
return <img src={withMountBase('/logo.svg')} alt="" className="cp-header-logo__img" />
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof logoConfig === 'string') {
|
|
93
|
+
return <img src={withMountBase(logoConfig)} alt="" className="cp-header-logo__img" />
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (themeContext === null) {
|
|
97
|
+
// Function-form logo: render nothing for a frame instead of flashing
|
|
98
|
+
// the ciderpress wordmark while themeContext resolves. The user's
|
|
99
|
+
// logo appears as soon as the layout effect runs.
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = logoConfig({ theme: themeContext })
|
|
104
|
+
if (isLogoImage(result)) {
|
|
105
|
+
return (
|
|
106
|
+
<img
|
|
107
|
+
src={withMountBase(result.src)}
|
|
108
|
+
alt={result.alt ?? ''}
|
|
109
|
+
width={result.width}
|
|
110
|
+
height={result.height}
|
|
111
|
+
className="cp-header-logo__img"
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return <>{result as React.ReactNode}</>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { HeaderLogo as default }
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract the `logo` field from the bundled user config module.
|
|
122
|
+
*
|
|
123
|
+
* @private
|
|
124
|
+
* @param mod - Module imported from `@ciderpress/internal/user-config`
|
|
125
|
+
* @returns The `logo` value or `undefined` when none is configured
|
|
126
|
+
*/
|
|
127
|
+
function readLogoConfig(mod: unknown): string | LogoFn | undefined {
|
|
128
|
+
if (mod === null || mod === undefined) {
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
const asRecord = mod as Record<string, unknown>
|
|
132
|
+
const candidate =
|
|
133
|
+
asRecord.default !== null && asRecord.default !== undefined
|
|
134
|
+
? (asRecord.default as Partial<CiderpressConfig>)
|
|
135
|
+
: (mod as Partial<CiderpressConfig>)
|
|
136
|
+
const brand = candidate.brand
|
|
137
|
+
if (brand === undefined) {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
const { logo } = brand
|
|
141
|
+
if (typeof logo === 'string') {
|
|
142
|
+
return logo
|
|
143
|
+
}
|
|
144
|
+
if (typeof logo === 'function') {
|
|
145
|
+
return logo
|
|
146
|
+
}
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Type guard distinguishing a `LogoImage` object from a React element.
|
|
152
|
+
*
|
|
153
|
+
* @private
|
|
154
|
+
* @param value - Return value from a `LogoFn`
|
|
155
|
+
* @returns True when `value` should be spread onto an `<img>` element
|
|
156
|
+
*/
|
|
157
|
+
function isLogoImage(value: unknown): value is LogoImage {
|
|
158
|
+
if (value === null || typeof value !== 'object') {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
const obj = value as Record<string, unknown>
|
|
162
|
+
if ('$$typeof' in obj) {
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
return typeof obj.src === 'string'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build the live `ThemeContext` passed to a function-form `logo`.
|
|
170
|
+
*
|
|
171
|
+
* @private
|
|
172
|
+
* @param html - Document root element
|
|
173
|
+
* @returns Live theme context snapshot
|
|
174
|
+
*/
|
|
175
|
+
function readThemeContext(html: HTMLElement): ThemeContext {
|
|
176
|
+
const variant: 'light' | 'dark' = html.dataset.cpVariant === 'light' ? 'light' : 'dark'
|
|
177
|
+
const name = typeof html.dataset.cpTheme === 'string' ? html.dataset.cpTheme : DEFAULT_THEME_NAME
|
|
178
|
+
|
|
179
|
+
const styles = globalThis.window.getComputedStyle(html)
|
|
180
|
+
function read(cssVar: string, fallback: string): string {
|
|
181
|
+
const raw = styles.getPropertyValue(cssVar).trim()
|
|
182
|
+
if (raw.length === 0) {
|
|
183
|
+
return fallback
|
|
184
|
+
}
|
|
185
|
+
return raw
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
name,
|
|
190
|
+
variant,
|
|
191
|
+
isDark: variant === 'dark',
|
|
192
|
+
colors: {
|
|
193
|
+
brand: read(COLOR_VARS.brand, FALLBACK_COLORS.brand),
|
|
194
|
+
brandHover: read(COLOR_VARS.brandHover, FALLBACK_COLORS.brandHover),
|
|
195
|
+
brandSoft: read(COLOR_VARS.brandSoft, FALLBACK_COLORS.brandSoft),
|
|
196
|
+
bg: read(COLOR_VARS.bg, FALLBACK_COLORS.bg),
|
|
197
|
+
text: read(COLOR_VARS.text, FALLBACK_COLORS.text),
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { SiteEditConfig, SiteReportConfig } from '@ciderpress/config'
|
|
2
1
|
import { useFrontmatter, useSite } from '@rspress/core/runtime'
|
|
3
2
|
import { Layout as OriginalLayout } from '@rspress/core/theme-original'
|
|
4
3
|
import { match, P } from 'massaman/match'
|
|
5
4
|
import type React from 'react'
|
|
6
5
|
import { useEffect } from 'react'
|
|
7
6
|
|
|
7
|
+
import type { CiderpressSiteBlock } from '../../hooks/use-ciderpress'
|
|
8
8
|
import { useCiderpress } from '../../hooks/use-ciderpress'
|
|
9
9
|
import { useNavItems } from '../../hooks/use-nav-items'
|
|
10
10
|
import { readSocialLinks } from '../../lib/read-social-links'
|
|
@@ -120,6 +120,30 @@ export function Layout(): React.ReactElement {
|
|
|
120
120
|
</ContentFooterPortal>
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
+
// Rspress's SSG-MD pipeline renders the React tree through
|
|
124
|
+
// `react-render-to-markdown` to produce the `.md` files served to
|
|
125
|
+
// `<LlmsCopyButton />`. If we render our chrome (header, docs bar,
|
|
126
|
+
// footer slot wrappers) during that pass, the topbar logo, search,
|
|
127
|
+
// and nav items all get serialized as markdown and end up at the
|
|
128
|
+
// top of the copied content. Short-circuit to just the article body
|
|
129
|
+
// via `<OriginalLayout />` (Rspress's stock layout already returns a
|
|
130
|
+
// Fragment-only output for SSG-MD). The visible HTML SSG path
|
|
131
|
+
// (`SSG_MD` undefined) still gets the full chrome.
|
|
132
|
+
if (import.meta.env.SSG_MD) {
|
|
133
|
+
return (
|
|
134
|
+
<OriginalLayout
|
|
135
|
+
top={null}
|
|
136
|
+
beforeNavMenu={null}
|
|
137
|
+
afterNavMenu={null}
|
|
138
|
+
beforeSidebar={null}
|
|
139
|
+
afterSidebar={null}
|
|
140
|
+
beforeDoc={null}
|
|
141
|
+
afterDoc={null}
|
|
142
|
+
bottom={null}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
123
147
|
return (
|
|
124
148
|
<>
|
|
125
149
|
<CiderpressHeader
|
|
@@ -169,16 +193,17 @@ function readNavItems(site: unknown): readonly CiderpressNavMenuItem[] {
|
|
|
169
193
|
|
|
170
194
|
/**
|
|
171
195
|
* Build the list of `MetaAction`s to render under each doc page,
|
|
172
|
-
* derived from `site.edit`
|
|
173
|
-
*
|
|
196
|
+
* derived from the serialised `site.edit` / `site.report` blocks
|
|
197
|
+
* (originally configured via top-level `editLink` / `reportLink`).
|
|
198
|
+
* Returns an empty array when neither is configured.
|
|
174
199
|
*
|
|
175
200
|
* @private
|
|
176
201
|
* @param params - Site edit/report config plus current page path
|
|
177
202
|
* @returns Ordered list of meta actions
|
|
178
203
|
*/
|
|
179
204
|
function collectMetaActions(params: {
|
|
180
|
-
readonly edit:
|
|
181
|
-
readonly report:
|
|
205
|
+
readonly edit: CiderpressSiteBlock['edit']
|
|
206
|
+
readonly report: CiderpressSiteBlock['report']
|
|
182
207
|
readonly pagePath: string
|
|
183
208
|
}): readonly MetaAction[] {
|
|
184
209
|
const { edit, report, pagePath } = params
|
|
@@ -204,9 +229,17 @@ function collectMetaActions(params: {
|
|
|
204
229
|
}
|
|
205
230
|
|
|
206
231
|
/**
|
|
232
|
+
* Construct the per-page "edit this page" URL from a serialised
|
|
233
|
+
* `editLink` block. The `buildSiteBlock` helper guarantees `repo` is
|
|
234
|
+
* present whenever the block itself is — callers should already have
|
|
235
|
+
* skipped the action when the block was `undefined`.
|
|
236
|
+
*
|
|
207
237
|
* @private
|
|
238
|
+
* @param edit - Site edit block (non-undefined when called)
|
|
239
|
+
* @param pagePath - Current page's source path (with leading slash)
|
|
240
|
+
* @returns Fully-qualified URL string
|
|
208
241
|
*/
|
|
209
|
-
function buildEditUrl(edit:
|
|
242
|
+
function buildEditUrl(edit: NonNullable<CiderpressSiteBlock['edit']>, pagePath: string): string {
|
|
210
243
|
if (edit.repo.startsWith('http')) {
|
|
211
244
|
return edit.repo
|
|
212
245
|
}
|
|
@@ -219,9 +252,14 @@ function buildEditUrl(edit: SiteEditConfig, pagePath: string): string {
|
|
|
219
252
|
}
|
|
220
253
|
|
|
221
254
|
/**
|
|
255
|
+
* Construct the "report an issue" URL from a serialised `reportLink`
|
|
256
|
+
* block. Same `repo`-presence contract as {@link buildEditUrl}.
|
|
257
|
+
*
|
|
222
258
|
* @private
|
|
259
|
+
* @param report - Site report block (non-undefined when called)
|
|
260
|
+
* @returns Fully-qualified URL string
|
|
223
261
|
*/
|
|
224
|
-
function buildReportUrl(report:
|
|
262
|
+
function buildReportUrl(report: NonNullable<CiderpressSiteBlock['report']>): string {
|
|
225
263
|
if (report.repo.startsWith('http')) {
|
|
226
264
|
return report.repo
|
|
227
265
|
}
|
|
@@ -6,8 +6,6 @@ import { BRAND_COLORS, DEFAULT_THEME_NAME } from '@ciderpress/theme'
|
|
|
6
6
|
import React, { useEffect, useState } from 'react'
|
|
7
7
|
import { createPortal } from 'react-dom'
|
|
8
8
|
|
|
9
|
-
import { CiderpressLogo } from '../shared/ciderpress-logo'
|
|
10
|
-
|
|
11
9
|
import './nav-logo.css'
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -37,7 +35,7 @@ const FALLBACK_COLORS = Object.freeze({
|
|
|
37
35
|
})
|
|
38
36
|
|
|
39
37
|
/**
|
|
40
|
-
* `NavLogo` — Rspress-aware logo slot
|
|
38
|
+
* `NavLogo` — Rspress-aware logo slot for function-form `logo` configs.
|
|
41
39
|
*
|
|
42
40
|
* - Reads `userConfig.logo` from the bundled user config via the
|
|
43
41
|
* `@ciderpress/internal/user-config` alias.
|
|
@@ -52,7 +50,10 @@ const FALLBACK_COLORS = Object.freeze({
|
|
|
52
50
|
* - `logo` is a function → call with the live `LogoContext`. If the return
|
|
53
51
|
* value is a `LogoImage`-shaped object, spread onto `<img>`. Otherwise
|
|
54
52
|
* render as a React node.
|
|
55
|
-
* - `logo` is missing → render
|
|
53
|
+
* - `logo` is missing → render `null`; Rspress paints the auto-generated
|
|
54
|
+
* `/logo.svg` (written to the public dir by the banner module, derived
|
|
55
|
+
* from the project title). Users who want the framework's themed wordmark
|
|
56
|
+
* opt in via `logo: ({ theme }) => <CiderpressLogo />`.
|
|
56
57
|
*
|
|
57
58
|
* @returns Portaled logo element or null
|
|
58
59
|
*/
|
|
@@ -116,13 +117,11 @@ export function NavLogo(): React.ReactElement | null {
|
|
|
116
117
|
|
|
117
118
|
const logoConfig = readLogoConfig(userConfigModule)
|
|
118
119
|
|
|
119
|
-
// String-form user logos
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
// shows. Theme color flips via `currentColor` reading from `--rp-c-brand`.
|
|
125
|
-
if (typeof logoConfig === 'string') {
|
|
120
|
+
// String-form user logos and the missing-logo default both ride Rspress's
|
|
121
|
+
// native <img>. resolveLogo() in @ciderpress/ui/config maps the missing
|
|
122
|
+
// case to the auto-generated /logo.svg path. Only function-form logos
|
|
123
|
+
// need this portal — they re-render when the theme/variant changes.
|
|
124
|
+
if (typeof logoConfig !== 'function') {
|
|
126
125
|
return null
|
|
127
126
|
}
|
|
128
127
|
|
|
@@ -130,13 +129,7 @@ export function NavLogo(): React.ReactElement | null {
|
|
|
130
129
|
return null
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
const rendered =
|
|
135
|
-
typeof logoConfig === 'function' ? (
|
|
136
|
-
renderLogoFn({ fn: logoConfig, theme: themeContext })
|
|
137
|
-
) : (
|
|
138
|
-
<CiderpressLogo />
|
|
139
|
-
)
|
|
132
|
+
const rendered = renderLogoFn({ fn: logoConfig, theme: themeContext })
|
|
140
133
|
|
|
141
134
|
return createPortal(<span className="cp-nav-logo">{rendered}</span>, target)
|
|
142
135
|
}
|
|
@@ -160,7 +153,11 @@ function readLogoConfig(mod: unknown): string | LogoFn | undefined {
|
|
|
160
153
|
asRecord.default !== null && asRecord.default !== undefined
|
|
161
154
|
? (asRecord.default as Partial<CiderpressConfig>)
|
|
162
155
|
: (mod as Partial<CiderpressConfig>)
|
|
163
|
-
const
|
|
156
|
+
const brand = candidate.brand
|
|
157
|
+
if (brand === undefined) {
|
|
158
|
+
return undefined
|
|
159
|
+
}
|
|
160
|
+
const { logo } = brand
|
|
164
161
|
if (typeof logo === 'string') {
|
|
165
162
|
return logo
|
|
166
163
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { match } from 'massaman/match'
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
|
|
4
|
+
import { Icon } from './icon'
|
|
5
|
+
import type { ResolvedCardIcon } from './resolve-card-icon'
|
|
6
|
+
|
|
7
|
+
interface CardIconProps {
|
|
8
|
+
readonly resolved: ResolvedCardIcon
|
|
9
|
+
readonly className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Card-surface icon renderer. Branches on the discriminated
|
|
14
|
+
* `ResolvedCardIcon`:
|
|
15
|
+
*
|
|
16
|
+
* - `kind: 'iconify'` → `<Icon icon={id} />` wrapped in a colour swatch.
|
|
17
|
+
* - `kind: 'image'` → `<img src alt>` painted at the same chip size
|
|
18
|
+
* without colour tinting (the image carries its own art direction).
|
|
19
|
+
*
|
|
20
|
+
* @param props - Resolved icon + optional className applied to the swatch wrapper
|
|
21
|
+
* @returns Branded icon element
|
|
22
|
+
*/
|
|
23
|
+
export function CardIcon(props: CardIconProps): React.ReactElement {
|
|
24
|
+
const wrapperClass = props.className ?? 'cp-card__icon'
|
|
25
|
+
return match(props.resolved)
|
|
26
|
+
.with({ kind: 'iconify' }, (r) => (
|
|
27
|
+
<span className={`${wrapperClass} ${wrapperClass}--${r.color}`}>
|
|
28
|
+
<Icon icon={r.id} />
|
|
29
|
+
</span>
|
|
30
|
+
))
|
|
31
|
+
.with({ kind: 'image' }, (r) => (
|
|
32
|
+
<span className={`${wrapperClass} ${wrapperClass}--image`}>
|
|
33
|
+
<img src={r.src} alt={r.alt} className={`${wrapperClass}-img`} />
|
|
34
|
+
</span>
|
|
35
|
+
))
|
|
36
|
+
.exhaustive()
|
|
37
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { BRAND_COLORS, DEFAULT_THEME_NAME } from '@ciderpress/theme'
|
|
2
|
-
import type
|
|
2
|
+
// Value import (not `import type`) — Rslib emits the classic JSX runtime
|
|
3
|
+
// (`React.createElement(...)`), so the `React` binding must survive
|
|
4
|
+
// compilation. With a type-only import, the bundle throws `React is not
|
|
5
|
+
// defined` at runtime.
|
|
6
|
+
import React from 'react'
|
|
3
7
|
|
|
4
8
|
const FONT_STACK = "'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace"
|
|
5
9
|
/**
|
|
@@ -1,28 +1,69 @@
|
|
|
1
|
+
import type { SerializedIcon } from '@ciderpress/config'
|
|
2
|
+
import { match } from 'massaman/match'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render-ready image icon — `src` plus `alt`.
|
|
6
|
+
*/
|
|
7
|
+
export interface ResolvedCardImageIcon {
|
|
8
|
+
readonly kind: 'image'
|
|
9
|
+
readonly src: string
|
|
10
|
+
readonly alt: string
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
/**
|
|
2
|
-
*
|
|
14
|
+
* Render-ready Iconify icon — `id` plus a colour-rotation key.
|
|
3
15
|
*/
|
|
4
|
-
export interface
|
|
16
|
+
export interface ResolvedCardIconifyIcon {
|
|
17
|
+
readonly kind: 'iconify'
|
|
5
18
|
readonly id: string
|
|
6
19
|
readonly color: string
|
|
7
20
|
}
|
|
8
21
|
|
|
9
22
|
/**
|
|
10
|
-
*
|
|
23
|
+
* Discriminated union of resolved card-icon variants. Render sites
|
|
24
|
+
* switch on `kind` to decide between an Iconify `<Icon>` and an `<img>`.
|
|
25
|
+
*/
|
|
26
|
+
export type ResolvedCardIcon = ResolvedCardIconifyIcon | ResolvedCardImageIcon
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Card-icon input shape — the `SerializedIcon` shape emitted by the
|
|
30
|
+
* sync engine (workspaces, sections, features), re-exported here under
|
|
31
|
+
* a local alias to keep render-site call signatures readable. Always
|
|
32
|
+
* comes from `@ciderpress/config` — there is no separate source of
|
|
33
|
+
* truth.
|
|
34
|
+
*/
|
|
35
|
+
export type CardIconInput = SerializedIcon | undefined
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a serialized icon value into a discriminated icon ready for
|
|
39
|
+
* render.
|
|
11
40
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
41
|
+
* - `string` → `{ kind: 'iconify', id, color: 'purple' }`
|
|
42
|
+
* - `{ id, color }` → `{ kind: 'iconify', id, color }`
|
|
43
|
+
* - `{ kind: 'image', src, alt }` → pass-through
|
|
44
|
+
* - `undefined` → `undefined`
|
|
14
45
|
*
|
|
15
|
-
* @param icon -
|
|
16
|
-
* @returns
|
|
46
|
+
* @param icon - Serialized icon value or `undefined`
|
|
47
|
+
* @returns Discriminated resolved icon, or `undefined`
|
|
17
48
|
*/
|
|
18
|
-
export function resolveCardIcon(
|
|
19
|
-
icon: string | { readonly id: string; readonly color: string } | undefined
|
|
20
|
-
): ResolvedCardIcon | undefined {
|
|
49
|
+
export function resolveCardIcon(icon: CardIconInput): ResolvedCardIcon | undefined {
|
|
21
50
|
if (icon === undefined) {
|
|
22
51
|
return undefined
|
|
23
52
|
}
|
|
24
53
|
if (typeof icon === 'string') {
|
|
25
|
-
return { id: icon, color: 'purple' }
|
|
54
|
+
return { kind: 'iconify', id: icon, color: 'purple' }
|
|
26
55
|
}
|
|
27
|
-
|
|
56
|
+
// Match on the discriminant value rather than `'kind' in icon` so a
|
|
57
|
+
// future variant that also carries a `kind` field (or a malformed
|
|
58
|
+
// runtime input) can't silently slip into the image branch.
|
|
59
|
+
return match(icon)
|
|
60
|
+
.with({ kind: 'image' }, (img) => ({
|
|
61
|
+
kind: 'image' as const,
|
|
62
|
+
src: img.src,
|
|
63
|
+
// Defence-in-depth: even if a downstream caller hand-builds the
|
|
64
|
+
// object and forgets `alt`, fall back to an empty string so React
|
|
65
|
+
// never sees `alt={undefined}` (which silences screen readers).
|
|
66
|
+
alt: img.alt ?? '',
|
|
67
|
+
}))
|
|
68
|
+
.otherwise((i) => ({ kind: 'iconify' as const, id: i.id, color: i.color }))
|
|
28
69
|
}
|