@ciderpress/ui 1.0.0-rc.3 → 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 +5 -1
- 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/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 +13 -13
- 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 +5 -1
- 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/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
|
@@ -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
|
}
|
|
@@ -153,7 +153,11 @@ function readLogoConfig(mod: unknown): string | LogoFn | undefined {
|
|
|
153
153
|
asRecord.default !== null && asRecord.default !== undefined
|
|
154
154
|
? (asRecord.default as Partial<CiderpressConfig>)
|
|
155
155
|
: (mod as Partial<CiderpressConfig>)
|
|
156
|
-
const
|
|
156
|
+
const brand = candidate.brand
|
|
157
|
+
if (brand === undefined) {
|
|
158
|
+
return undefined
|
|
159
|
+
}
|
|
160
|
+
const { logo } = brand
|
|
157
161
|
if (typeof logo === 'string') {
|
|
158
162
|
return logo
|
|
159
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
|
}
|
|
@@ -2,14 +2,15 @@ import { match, P } from 'massaman/match'
|
|
|
2
2
|
import type React from 'react'
|
|
3
3
|
|
|
4
4
|
import { Card } from './card'
|
|
5
|
-
import {
|
|
5
|
+
import { CardIcon } from './card-icon'
|
|
6
|
+
import type { CardIconInput } from './resolve-card-icon'
|
|
6
7
|
import { resolveCardIcon } from './resolve-card-icon'
|
|
7
8
|
|
|
8
9
|
export interface SectionCardProps {
|
|
9
10
|
readonly href: string
|
|
10
11
|
readonly title: string
|
|
11
12
|
readonly description?: string
|
|
12
|
-
readonly icon?:
|
|
13
|
+
readonly icon?: CardIconInput
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -25,7 +26,11 @@ export function SectionCard({
|
|
|
25
26
|
description,
|
|
26
27
|
icon = 'pixelarticons:file',
|
|
27
28
|
}: SectionCardProps): React.ReactElement {
|
|
28
|
-
const resolved = resolveCardIcon(icon) ?? {
|
|
29
|
+
const resolved = resolveCardIcon(icon) ?? {
|
|
30
|
+
kind: 'iconify' as const,
|
|
31
|
+
id: 'pixelarticons:file',
|
|
32
|
+
color: 'purple',
|
|
33
|
+
}
|
|
29
34
|
const descEl = match(description)
|
|
30
35
|
.with(P.nonNullable, (d) => <span className="cp-section-card__desc">{d}</span>)
|
|
31
36
|
.otherwise(() => null)
|
|
@@ -33,9 +38,7 @@ export function SectionCard({
|
|
|
33
38
|
return (
|
|
34
39
|
<Card href={href} className="cp-section-card">
|
|
35
40
|
<div className="cp-section-card__header">
|
|
36
|
-
<
|
|
37
|
-
<Icon icon={resolved.id} />
|
|
38
|
-
</span>
|
|
41
|
+
<CardIcon resolved={resolved} className="cp-section-card__icon" />
|
|
39
42
|
<span className="cp-section-card__title">{title}</span>
|
|
40
43
|
</div>
|
|
41
44
|
{descEl}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SerializedIcon } from '@ciderpress/config'
|
|
1
2
|
import { Link } from '@rspress/core/runtime'
|
|
2
3
|
import { match, P } from 'massaman/match'
|
|
3
4
|
import type React from 'react'
|
|
@@ -10,7 +11,7 @@ import './sidebar-links.css'
|
|
|
10
11
|
interface SidebarLinkItem {
|
|
11
12
|
readonly text: string
|
|
12
13
|
readonly link: string
|
|
13
|
-
readonly icon?:
|
|
14
|
+
readonly icon?: SerializedIcon
|
|
14
15
|
readonly style?: 'brand' | 'alt' | 'ghost'
|
|
15
16
|
readonly shape?: 'square' | 'rounded' | 'circle'
|
|
16
17
|
}
|
|
@@ -51,7 +52,10 @@ export function SidebarLinks(props: SidebarLinksProps): React.ReactElement | nul
|
|
|
51
52
|
function renderIcon(icon: SidebarLinkItem['icon']): React.ReactElement | null {
|
|
52
53
|
return match(icon)
|
|
53
54
|
.with(P.string, (id) => <Icon icon={id} className="cp-sidebar-link-icon" />)
|
|
54
|
-
.with({
|
|
55
|
+
.with({ kind: 'image' }, (img) => (
|
|
56
|
+
<img src={img.src} alt={img.alt} className="cp-sidebar-link-icon cp-sidebar-link-icon--img" />
|
|
57
|
+
))
|
|
58
|
+
.with({ id: P.string }, (i) => (
|
|
55
59
|
<Icon icon={i.id} className="cp-sidebar-link-icon" style={{ color: i.color }} />
|
|
56
60
|
))
|
|
57
61
|
.otherwise(() => null)
|
|
@@ -11,6 +11,8 @@ declare const __CIDERPRESS_DEFAULT_VARIANT__: string
|
|
|
11
11
|
declare const __CIDERPRESS_THEME_COLORS__: string
|
|
12
12
|
declare const __CIDERPRESS_THEME_DARK_COLORS__: string
|
|
13
13
|
declare const __CIDERPRESS_THEME_REGISTRY__: string
|
|
14
|
+
declare const __CIDERPRESS_HAS_USER_FAVICON__: boolean
|
|
15
|
+
declare const __CIDERPRESS_LOADER_MIN_MS__: number
|
|
14
16
|
|
|
15
17
|
interface RegistryEntry {
|
|
16
18
|
readonly name: string
|
|
@@ -26,7 +28,7 @@ type ThemeColorKey = keyof ThemeColors
|
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Parsed theme registry — built-in themes plus any user themes from
|
|
29
|
-
* `
|
|
31
|
+
* `theme.themes`. Read from the build-time define so the client bundle
|
|
30
32
|
* does not pull `@ciderpress/theme`'s factory + Zod into the runtime path.
|
|
31
33
|
*/
|
|
32
34
|
const REGISTRY_ENTRIES: readonly RegistryEntry[] = parseRegistry(__CIDERPRESS_THEME_REGISTRY__)
|
|
@@ -123,9 +125,11 @@ const COLOR_VAR_MAP: Readonly<Record<ThemeColorKey, readonly string[]>> = Object
|
|
|
123
125
|
const ALL_CSS_VARS: readonly string[] = Object.values(COLOR_VAR_MAP).flat()
|
|
124
126
|
|
|
125
127
|
/**
|
|
126
|
-
* Minimum time (ms) the loading overlay stays visible before fading
|
|
128
|
+
* Minimum time (ms) the loading overlay stays visible before fading
|
|
129
|
+
* out. Resolved at build time from `brand.loader.minDisplayMs` —
|
|
130
|
+
* defaults to 150ms in `packages/ui/src/config.ts`.
|
|
127
131
|
*/
|
|
128
|
-
const LOADER_MIN_DISPLAY_MS =
|
|
132
|
+
const LOADER_MIN_DISPLAY_MS = __CIDERPRESS_LOADER_MIN_MS__
|
|
129
133
|
|
|
130
134
|
/**
|
|
131
135
|
* Duration (ms) of the CSS fade-out transition. Must match the
|
|
@@ -199,12 +203,14 @@ export function ThemeProvider(): React.ReactElement | null {
|
|
|
199
203
|
applyColorOverrides(html, darkColors)
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
// Sync the document favicon with the active theme's brand colour
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
206
|
+
// Sync the document favicon with the active theme's brand colour —
|
|
207
|
+
// only when the user has NOT set their own `favicon` in config. When
|
|
208
|
+
// they have, Rspress already points `<link rel="icon">` at the
|
|
209
|
+
// user's asset, and overwriting it with the themed apple data-URI
|
|
210
|
+
// would silently restore ciderpress branding.
|
|
211
|
+
if (!__CIDERPRESS_HAS_USER_FAVICON__) {
|
|
212
|
+
syncThemeFavicon(html)
|
|
213
|
+
}
|
|
208
214
|
|
|
209
215
|
// Observe `.rp-dark` class changes so Rspress's built-in dark toggle
|
|
210
216
|
// stays the single source of truth for variant flips. The new variant
|
|
@@ -253,8 +259,11 @@ export function ThemeProvider(): React.ReactElement | null {
|
|
|
253
259
|
}
|
|
254
260
|
// Re-sync the favicon — brand colour is constant across variants for
|
|
255
261
|
// built-in themes, but surface overrides on the dark variant can
|
|
256
|
-
// affect the chip background colour we bake into the SVG.
|
|
257
|
-
|
|
262
|
+
// affect the chip background colour we bake into the SVG. Skipped
|
|
263
|
+
// when the user supplied their own favicon (see initial sync above).
|
|
264
|
+
if (!__CIDERPRESS_HAS_USER_FAVICON__) {
|
|
265
|
+
syncThemeFavicon(html)
|
|
266
|
+
}
|
|
258
267
|
})
|
|
259
268
|
observer.observe(html, { attributes: true, attributeFilter: ['class'] })
|
|
260
269
|
|
|
@@ -628,6 +637,13 @@ function dismissLoader(html: HTMLElement): () => void {
|
|
|
628
637
|
clearDotsInterval()
|
|
629
638
|
}, LOADER_MIN_DISPLAY_MS + LOADER_FADE_MS)
|
|
630
639
|
|
|
640
|
+
// The forced-dismiss fallback (covering the case where React never
|
|
641
|
+
// hydrates) lives in the inline head script — see `buildHeadScriptBody`
|
|
642
|
+
// in `packages/ui/src/config.ts`. That timer always runs, so a
|
|
643
|
+
// duplicate React-side fallback here would only fire AFTER ThemeProvider
|
|
644
|
+
// has already mounted — at which point the normal fade path above has
|
|
645
|
+
// already won.
|
|
646
|
+
|
|
631
647
|
return () => {
|
|
632
648
|
clearTimeout(fadeTimer)
|
|
633
649
|
clearTimeout(removeTimer)
|
|
@@ -2,7 +2,8 @@ import { match, P } from 'massaman/match'
|
|
|
2
2
|
import type React from 'react'
|
|
3
3
|
|
|
4
4
|
import { Card } from '../shared/card'
|
|
5
|
-
import {
|
|
5
|
+
import { CardIcon } from '../shared/card-icon'
|
|
6
|
+
import type { CardIconInput } from '../shared/resolve-card-icon'
|
|
6
7
|
import { resolveCardIcon } from '../shared/resolve-card-icon'
|
|
7
8
|
import { TechTag } from '../shared/tech-tag'
|
|
8
9
|
|
|
@@ -16,9 +17,10 @@ export interface WorkspaceCardProps {
|
|
|
16
17
|
*/
|
|
17
18
|
readonly href: string
|
|
18
19
|
/**
|
|
19
|
-
* Icon config — Iconify identifier string
|
|
20
|
+
* Icon config — Iconify identifier string, `{ id, color }` object, or
|
|
21
|
+
* `{ kind: 'image', src, alt }` for a static image asset.
|
|
20
22
|
*/
|
|
21
|
-
readonly icon?:
|
|
23
|
+
readonly icon?: CardIconInput
|
|
22
24
|
/**
|
|
23
25
|
* Scope prefix shown above the name (e.g. "apps/").
|
|
24
26
|
*/
|
|
@@ -67,13 +69,9 @@ export function WorkspaceCard({
|
|
|
67
69
|
const resolved = resolveCardIcon(icon)
|
|
68
70
|
|
|
69
71
|
const iconEl = match(resolved)
|
|
70
|
-
.with(P.nonNullable, (r) => <
|
|
72
|
+
.with(P.nonNullable, (r) => <CardIcon resolved={r} className="cp-card__icon" />)
|
|
71
73
|
.otherwise(() => null)
|
|
72
74
|
|
|
73
|
-
const iconColor = match(resolved)
|
|
74
|
-
.with(P.nonNullable, (r) => r.color)
|
|
75
|
-
.otherwise(() => 'purple')
|
|
76
|
-
|
|
77
75
|
const scopeEl = match(scope)
|
|
78
76
|
.with(
|
|
79
77
|
P.when((s): s is string => s !== undefined && s.length > 0),
|
|
@@ -117,7 +115,7 @@ export function WorkspaceCard({
|
|
|
117
115
|
<Card href={href} className="cp-workspace-card">
|
|
118
116
|
<div className="cp-workspace-card__header">
|
|
119
117
|
<div className="cp-workspace-card__identity">
|
|
120
|
-
|
|
118
|
+
{iconEl}
|
|
121
119
|
<div className="cp-workspace-card__title">
|
|
122
120
|
{scopeEl}
|
|
123
121
|
<span
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type { FooterConfig, HomeConfig,
|
|
1
|
+
import type { FooterConfig, HomeConfig, SerializedIcon } from '@ciderpress/config'
|
|
2
2
|
import { useSite } from '@rspress/core/runtime'
|
|
3
3
|
|
|
4
4
|
export interface CiderpressSidebarItem {
|
|
5
5
|
readonly text?: string
|
|
6
6
|
readonly link?: string
|
|
7
|
-
readonly icon?:
|
|
7
|
+
readonly icon?: SerializedIcon
|
|
8
8
|
readonly items?: readonly CiderpressSidebarItem[]
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface CiderpressSidebarLink {
|
|
12
12
|
readonly text: string
|
|
13
13
|
readonly link: string
|
|
14
|
-
readonly icon?:
|
|
14
|
+
readonly icon?: SerializedIcon
|
|
15
15
|
readonly style?: 'brand' | 'alt' | 'ghost'
|
|
16
16
|
readonly shape?: 'square' | 'rounded' | 'circle'
|
|
17
17
|
}
|
|
@@ -19,7 +19,7 @@ export interface CiderpressSidebarLink {
|
|
|
19
19
|
export interface WorkspaceCardData {
|
|
20
20
|
readonly title: string
|
|
21
21
|
readonly href: string
|
|
22
|
-
readonly icon:
|
|
22
|
+
readonly icon: SerializedIcon | undefined
|
|
23
23
|
readonly scope: string | undefined
|
|
24
24
|
readonly description: string | undefined
|
|
25
25
|
readonly tags: readonly string[]
|
|
@@ -33,6 +33,61 @@ export interface WorkspaceGroupData {
|
|
|
33
33
|
readonly cards: readonly WorkspaceCardData[]
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Serialised view of the unified `CiderpressConfig` that the runtime
|
|
38
|
+
* theme components still read through. The `site` field is rebuilt in
|
|
39
|
+
* `packages/ui/src/config.ts` (`buildSiteBlock`) from the new top-level
|
|
40
|
+
* `editLink` / `reportLink` / `topbar` / `sidebar` / `footer` / `version`
|
|
41
|
+
* config — kept under the legacy `site.*` name so theme components don't
|
|
42
|
+
* need to be rewired to look up each field individually.
|
|
43
|
+
*/
|
|
44
|
+
export interface CiderpressSiteBlock {
|
|
45
|
+
readonly version: string | undefined
|
|
46
|
+
readonly edit:
|
|
47
|
+
| {
|
|
48
|
+
readonly repo: string
|
|
49
|
+
readonly branch?: string
|
|
50
|
+
readonly directory?: string
|
|
51
|
+
readonly label?: string
|
|
52
|
+
}
|
|
53
|
+
| undefined
|
|
54
|
+
readonly report:
|
|
55
|
+
| {
|
|
56
|
+
readonly repo: string
|
|
57
|
+
readonly branch?: string
|
|
58
|
+
readonly directory?: string
|
|
59
|
+
readonly label?: string
|
|
60
|
+
}
|
|
61
|
+
| undefined
|
|
62
|
+
readonly topbarCta: { readonly text: string; readonly href: string } | undefined
|
|
63
|
+
readonly sidebarPromo:
|
|
64
|
+
| {
|
|
65
|
+
readonly title: string
|
|
66
|
+
readonly body: string
|
|
67
|
+
readonly cta: { readonly text: string; readonly href: string }
|
|
68
|
+
}
|
|
69
|
+
| undefined
|
|
70
|
+
readonly announcement:
|
|
71
|
+
| {
|
|
72
|
+
readonly id?: string
|
|
73
|
+
readonly lead?: string
|
|
74
|
+
readonly message: string
|
|
75
|
+
readonly cta?: { readonly href: string; readonly label: string }
|
|
76
|
+
readonly persistent?: boolean
|
|
77
|
+
}
|
|
78
|
+
| undefined
|
|
79
|
+
readonly footer:
|
|
80
|
+
| {
|
|
81
|
+
readonly columns?: readonly {
|
|
82
|
+
readonly heading: string
|
|
83
|
+
readonly links: readonly { readonly text: string; readonly href: string }[]
|
|
84
|
+
}[]
|
|
85
|
+
readonly tagline?: string
|
|
86
|
+
readonly brandMark?: string
|
|
87
|
+
}
|
|
88
|
+
| undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
36
91
|
interface CiderpressThemeConfig {
|
|
37
92
|
readonly sidebar: Record<string, readonly CiderpressSidebarItem[]>
|
|
38
93
|
readonly sidebarAbove: readonly CiderpressSidebarLink[] | undefined
|
|
@@ -41,7 +96,7 @@ interface CiderpressThemeConfig {
|
|
|
41
96
|
readonly standaloneScopePaths: readonly string[] | undefined
|
|
42
97
|
readonly home: HomeConfig | undefined
|
|
43
98
|
readonly ciderpressFooter: FooterConfig | undefined
|
|
44
|
-
readonly site:
|
|
99
|
+
readonly site: CiderpressSiteBlock | undefined
|
|
45
100
|
}
|
|
46
101
|
|
|
47
102
|
/**
|
|
@@ -2,8 +2,9 @@ import type { CiderpressSocialLink } from '../components/nav/ciderpress-nav-soci
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Pull social-link entries (GitHub, npm, etc) from Rspress's `site`
|
|
5
|
-
* shape. Lives on `site.themeConfig.socialLinks` after
|
|
6
|
-
*
|
|
5
|
+
* shape. Lives on `site.themeConfig.socialLinks` after the
|
|
6
|
+
* `packages/ui/src/config.ts` serialiser writes the unified `socials`
|
|
7
|
+
* config into the runtime theme config.
|
|
7
8
|
*
|
|
8
9
|
* @param site - Rspress site data (typically the result of `useSite().site`)
|
|
9
10
|
* @returns Array of social-link entries (empty array when not configured)
|
|
@@ -22,6 +23,6 @@ export function readSocialLinks(site: unknown): readonly CiderpressSocialLink[]
|
|
|
22
23
|
typeof item === 'object' &&
|
|
23
24
|
item !== null &&
|
|
24
25
|
typeof (item as { icon?: unknown }).icon === 'string' &&
|
|
25
|
-
typeof (item as {
|
|
26
|
+
typeof (item as { url?: unknown }).url === 'string'
|
|
26
27
|
)
|
|
27
28
|
}
|
|
@@ -107,7 +107,7 @@ function buildIconDataUri(brand: string, shadow: string, surface: string): strin
|
|
|
107
107
|
/**
|
|
108
108
|
* Locate the existing `<link rel="icon">` element, creating one if the
|
|
109
109
|
* document head doesn't have one yet (a defensive fallback — Rspress
|
|
110
|
-
* normally renders one from `
|
|
110
|
+
* normally renders one from `brand.favicon`).
|
|
111
111
|
*
|
|
112
112
|
* @private
|
|
113
113
|
* @returns Mutable link element
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { withBase } from '@rspress/core/runtime'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prefix `value` with the configured Rspress `base` only when it's an
|
|
5
|
+
* absolute path that would otherwise escape the mount. Pass-through for
|
|
6
|
+
* data URIs, fully-qualified URLs, hash anchors, and relative paths —
|
|
7
|
+
* applying `withBase()` to those produces broken results (the kitchen-
|
|
8
|
+
* sink data-URI logo regression that the examples e2e caught).
|
|
9
|
+
*
|
|
10
|
+
* Use this wrapper at every render site that takes a user-supplied asset
|
|
11
|
+
* URL from the ciderpress config (`logo`, `heroDemo.src`, brand chips
|
|
12
|
+
* etc.), instead of calling `withBase()` directly.
|
|
13
|
+
*
|
|
14
|
+
* @param value - Asset URL or path from user config (may be `undefined`)
|
|
15
|
+
* @returns The base-prefixed URL when applicable, otherwise `value` as-is
|
|
16
|
+
*/
|
|
17
|
+
export function withMountBase(value: string | undefined): string {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return ''
|
|
20
|
+
}
|
|
21
|
+
if (!value.startsWith('/')) {
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
// Data URIs (`data:image/svg+xml;...`) and protocol-relative or
|
|
25
|
+
// explicit-protocol URLs are absolute in their own namespace — never
|
|
26
|
+
// prefix.
|
|
27
|
+
if (value.startsWith('//') || /^[a-z][a-z0-9+\-.]*:/i.test(value)) {
|
|
28
|
+
return value
|
|
29
|
+
}
|
|
30
|
+
return withBase(value)
|
|
31
|
+
}
|