@ciderpress/ui 1.0.0-rc.1 → 1.0.0-rc.10
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 +335 -51
- package/dist/plugins/mermaid/MermaidRenderer.tsx +49 -6
- 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-docs-bar.css +13 -1
- package/dist/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
- 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-hamburger.css +50 -0
- package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
- package/dist/theme/components/nav/ciderpress-nav-menu.css +88 -0
- package/dist/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
- package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
- 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 +117 -16
- package/dist/theme/components/nav/nav-logo.tsx +16 -19
- package/dist/theme/components/openapi/index.ts +0 -2
- package/dist/theme/components/openapi/openapi.css +0 -14
- 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/icon.tsx +120 -35
- package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
- package/dist/theme/components/shared/section-card.tsx +37 -6
- package/dist/theme/components/sidebar/sidebar-badge.css +50 -0
- package/dist/theme/components/sidebar/sidebar-badge.tsx +153 -0
- package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/dist/theme/components/sidebar/sidebar-scope.tsx +24 -1
- 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 +65 -5
- package/dist/theme/hooks/use-nav-items.ts +82 -11
- package/dist/theme/index.tsx +5 -3
- 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/dist/theme/styles/overrides/footnotes.css +54 -0
- package/dist/theme/styles/overrides/lists.css +83 -0
- package/dist/theme/styles/overrides/rail.css +27 -10
- package/dist/theme/styles/overrides/section-card.css +9 -0
- package/dist/theme/styles/overrides/sidebar.css +22 -0
- package/package.json +18 -16
- 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-docs-bar.css +13 -1
- package/src/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
- 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-hamburger.css +50 -0
- package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
- package/src/theme/components/nav/ciderpress-nav-menu.css +88 -0
- package/src/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
- package/src/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
- 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 +117 -16
- package/src/theme/components/nav/nav-logo.tsx +16 -19
- package/src/theme/components/openapi/index.ts +0 -2
- package/src/theme/components/openapi/openapi.css +0 -14
- 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/icon.tsx +120 -35
- package/src/theme/components/shared/resolve-card-icon.ts +53 -12
- package/src/theme/components/shared/section-card.tsx +37 -6
- package/src/theme/components/sidebar/sidebar-badge.css +50 -0
- package/src/theme/components/sidebar/sidebar-badge.tsx +153 -0
- package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/src/theme/components/sidebar/sidebar-scope.tsx +24 -1
- 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 +65 -5
- package/src/theme/hooks/use-nav-items.ts +82 -11
- package/src/theme/index.tsx +5 -3
- 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
- package/src/theme/styles/overrides/footnotes.css +54 -0
- package/src/theme/styles/overrides/lists.css +83 -0
- package/src/theme/styles/overrides/rail.css +27 -10
- package/src/theme/styles/overrides/section-card.css +9 -0
- package/src/theme/styles/overrides/sidebar.css +22 -0
- package/dist/theme/components/openapi/copy-markdown-button.tsx +0 -41
- package/src/theme/components/openapi/copy-markdown-button.tsx +0 -41
|
@@ -1,41 +1,126 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import mdi from '@iconify-json/mdi/icons.json' with { type: 'json' }
|
|
6
|
-
import pixelarticons from '@iconify-json/pixelarticons/icons.json' with { type: 'json' }
|
|
7
|
-
import simpleIcons from '@iconify-json/simple-icons/icons.json' with { type: 'json' }
|
|
8
|
-
import skillIcons from '@iconify-json/skill-icons/icons.json' with { type: 'json' }
|
|
9
|
-
import vscodeIcons from '@iconify-json/vscode-icons/icons.json' with { type: 'json' }
|
|
10
|
-
import { addCollection, Icon } from '@iconify/react'
|
|
11
|
-
|
|
12
|
-
// Register all icon collections for offline Iconify resolution.
|
|
13
|
-
// `addCollection` is called purely for its side effect of mutating
|
|
14
|
-
// Iconify's internal registry. Holding the return values in a
|
|
15
|
-
// throwaway `const` keeps the call list a single expression statement
|
|
16
|
-
// rather than nine misleading named exports.
|
|
17
|
-
// oxlint-disable-next-line no-unused-vars
|
|
18
|
-
const _iconCollectionsLoaded = [
|
|
19
|
-
addCollection(cast(pixelarticons)),
|
|
20
|
-
addCollection(cast(devicon)),
|
|
21
|
-
addCollection(cast(mdi)),
|
|
22
|
-
addCollection(cast(simpleIcons)),
|
|
23
|
-
addCollection(cast(skillIcons)),
|
|
24
|
-
addCollection(cast(catppuccin)),
|
|
25
|
-
addCollection(cast(logos)),
|
|
26
|
-
addCollection(cast(vscodeIcons)),
|
|
27
|
-
addCollection(cast(materialIconTheme)),
|
|
28
|
-
] as const
|
|
29
|
-
|
|
30
|
-
export { Icon }
|
|
1
|
+
import { addCollection, Icon as IconifyIcon } from '@iconify/react'
|
|
2
|
+
import type { IconProps } from '@iconify/react'
|
|
3
|
+
import type React from 'react'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
31
5
|
|
|
32
6
|
/**
|
|
33
|
-
*
|
|
7
|
+
* Per-collection lazy loaders keyed by Iconify prefix.
|
|
8
|
+
*
|
|
9
|
+
* Each entry is a bare dynamic `import()` so the consuming site's Rsbuild
|
|
10
|
+
* build emits **one async chunk per collection** instead of folding all ten
|
|
11
|
+
* `icons.json` files into a single eager ~30MB chunk pulled on every route.
|
|
12
|
+
* Two consequences fall out of that:
|
|
13
|
+
*
|
|
14
|
+
* - **Deployability** — the largest collection (`logos`, ~8MB) stays well
|
|
15
|
+
* under per-file host caps (Cloudflare Pages rejects files >25MB), where
|
|
16
|
+
* the combined blob failed outright.
|
|
17
|
+
* - **Performance** — a page only downloads the collections it actually
|
|
18
|
+
* references, not the full set on first paint.
|
|
19
|
+
*
|
|
20
|
+
* The specifiers are string literals (not computed) so the bundler can
|
|
21
|
+
* statically resolve every chunk at build time.
|
|
22
|
+
*
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
const COLLECTION_LOADERS: Record<string, () => Promise<{ readonly default: unknown }>> = {
|
|
26
|
+
catppuccin: () => import('@iconify-json/catppuccin/icons.json'),
|
|
27
|
+
devicon: () => import('@iconify-json/devicon/icons.json'),
|
|
28
|
+
logos: () => import('@iconify-json/logos/icons.json'),
|
|
29
|
+
'material-icon-theme': () => import('@iconify-json/material-icon-theme/icons.json'),
|
|
30
|
+
mdi: () => import('@iconify-json/mdi/icons.json'),
|
|
31
|
+
pixel: () => import('@iconify-json/pixel/icons.json'),
|
|
32
|
+
pixelarticons: () => import('@iconify-json/pixelarticons/icons.json'),
|
|
33
|
+
'simple-icons': () => import('@iconify-json/simple-icons/icons.json'),
|
|
34
|
+
'skill-icons': () => import('@iconify-json/skill-icons/icons.json'),
|
|
35
|
+
'vscode-icons': () => import('@iconify-json/vscode-icons/icons.json'),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cache of in-flight / settled collection registrations keyed by prefix.
|
|
40
|
+
* Guarantees each collection's chunk is fetched and merged into Iconify's
|
|
41
|
+
* registry exactly once, regardless of how many `<Icon>` instances on a
|
|
42
|
+
* page reference it.
|
|
43
|
+
*
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
const collectionCache = new Map<string, Promise<void>>()
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Offline-registered Iconify icon.
|
|
50
|
+
*
|
|
51
|
+
* Renders `@iconify/react`'s `Icon` unchanged, but registers the icon's
|
|
52
|
+
* collection on demand: the first time a prefix is seen the matching
|
|
53
|
+
* `@iconify-json` chunk is dynamically imported and merged into Iconify's
|
|
54
|
+
* registry, then a re-render paints the resolved SVG. Because `IconifyIcon`
|
|
55
|
+
* reads the live registry on every render, an icon appears as soon as its
|
|
56
|
+
* collection chunk resolves.
|
|
57
|
+
*
|
|
58
|
+
* @param props - Standard `@iconify/react` icon props; `icon` is the
|
|
59
|
+
* `prefix:name` identifier (e.g. `devicon:typescript`)
|
|
60
|
+
* @returns The Iconify icon element
|
|
61
|
+
*/
|
|
62
|
+
export function Icon(props: IconProps): React.ReactElement {
|
|
63
|
+
const prefix = resolvePrefix(props.icon)
|
|
64
|
+
const [, markRegistered] = useState(false)
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
ensureCollection(prefix).then(() => markRegistered(true))
|
|
68
|
+
}, [prefix])
|
|
69
|
+
|
|
70
|
+
return <IconifyIcon {...props} />
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Dynamically import and register the collection for a prefix, once.
|
|
75
|
+
*
|
|
76
|
+
* Returns the cached registration promise on repeat calls so the chunk is
|
|
77
|
+
* fetched a single time. Unknown prefixes (no bundled collection) resolve
|
|
78
|
+
* immediately — `IconifyIcon` falls back to its own resolution for those.
|
|
79
|
+
*
|
|
80
|
+
* @private
|
|
81
|
+
* @param prefix - Iconify collection prefix (e.g. `logos`)
|
|
82
|
+
* @returns Promise that settles once the collection is registered
|
|
83
|
+
*/
|
|
84
|
+
function ensureCollection(prefix: string): Promise<void> {
|
|
85
|
+
const cached = collectionCache.get(prefix)
|
|
86
|
+
if (cached !== undefined) {
|
|
87
|
+
return cached
|
|
88
|
+
}
|
|
89
|
+
const loader = COLLECTION_LOADERS[prefix]
|
|
90
|
+
if (loader === undefined) {
|
|
91
|
+
return Promise.resolve()
|
|
92
|
+
}
|
|
93
|
+
const registration = loader().then(registerModule)
|
|
94
|
+
collectionCache.set(prefix, registration)
|
|
95
|
+
return registration
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Merge a dynamically imported `icons.json` module into Iconify's registry.
|
|
100
|
+
*
|
|
101
|
+
* @private
|
|
102
|
+
* @param mod - Module namespace whose `default` export is the collection JSON
|
|
103
|
+
*/
|
|
104
|
+
function registerModule(mod: { readonly default: unknown }): void {
|
|
105
|
+
addCollection(mod.default as Parameters<typeof addCollection>[0])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract the collection prefix from an Iconify identifier. Non-string icon
|
|
110
|
+
* inputs and identifiers without a `prefix:name` shape yield an empty string,
|
|
111
|
+
* which `ensureCollection` treats as "nothing to load".
|
|
34
112
|
*
|
|
35
113
|
* @private
|
|
36
|
-
* @param
|
|
37
|
-
* @returns
|
|
114
|
+
* @param icon - The `icon` prop passed to `<Icon>`
|
|
115
|
+
* @returns The collection prefix, or `''` when none can be determined
|
|
38
116
|
*/
|
|
39
|
-
function
|
|
40
|
-
|
|
117
|
+
function resolvePrefix(icon: IconProps['icon']): string {
|
|
118
|
+
if (typeof icon !== 'string') {
|
|
119
|
+
return ''
|
|
120
|
+
}
|
|
121
|
+
const parts = icon.split(':')
|
|
122
|
+
if (parts.length < 2) {
|
|
123
|
+
return ''
|
|
124
|
+
}
|
|
125
|
+
return parts[0]
|
|
41
126
|
}
|
|
@@ -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
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import type { BadgeConfig } from '@ciderpress/config'
|
|
1
2
|
import { match, P } from 'massaman/match'
|
|
2
3
|
import type React from 'react'
|
|
3
4
|
|
|
5
|
+
import { useCiderpress } from '../../hooks/use-ciderpress'
|
|
6
|
+
import { BadgeChips } from '../sidebar/sidebar-badge'
|
|
4
7
|
import { Card } from './card'
|
|
5
|
-
import {
|
|
8
|
+
import { CardIcon } from './card-icon'
|
|
9
|
+
import type { CardIconInput } from './resolve-card-icon'
|
|
6
10
|
import { resolveCardIcon } from './resolve-card-icon'
|
|
7
11
|
|
|
8
12
|
export interface SectionCardProps {
|
|
9
13
|
readonly href: string
|
|
10
14
|
readonly title: string
|
|
11
15
|
readonly description?: string
|
|
12
|
-
readonly icon?:
|
|
16
|
+
readonly icon?: CardIconInput
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -25,7 +29,13 @@ export function SectionCard({
|
|
|
25
29
|
description,
|
|
26
30
|
icon = 'pixelarticons:file',
|
|
27
31
|
}: SectionCardProps): React.ReactElement {
|
|
28
|
-
const
|
|
32
|
+
const { pageBadges } = useCiderpress()
|
|
33
|
+
const badges = lookupBadges({ pageBadges, href })
|
|
34
|
+
const resolved = resolveCardIcon(icon) ?? {
|
|
35
|
+
kind: 'iconify' as const,
|
|
36
|
+
id: 'pixelarticons:file',
|
|
37
|
+
color: 'purple',
|
|
38
|
+
}
|
|
29
39
|
const descEl = match(description)
|
|
30
40
|
.with(P.nonNullable, (d) => <span className="cp-section-card__desc">{d}</span>)
|
|
31
41
|
.otherwise(() => null)
|
|
@@ -33,12 +43,33 @@ export function SectionCard({
|
|
|
33
43
|
return (
|
|
34
44
|
<Card href={href} className="cp-section-card">
|
|
35
45
|
<div className="cp-section-card__header">
|
|
36
|
-
<
|
|
37
|
-
<Icon icon={resolved.id} />
|
|
38
|
-
</span>
|
|
46
|
+
<CardIcon resolved={resolved} className="cp-section-card__icon" />
|
|
39
47
|
<span className="cp-section-card__title">{title}</span>
|
|
48
|
+
{badges.length > 0 && (
|
|
49
|
+
<span className="cp-section-card__badges">
|
|
50
|
+
<BadgeChips badges={badges} />
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
40
53
|
</div>
|
|
41
54
|
{descEl}
|
|
42
55
|
</Card>
|
|
43
56
|
)
|
|
44
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Look up a page's badges from the route→badges map by its href.
|
|
61
|
+
*
|
|
62
|
+
* @private
|
|
63
|
+
* @param params - The route→badges map (if present) and card destination.
|
|
64
|
+
* @returns The page's badges, or an empty array when none apply
|
|
65
|
+
*/
|
|
66
|
+
function lookupBadges(params: {
|
|
67
|
+
readonly pageBadges: Record<string, readonly BadgeConfig[]> | undefined
|
|
68
|
+
readonly href: string
|
|
69
|
+
}): readonly BadgeConfig[] {
|
|
70
|
+
const { pageBadges, href } = params
|
|
71
|
+
if (pageBadges === undefined) {
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
return pageBadges[href] ?? []
|
|
75
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar badge chips — small uppercase labels (ALPHA, WIP, …) rendered
|
|
3
|
+
* in a sidebar item's right slot via the `Tag` override.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
@layer ciderpress.overrides {
|
|
7
|
+
.cp-sbadge {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
padding: 1px 5px;
|
|
11
|
+
border-radius: var(--cp-radius-pill, 9999px);
|
|
12
|
+
font-family: var(--cp-font-family-mono, inherit);
|
|
13
|
+
font-size: 9px;
|
|
14
|
+
font-weight: 600;
|
|
15
|
+
line-height: 1.4;
|
|
16
|
+
letter-spacing: 0.04em;
|
|
17
|
+
text-transform: uppercase;
|
|
18
|
+
white-space: nowrap;
|
|
19
|
+
vertical-align: middle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.cp-sbadge + .cp-sbadge {
|
|
23
|
+
margin-left: 4px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.cp-sbadge--info {
|
|
27
|
+
background: var(--cp-c-badge-info-bg);
|
|
28
|
+
color: var(--cp-c-badge-info-fg);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.cp-sbadge--success {
|
|
32
|
+
background: var(--cp-c-badge-success-bg);
|
|
33
|
+
color: var(--cp-c-badge-success-fg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.cp-sbadge--warning {
|
|
37
|
+
background: var(--cp-c-badge-warning-bg);
|
|
38
|
+
color: var(--cp-c-badge-warning-fg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.cp-sbadge--danger {
|
|
42
|
+
background: var(--cp-c-badge-error-bg);
|
|
43
|
+
color: var(--cp-c-badge-error-fg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.cp-sbadge--neutral {
|
|
47
|
+
background: var(--rp-c-bg-mute);
|
|
48
|
+
color: var(--rp-c-text-2);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar badge rendering — overrides Rspress's `Tag` component.
|
|
3
|
+
*
|
|
4
|
+
* Rspress renders `<Tag tag={...} />` inside every sidebar item. Ciderpress
|
|
5
|
+
* encodes page badges (label + variant + tooltip) into that `tag` string via
|
|
6
|
+
* `@ciderpress/config`'s `encodeBadges`. This override decodes ciderpress
|
|
7
|
+
* badges and renders styled chips; any other tag falls through to Rspress's
|
|
8
|
+
* native `Tag` so existing tag behavior (images, keyword badges) is preserved.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { decodeBadges } from '@ciderpress/config'
|
|
12
|
+
import type { BadgeConfig, BadgeVariant } from '@ciderpress/config'
|
|
13
|
+
import { Tag as RspressTag } from '@rspress/core/theme-original'
|
|
14
|
+
import { match, P } from 'massaman/match'
|
|
15
|
+
import type React from 'react'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Props mirror Rspress's `Tag` — a single (optional) tag string.
|
|
19
|
+
*
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
interface TagProps {
|
|
23
|
+
readonly tag?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sidebar tag renderer. Renders ciderpress badge chips for encoded tags,
|
|
28
|
+
* delegating everything else to Rspress's native `Tag`.
|
|
29
|
+
*
|
|
30
|
+
* @param props - The sidebar item's tag string
|
|
31
|
+
* @returns Badge chips, a native tag, or `null` when there is no tag
|
|
32
|
+
*/
|
|
33
|
+
export function Tag({ tag }: TagProps): React.ReactElement | null {
|
|
34
|
+
const badges = decodeTag(tag)
|
|
35
|
+
if (badges === null) {
|
|
36
|
+
return <RspressTag tag={tag} />
|
|
37
|
+
}
|
|
38
|
+
return <BadgeChips badges={badges} />
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Props for {@link BadgeChips}.
|
|
43
|
+
*/
|
|
44
|
+
export interface BadgeChipsProps {
|
|
45
|
+
readonly badges: readonly BadgeConfig[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Render a list of badge chips (variant color, custom-color tint, hover
|
|
50
|
+
* tooltip). Used by the sidebar `Tag` override and the breadcrumb bar.
|
|
51
|
+
*
|
|
52
|
+
* @param props - The badges to render
|
|
53
|
+
* @returns Chip elements, or `null` when there are no badges
|
|
54
|
+
*/
|
|
55
|
+
export function BadgeChips({ badges }: BadgeChipsProps): React.ReactElement | null {
|
|
56
|
+
if (badges.length === 0) {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{badges.map((badge, index) => (
|
|
62
|
+
<SidebarBadge key={`${badge.text}-${String(index)}`} badge={badge} />
|
|
63
|
+
))}
|
|
64
|
+
</>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Decode a tag string into ciderpress badges, or `null` when the tag is
|
|
70
|
+
* absent or not ciderpress-encoded.
|
|
71
|
+
*
|
|
72
|
+
* @private
|
|
73
|
+
* @param tag - Raw tag string from the sidebar item
|
|
74
|
+
* @returns Decoded badges, or `null` to signal native fallback
|
|
75
|
+
*/
|
|
76
|
+
function decodeTag(tag: string | undefined): readonly BadgeConfig[] | null {
|
|
77
|
+
if (tag === undefined) {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
return decodeBadges(tag)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A single sidebar badge chip.
|
|
85
|
+
*
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
interface SidebarBadgeProps {
|
|
89
|
+
readonly badge: BadgeConfig
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render one badge chip with variant color (or a custom color tint) and a
|
|
94
|
+
* hover tooltip.
|
|
95
|
+
*
|
|
96
|
+
* @private
|
|
97
|
+
* @param props - The badge to render
|
|
98
|
+
* @returns A styled badge chip element
|
|
99
|
+
*/
|
|
100
|
+
function SidebarBadge({ badge }: SidebarBadgeProps): React.ReactElement {
|
|
101
|
+
const tooltip = resolveTooltip(badge)
|
|
102
|
+
return match(badge.color)
|
|
103
|
+
.with(P.string, (color) => (
|
|
104
|
+
<span className="cp-sbadge" style={{ color, backgroundColor: tint(color) }} title={tooltip}>
|
|
105
|
+
{badge.text}
|
|
106
|
+
</span>
|
|
107
|
+
))
|
|
108
|
+
.otherwise(() => (
|
|
109
|
+
<span className={variantClass(badge.variant)} title={tooltip}>
|
|
110
|
+
{badge.text}
|
|
111
|
+
</span>
|
|
112
|
+
))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the hover tooltip for a badge, defaulting to its label text.
|
|
117
|
+
*
|
|
118
|
+
* @private
|
|
119
|
+
* @param badge - The badge to resolve a tooltip for
|
|
120
|
+
* @returns The tooltip string
|
|
121
|
+
*/
|
|
122
|
+
function resolveTooltip(badge: BadgeConfig): string {
|
|
123
|
+
return badge.tooltip ?? badge.text
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Map a badge variant to its chip class, defaulting to `neutral`.
|
|
128
|
+
*
|
|
129
|
+
* @private
|
|
130
|
+
* @param variant - The badge variant, if any
|
|
131
|
+
* @returns The full class string for the chip
|
|
132
|
+
*/
|
|
133
|
+
function variantClass(variant: BadgeVariant | undefined): string {
|
|
134
|
+
return match(variant)
|
|
135
|
+
.with('info', () => 'cp-sbadge cp-sbadge--info')
|
|
136
|
+
.with('success', () => 'cp-sbadge cp-sbadge--success')
|
|
137
|
+
.with('warning', () => 'cp-sbadge cp-sbadge--warning')
|
|
138
|
+
.with('danger', () => 'cp-sbadge cp-sbadge--danger')
|
|
139
|
+
.with('neutral', () => 'cp-sbadge cp-sbadge--neutral')
|
|
140
|
+
.with(undefined, () => 'cp-sbadge cp-sbadge--neutral')
|
|
141
|
+
.exhaustive()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build a subtle background tint from a custom color.
|
|
146
|
+
*
|
|
147
|
+
* @private
|
|
148
|
+
* @param color - CSS color value
|
|
149
|
+
* @returns A `color-mix` background at low opacity
|
|
150
|
+
*/
|
|
151
|
+
function tint(color: string): string {
|
|
152
|
+
return `color-mix(in srgb, ${color} 14%, transparent)`
|
|
153
|
+
}
|
|
@@ -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)
|
|
@@ -14,7 +14,7 @@ import type { SidebarData } from '@rspress/core'
|
|
|
14
14
|
import { useActiveMatcher, useLocation, useSidebar } from '@rspress/core/runtime'
|
|
15
15
|
import { SidebarList } from '@rspress/core/theme-original'
|
|
16
16
|
import type React from 'react'
|
|
17
|
-
import { useLayoutEffect, useMemo, useState } from 'react'
|
|
17
|
+
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
|
18
18
|
|
|
19
19
|
import { useCiderpress } from '../../hooks/use-ciderpress'
|
|
20
20
|
import { resolveScopedSidebar } from './sidebar-filter'
|
|
@@ -59,9 +59,32 @@ export function Sidebar(): React.ReactElement {
|
|
|
59
59
|
setSidebarData(initializeCollapsed(filteredData, activeMatcher))
|
|
60
60
|
}, [activeMatcher, filteredData])
|
|
61
61
|
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
decorateOverflowTitles()
|
|
64
|
+
}, [sidebarData])
|
|
65
|
+
|
|
62
66
|
return <SidebarList sidebarData={sidebarData} setSidebarData={setSidebarData} />
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Add a `title` attribute to sidebar labels that overflow their column so
|
|
71
|
+
* the full text shows on hover; clear it when the label fits. Runs after
|
|
72
|
+
* render because overflow is a measured, layout-dependent property.
|
|
73
|
+
*
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
function decorateOverflowTitles(): void {
|
|
77
|
+
const labels = [...document.querySelectorAll<HTMLElement>('.rp-sidebar-item__left > .rp-doc')]
|
|
78
|
+
labels.reduce<null>((_, label) => {
|
|
79
|
+
if (label.scrollWidth > label.clientWidth) {
|
|
80
|
+
label.setAttribute('title', label.textContent ?? '')
|
|
81
|
+
} else {
|
|
82
|
+
label.removeAttribute('title')
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}, null)
|
|
86
|
+
}
|
|
87
|
+
|
|
65
88
|
/**
|
|
66
89
|
* Walk the sidebar tree and uncollapse groups that contain the active path.
|
|
67
90
|
*
|
|
@@ -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)
|