@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.
Files changed (106) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.mjs +4 -3
  3. package/dist/node.d.ts +1 -1
  4. package/dist/node.mjs +335 -51
  5. package/dist/plugins/mermaid/MermaidRenderer.tsx +49 -6
  6. package/dist/theme/components/footer/site-footer.tsx +68 -9
  7. package/dist/theme/components/home/feature-card.tsx +5 -8
  8. package/dist/theme/components/home/feature.tsx +31 -11
  9. package/dist/theme/components/home/hero-demo-custom.tsx +116 -0
  10. package/dist/theme/components/home/hero-demo.css +27 -0
  11. package/dist/theme/components/home/layout.tsx +149 -26
  12. package/dist/theme/components/home/split-visual-custom.tsx +26 -0
  13. package/dist/theme/components/home/workspaces.tsx +39 -3
  14. package/dist/theme/components/nav/branch-tag.tsx +7 -7
  15. package/dist/theme/components/nav/ciderpress-docs-bar.css +13 -1
  16. package/dist/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
  17. package/dist/theme/components/nav/ciderpress-header.css +2 -1
  18. package/dist/theme/components/nav/ciderpress-header.tsx +26 -3
  19. package/dist/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
  20. package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
  21. package/dist/theme/components/nav/ciderpress-nav-menu.css +88 -0
  22. package/dist/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
  23. package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
  24. package/dist/theme/components/nav/floating-branch-indicator.tsx +7 -7
  25. package/dist/theme/components/nav/header-icon.css +24 -0
  26. package/dist/theme/components/nav/header-icon.tsx +119 -0
  27. package/dist/theme/components/nav/header-logo.css +16 -0
  28. package/dist/theme/components/nav/header-logo.tsx +200 -0
  29. package/dist/theme/components/nav/layout.tsx +117 -16
  30. package/dist/theme/components/nav/nav-logo.tsx +16 -19
  31. package/dist/theme/components/openapi/index.ts +0 -2
  32. package/dist/theme/components/openapi/openapi.css +0 -14
  33. package/dist/theme/components/shared/card-icon.tsx +37 -0
  34. package/dist/theme/components/shared/ciderpress-logo.tsx +5 -1
  35. package/dist/theme/components/shared/icon.tsx +120 -35
  36. package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
  37. package/dist/theme/components/shared/section-card.tsx +37 -6
  38. package/dist/theme/components/sidebar/sidebar-badge.css +50 -0
  39. package/dist/theme/components/sidebar/sidebar-badge.tsx +153 -0
  40. package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
  41. package/dist/theme/components/sidebar/sidebar-scope.tsx +24 -1
  42. package/dist/theme/components/theme-provider.tsx +27 -11
  43. package/dist/theme/components/workspaces/card.tsx +7 -9
  44. package/dist/theme/hooks/use-ciderpress.ts +65 -5
  45. package/dist/theme/hooks/use-nav-items.ts +82 -11
  46. package/dist/theme/index.tsx +5 -3
  47. package/dist/theme/lib/read-social-links.ts +4 -3
  48. package/dist/theme/lib/theme-favicon.ts +1 -1
  49. package/dist/theme/lib/with-mount-base.ts +31 -0
  50. package/dist/theme/styles/overrides/footnotes.css +54 -0
  51. package/dist/theme/styles/overrides/lists.css +83 -0
  52. package/dist/theme/styles/overrides/rail.css +27 -10
  53. package/dist/theme/styles/overrides/section-card.css +9 -0
  54. package/dist/theme/styles/overrides/sidebar.css +22 -0
  55. package/package.json +18 -16
  56. package/src/theme/components/footer/site-footer.tsx +68 -9
  57. package/src/theme/components/home/feature-card.tsx +5 -8
  58. package/src/theme/components/home/feature.tsx +31 -11
  59. package/src/theme/components/home/hero-demo-custom.tsx +116 -0
  60. package/src/theme/components/home/hero-demo.css +27 -0
  61. package/src/theme/components/home/layout.tsx +149 -26
  62. package/src/theme/components/home/split-visual-custom.tsx +26 -0
  63. package/src/theme/components/home/workspaces.tsx +39 -3
  64. package/src/theme/components/nav/branch-tag.tsx +7 -7
  65. package/src/theme/components/nav/ciderpress-docs-bar.css +13 -1
  66. package/src/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
  67. package/src/theme/components/nav/ciderpress-header.css +2 -1
  68. package/src/theme/components/nav/ciderpress-header.tsx +26 -3
  69. package/src/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
  70. package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
  71. package/src/theme/components/nav/ciderpress-nav-menu.css +88 -0
  72. package/src/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
  73. package/src/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
  74. package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
  75. package/src/theme/components/nav/header-icon.css +24 -0
  76. package/src/theme/components/nav/header-icon.tsx +119 -0
  77. package/src/theme/components/nav/header-logo.css +16 -0
  78. package/src/theme/components/nav/header-logo.tsx +200 -0
  79. package/src/theme/components/nav/layout.tsx +117 -16
  80. package/src/theme/components/nav/nav-logo.tsx +16 -19
  81. package/src/theme/components/openapi/index.ts +0 -2
  82. package/src/theme/components/openapi/openapi.css +0 -14
  83. package/src/theme/components/shared/card-icon.tsx +37 -0
  84. package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
  85. package/src/theme/components/shared/icon.tsx +120 -35
  86. package/src/theme/components/shared/resolve-card-icon.ts +53 -12
  87. package/src/theme/components/shared/section-card.tsx +37 -6
  88. package/src/theme/components/sidebar/sidebar-badge.css +50 -0
  89. package/src/theme/components/sidebar/sidebar-badge.tsx +153 -0
  90. package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
  91. package/src/theme/components/sidebar/sidebar-scope.tsx +24 -1
  92. package/src/theme/components/theme-provider.tsx +27 -11
  93. package/src/theme/components/workspaces/card.tsx +7 -9
  94. package/src/theme/hooks/use-ciderpress.ts +65 -5
  95. package/src/theme/hooks/use-nav-items.ts +82 -11
  96. package/src/theme/index.tsx +5 -3
  97. package/src/theme/lib/read-social-links.ts +4 -3
  98. package/src/theme/lib/theme-favicon.ts +1 -1
  99. package/src/theme/lib/with-mount-base.ts +31 -0
  100. package/src/theme/styles/overrides/footnotes.css +54 -0
  101. package/src/theme/styles/overrides/lists.css +83 -0
  102. package/src/theme/styles/overrides/rail.css +27 -10
  103. package/src/theme/styles/overrides/section-card.css +9 -0
  104. package/src/theme/styles/overrides/sidebar.css +22 -0
  105. package/dist/theme/components/openapi/copy-markdown-button.tsx +0 -41
  106. package/src/theme/components/openapi/copy-markdown-button.tsx +0 -41
@@ -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 { Icon } from '../shared/icon'
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 or `{ id, color }` object.
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?: string | { readonly id: string; readonly color: string }
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) => <Icon icon={r.id} />)
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
- <span className={`cp-card__icon cp-card__icon--${iconColor}`}>{iconEl}</span>
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, SiteConfig } from '@ciderpress/config'
1
+ import type { BadgeConfig, 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?: string
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?: string | { readonly id: string; readonly color: string }
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: string | { readonly id: string; readonly color: string } | undefined
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,65 @@ 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
+ readonly feedback: {
90
+ readonly enabled: boolean
91
+ readonly question: string | undefined
92
+ }
93
+ }
94
+
36
95
  interface CiderpressThemeConfig {
37
96
  readonly sidebar: Record<string, readonly CiderpressSidebarItem[]>
38
97
  readonly sidebarAbove: readonly CiderpressSidebarLink[] | undefined
@@ -41,7 +100,8 @@ interface CiderpressThemeConfig {
41
100
  readonly standaloneScopePaths: readonly string[] | undefined
42
101
  readonly home: HomeConfig | undefined
43
102
  readonly ciderpressFooter: FooterConfig | undefined
44
- readonly site: SiteConfig | undefined
103
+ readonly site: CiderpressSiteBlock | undefined
104
+ readonly pageBadges: Record<string, readonly BadgeConfig[]> | undefined
45
105
  }
46
106
 
47
107
  /**
@@ -1,3 +1,4 @@
1
+ import { removeBase } from '@rspress/core/runtime'
1
2
  import { useEffect, useState } from 'react'
2
3
 
3
4
  import type { CiderpressNavMenuItem } from '../components/nav/ciderpress-nav-menu'
@@ -41,34 +42,104 @@ export function useNavItems(): readonly CiderpressNavMenuItem[] {
41
42
  }
42
43
 
43
44
  /**
44
- * Read every anchor under Rspress's hidden `.rp-nav-menu` and project
45
- * it into a `{ text, link }` item. Anchors with empty text or `href`
46
- * are dropped so we never surface a placeholder entry.
45
+ * Reconstruct the primary nav from Rspress's hidden `.rp-nav-menu`,
46
+ * preserving dropdowns. Each top-level `.rp-nav-menu__item` is either a
47
+ * leaf (its container is an anchor) or a dropdown parent (it wraps a
48
+ * `.rp-hover-group` of child links). Items with empty text, or dropdown
49
+ * parents with no usable children, are dropped.
47
50
  *
48
51
  * @private
49
52
  * @returns Nav items currently in the DOM (empty array when not mounted).
50
53
  */
51
54
  function scrapeNavItems(): readonly CiderpressNavMenuItem[] {
52
- const anchors = document.querySelectorAll<HTMLAnchorElement>('.rp-nav-menu .rp-nav-menu__item a')
55
+ return navMenuRoots()
56
+ .map(scrapeNavItem)
57
+ .filter((item): item is CiderpressNavMenuItem => item !== null)
58
+ }
59
+
60
+ /**
61
+ * Collect the top-level `.rp-nav-menu__item` `<li>`s to scrape. Rspress
62
+ * renders separate left and right nav `<ul>`s; the ciderpress topbar is
63
+ * right-aligned, so we read the right menu and only fall back to the
64
+ * unscoped selector when it isn't present.
65
+ *
66
+ * @private
67
+ * @returns Top-level nav item elements.
68
+ */
69
+ function navMenuRoots(): readonly HTMLElement[] {
70
+ const right = document.querySelectorAll<HTMLElement>('.rp-nav-menu--right > .rp-nav-menu__item')
71
+ if (right.length > 0) {
72
+ return [...right]
73
+ }
74
+ return [...document.querySelectorAll<HTMLElement>('.rp-nav-menu > .rp-nav-menu__item')]
75
+ }
76
+
77
+ /**
78
+ * Project a single top-level `.rp-nav-menu__item` element into a nav
79
+ * item, recursing one level into its `.rp-hover-group` dropdown when
80
+ * present.
81
+ *
82
+ * @private
83
+ * @param root - Top-level nav `<li>` element.
84
+ * @returns Parsed nav item, or `null` when unusable.
85
+ */
86
+ function scrapeNavItem(root: HTMLElement): CiderpressNavMenuItem | null {
87
+ const container = root.querySelector(':scope > .rp-nav-menu__item__container')
88
+ if (container === null) {
89
+ return null
90
+ }
91
+ const text = readElementText(container)
92
+ if (text === '') {
93
+ return null
94
+ }
95
+ const group = root.querySelector(':scope > .rp-hover-group')
96
+ if (group !== null) {
97
+ const items = scrapeGroupItems(group)
98
+ if (items.length === 0) {
99
+ return null
100
+ }
101
+ return { text, items }
102
+ }
103
+ const href = container.getAttribute('href')
104
+ if (href === null || href === '') {
105
+ return null
106
+ }
107
+ // Un-base the scraped href so `<Link>` re-applies the site `base` once
108
+ // rather than doubling the mount prefix on subpath deploys.
109
+ return { text, link: removeBase(href) }
110
+ }
111
+
112
+ /**
113
+ * Read the child links out of a Rspress `.rp-hover-group` dropdown.
114
+ *
115
+ * @private
116
+ * @param group - The `.rp-hover-group` element.
117
+ * @returns Child nav items with text + link (empties dropped).
118
+ */
119
+ function scrapeGroupItems(group: Element): readonly CiderpressNavMenuItem[] {
120
+ const anchors = group.querySelectorAll<HTMLAnchorElement>('.rp-hover-group__item__link')
53
121
  return [...anchors]
54
122
  .map((anchor) => ({
55
- text: readAnchorText(anchor),
56
- link: anchor.getAttribute('href') ?? '',
123
+ text: readElementText(anchor),
124
+ // Rspress's rendered hrefs already include the site `base`; strip it so
125
+ // the consuming `<Link>` re-applies it once instead of doubling the mount
126
+ // prefix on subpath deploys (the `/examples/<slug>/examples/<slug>/…` 404).
127
+ link: removeBase(anchor.getAttribute('href') ?? ''),
57
128
  }))
58
129
  .filter((item) => item.text !== '' && item.link !== '')
59
130
  }
60
131
 
61
132
  /**
62
- * Pull the trimmed text content from an anchor. Returns an empty
133
+ * Pull the trimmed text content from an element. Returns an empty
63
134
  * string when `textContent` is missing — callers treat empty as "skip
64
- * this anchor".
135
+ * this element".
65
136
  *
66
137
  * @private
67
- * @param anchor - Anchor element to read.
138
+ * @param element - Element to read.
68
139
  * @returns Trimmed inner text, or empty string when absent.
69
140
  */
70
- function readAnchorText(anchor: HTMLAnchorElement): string {
71
- const text = anchor.textContent
141
+ function readElementText(element: Element): string {
142
+ const text = element.textContent
72
143
  if (text === null) {
73
144
  return ''
74
145
  }
@@ -19,7 +19,6 @@ import '@rspress/core/dist/theme/components/Sidebar/SidebarDivider.css'
19
19
  import '@rspress/core/dist/theme/components/Sidebar/SidebarGroup.css'
20
20
  import '@rspress/core/dist/theme/components/Sidebar/SidebarItem.css'
21
21
  import '@rspress/core/dist/theme/components/Sidebar/SidebarSectionHeader.css'
22
-
23
22
  import './styles/layers.css'
24
23
  import './styles/overrides/fonts.css'
25
24
  import './styles/overrides/tokens.css'
@@ -39,6 +38,8 @@ import './styles/themes/arcade.css'
39
38
  // (border trace, neon pulse, CRT scanlines, etc.) scoped to [data-cp-theme='arcade']
40
39
  import './styles/themes/arcade-fx.css'
41
40
  import './styles/overrides/details.css'
41
+ import './styles/overrides/lists.css'
42
+ import './styles/overrides/footnotes.css'
42
43
  import './styles/overrides/scrollbar.css'
43
44
  import './styles/overrides/rail.css'
44
45
  import './styles/overrides/sidebar.css'
@@ -54,6 +55,7 @@ import './components/content-footer/feedback.css'
54
55
  import './components/content-footer/meta-actions.css'
55
56
  import './components/content-footer/page-pager.css'
56
57
  import './components/sidebar/framework-picker.css'
58
+ import './components/sidebar/sidebar-badge.css'
57
59
  import './components/sidebar/sidebar-promo.css'
58
60
  import './components/sidebar/sidebar-toggle.css'
59
61
  import './components/home/page-rail.css'
@@ -90,6 +92,8 @@ export { HomeFeature } from './components/home/feature'
90
92
  export { HomeLayout } from './components/home/layout'
91
93
  /** @internal Sidebar override — multi-scope filtering for standalone sections */
92
94
  export { Sidebar } from './components/sidebar/sidebar-scope'
95
+ /** @internal Tag override — renders ciderpress sidebar badges, delegates other tags to Rspress */
96
+ export { Tag } from './components/sidebar/sidebar-badge'
93
97
 
94
98
  export { FeatureCard, FeatureGrid } from './components/home/feature-card'
95
99
  export type { FeatureCardProps, FeatureItem } from './components/home/feature-card'
@@ -150,8 +154,6 @@ export type { StepsProps, StepProps } from './components/shared/steps'
150
154
  export { Field, FieldGroup } from './components/shared/field'
151
155
  export type { FieldProps, FieldGroupProps } from './components/shared/field'
152
156
 
153
- export { CopyMarkdownButton } from './components/openapi'
154
- export type { CopyMarkdownButtonProps } from './components/openapi'
155
157
  export { OpenAPIOperation } from './components/openapi'
156
158
  export type { OpenAPIOperationProps } from './components/openapi'
157
159
  export { OpenAPIOverview } from './components/openapi'
@@ -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 Rspress
6
- * normalisation but isn't surfaced on the public typings.
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 { content?: unknown }).content === 'string'
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 `config.icon`).
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
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * GFM footnote section styling — deliberately UNLAYERED.
3
+ *
4
+ * `remark-gfm` appends a `<section class="footnotes">` with an `<ol>` of
5
+ * definitions and a `#footnote-label` heading. Rspress tags that heading
6
+ * `sr-only` (visually hidden, kept for a11y + the on-this-page nav), but the
7
+ * theme never defined `.sr-only`, so it rendered as a full 1.75rem section
8
+ * heading and the definitions sat at body size with no separation from the
9
+ * content above — nothing like how GitHub and most renderers present footnotes.
10
+ *
11
+ * These rules are NOT in `ciderpress.overrides`: Rspress sizes headings with an
12
+ * unlayered, low-specificity `.rp-doc :not(:where(...)):where(h2)` rule
13
+ * (specificity 0,1,0), and layer-scoped rules always lose to unlayered ones
14
+ * regardless of specificity. Match Rspress's approach — unlayered, class-scoped
15
+ * under `.rp-doc` (specificity 0,2,0+ > 0,1,0) — so the label restyle actually
16
+ * wins. Restores the expected treatment: a top-border separator, a small muted
17
+ * label, and compact muted definition text set apart from the body.
18
+ */
19
+
20
+ .rp-doc .footnotes {
21
+ margin-block-start: 2.5rem;
22
+ padding-block-start: 1.25rem;
23
+ border-block-start: 1px solid var(--cp-c-border);
24
+ font-size: 0.85em;
25
+ color: var(--cp-c-text-2);
26
+ }
27
+
28
+ .rp-doc .footnotes > h2 {
29
+ margin: 0 0 0.75rem;
30
+ padding: 0;
31
+ border: 0;
32
+ font-size: 0.75rem;
33
+ font-weight: 600;
34
+ letter-spacing: 0.05em;
35
+ text-transform: uppercase;
36
+ color: var(--cp-c-text-3);
37
+ }
38
+
39
+ .rp-doc .footnotes ol {
40
+ margin: 0;
41
+ }
42
+
43
+ .rp-doc .footnotes li {
44
+ margin-block: 0.35rem;
45
+ }
46
+
47
+ .rp-doc .footnotes li p {
48
+ margin: 0.25rem 0;
49
+ }
50
+
51
+ .rp-doc .data-footnote-backref {
52
+ margin-inline-start: 0.35em;
53
+ text-decoration: none;
54
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Content list markers — fixed-width marker gutter.
3
+ *
4
+ * Rspress renders list markers `outside`, which right-aligns them to the text
5
+ * edge: a wide decimal marker (`10.`, `99.`) hangs further left than a bullet,
6
+ * so the marker column differs between `ol` and `ul` and numbers drift into the
7
+ * body-text margin. Text lines up, but the markers do not.
8
+ *
9
+ * Replace the native markers with a fixed-width gutter shared by `ol` and `ul`,
10
+ * sized for two digits (`99.`). Every marker occupies the same column, decimal
11
+ * points align, and list text starts at the same offset regardless of marker
12
+ * width. `ol` numbering comes from the automatic `list-item` counter (still
13
+ * incremented under `list-style: none`), so nested lists and `start` offsets
14
+ * keep working. The marker is absolutely positioned in the `li` inline padding
15
+ * so it aligns to the first line whether the item holds inline text (tight
16
+ * lists) or a block `<p>` (loose lists, e.g. footnote definitions); wrapped and
17
+ * following lines hang under the text.
18
+ *
19
+ * Deliberately UNLAYERED: Rspress sets base list padding with an unlayered,
20
+ * low-specificity `.rp-doc :not(:where(...)):where(ol|ul)` rule (specificity
21
+ * 0,1,0). Layer-scoped rules always lose to unlayered ones regardless of
22
+ * specificity, so these match Rspress's approach — unlayered, class-scoped under
23
+ * `.rp-doc` (specificity 0,2,0+ > 0,1,0).
24
+ *
25
+ * Task lists (GFM `- [ ]`) are excluded — they render a checkbox instead of a
26
+ * marker; see the dedicated rules below.
27
+ */
28
+
29
+ .rp-doc :where(ol, ul):not(.contains-task-list) {
30
+ list-style: none;
31
+ padding-inline-start: 0;
32
+ }
33
+
34
+ .rp-doc :where(ol, ul):not(.contains-task-list) > li {
35
+ position: relative;
36
+ padding-inline-start: 2.1em;
37
+ margin-block: 0.35rem;
38
+ }
39
+
40
+ .rp-doc :where(ol, ul):not(.contains-task-list) > li::before {
41
+ position: absolute;
42
+ inset-inline-start: 0;
43
+ width: 1.6em;
44
+ text-align: end;
45
+ }
46
+
47
+ .rp-doc ol:not(.contains-task-list) > li::before {
48
+ content: counter(list-item) '.';
49
+ }
50
+
51
+ .rp-doc ul:not(.contains-task-list) > li::before {
52
+ content: '•';
53
+ box-sizing: border-box;
54
+ padding-inline-end: 0.22em;
55
+ }
56
+
57
+ /*
58
+ * Task-list normalization — deliberately UNLAYERED (same specificity reasoning
59
+ * as above). GFM leaves a `disc` bullet on `- [ ]` items next to the checkbox;
60
+ * hide it, then seat the item in the SAME marker gutter as ordered/unordered
61
+ * lists so task text lines up with every other list. The checkbox is absolutely
62
+ * positioned in the gutter (right edge at the marker column, centered on the
63
+ * first line), so text starts at the shared 2.5em offset regardless of the
64
+ * checkbox's intrinsic size.
65
+ */
66
+ .rp-doc .contains-task-list {
67
+ list-style: none;
68
+ padding-inline-start: 0;
69
+ }
70
+
71
+ .rp-doc .task-list-item {
72
+ position: relative;
73
+ list-style: none;
74
+ padding-inline-start: 2.1em;
75
+ margin-block: 0.35rem;
76
+ }
77
+
78
+ .rp-doc .task-list-item > input[type='checkbox'] {
79
+ position: absolute;
80
+ inset-inline-start: 0.8em;
81
+ inset-block-start: 0.45em;
82
+ margin: 0;
83
+ }
@@ -145,18 +145,22 @@ html .rp-doc-layout__sidebar > :first-child:not(.cp-sidebar-top) {
145
145
  }
146
146
 
147
147
  html .cp-sidebar-bottom {
148
- /* Grow to fill the remaining sidebar space so the nav tree stacks
149
- from the top and this band occupies everything below it. When the
150
- nav tree is short, the band is tall and its content (promo +
151
- below-links) sits at its bottom via `justify-content: flex-end`. */
152
- flex: 1 0 auto;
148
+ /* Size to content and sit directly under the nav tree. `flex: 0 0 auto`
149
+ (no grow) is deliberate: growing to fill (`1 0 auto`) stretched the
150
+ band across all leftover sidebar height, and a short nav tree left a
151
+ tall empty band with its content (promo + below-links) stranded at the
152
+ bottom. With no grow the band is exactly as tall as its content and
153
+ the promo/links hug the last nav item. */
154
+ flex: 0 0 auto;
153
155
  display: flex;
154
156
  flex-direction: column;
155
- justify-content: flex-end;
156
- /* When the sidebar scrolls (nav tree taller than viewport), this band
157
- stays glued to the viewport bottom so the promo is always visible.
158
- `z-index: 3` so the band paints above the thin overlay scrollbar
159
- thumb so it doesn't appear to "go through" the band region. */
157
+ /* When the nav tree overflows the viewport the sidebar scrolls; this
158
+ band stays glued to the viewport bottom (`sticky bottom: 0`) so the
159
+ promo/links remain visible while the tree scrolls behind them. With a
160
+ short (non-scrolling) nav tree the sticky offset is never engaged, so
161
+ the band simply flows under the last nav item. `z-index: 3` paints it
162
+ above the thin overlay scrollbar thumb so it doesn't appear to "go
163
+ through" the band region. */
160
164
  position: sticky;
161
165
  bottom: 0;
162
166
  z-index: 3;
@@ -189,6 +193,19 @@ html .rp-outline__title {
189
193
  display: none;
190
194
  }
191
195
 
196
+ /* "On this page" outline — truncate long headings to a single line with
197
+ an ellipsis instead of wrapping. Rspress already sets a native `title`
198
+ on each `.rp-toc-item` anchor, so hovering a truncated heading reveals
199
+ the full text. Targets `.rp-toc-item__text`, shared by the desktop
200
+ rail and the mobile outline dropdown. */
201
+ html .rp-toc-item__text {
202
+ display: block;
203
+ min-width: 0;
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
207
+ }
208
+
192
209
  html .rp-doc-layout__doc {
193
210
  background: var(--cp-c-bg);
194
211
  }
@@ -31,6 +31,15 @@
31
31
  gap: 10px;
32
32
  }
33
33
 
34
+ /* Page badges pinned to the right of the card title row. */
35
+ .cp-section-card__badges {
36
+ margin-left: auto;
37
+ display: inline-flex;
38
+ align-items: center;
39
+ gap: 4px;
40
+ flex: 0 0 auto;
41
+ }
42
+
34
43
  .cp-section-card:hover {
35
44
  border-color: var(--cp-c-brand-1);
36
45
  background: var(--cp-c-bg-elv);
@@ -24,6 +24,28 @@
24
24
  }
25
25
  }
26
26
 
27
+ /* ── Long titles — single-line ellipsis, badge stays pinned ──── */
28
+ /* The item's left slot holds the label; constrain it so the label
29
+ truncates instead of pushing the badge (right slot) out of view.
30
+ A `title` attribute is added by the Sidebar effect on overflow. */
31
+ html .rp-sidebar-item__left {
32
+ min-width: 0;
33
+ flex: 1 1 auto;
34
+ overflow: hidden;
35
+ }
36
+
37
+ html .rp-sidebar-item__left > .rp-doc {
38
+ min-width: 0;
39
+ overflow: hidden;
40
+ text-overflow: ellipsis;
41
+ white-space: nowrap;
42
+ }
43
+
44
+ html .rp-sidebar-item__right {
45
+ flex: 0 0 auto;
46
+ margin-left: 6px;
47
+ }
48
+
27
49
  /* ── Group section headers — mono ALL CAPS (mockup pattern) ──── */
28
50
  html .rp-sidebar-section-header {
29
51
  font-family: var(--cp-font-family-mono);