@ciderpress/ui 1.0.0-rc.1 → 1.0.0-rc.11

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 (162) hide show
  1. package/dist/head/css/loader-apple.css +1 -1
  2. package/dist/head/css/loader-dots.css +1 -1
  3. package/dist/head/css/themes/amber.css +1 -1
  4. package/dist/head/css/themes/arcade.css +1 -1
  5. package/dist/head/css/themes/grannysmith.css +1 -1
  6. package/dist/head/css/themes/honeycrisp.css +1 -1
  7. package/dist/head/css/themes/midnight.css +1 -1
  8. package/dist/head/css/themes/mulled.css +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.mjs +4 -3
  11. package/dist/node.d.ts +1 -1
  12. package/dist/node.mjs +732 -55
  13. package/dist/plugins/mermaid/MermaidRenderer.tsx +49 -6
  14. package/dist/theme/components/footer/site-footer.tsx +68 -9
  15. package/dist/theme/components/home/cta.css +15 -0
  16. package/dist/theme/components/home/cta.tsx +10 -2
  17. package/dist/theme/components/home/feature-card.css +1 -1
  18. package/dist/theme/components/home/feature-card.tsx +8 -10
  19. package/dist/theme/components/home/feature.tsx +54 -31
  20. package/dist/theme/components/home/hero-demo.css +38 -0
  21. package/dist/theme/components/home/home-visual.tsx +226 -0
  22. package/dist/theme/components/home/layout.tsx +410 -108
  23. package/dist/theme/components/home/split.css +46 -19
  24. package/dist/theme/components/home/split.tsx +28 -8
  25. package/dist/theme/components/home/tabs.css +299 -0
  26. package/dist/theme/components/home/tabs.tsx +351 -0
  27. package/dist/theme/components/home/trust-strip.css +41 -0
  28. package/dist/theme/components/home/trust-strip.tsx +168 -11
  29. package/dist/theme/components/home/workspaces.tsx +176 -38
  30. package/dist/theme/components/nav/branch-tag.tsx +7 -7
  31. package/dist/theme/components/nav/ciderpress-docs-bar.css +13 -1
  32. package/dist/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
  33. package/dist/theme/components/nav/ciderpress-header.css +2 -1
  34. package/dist/theme/components/nav/ciderpress-header.tsx +26 -3
  35. package/dist/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
  36. package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
  37. package/dist/theme/components/nav/ciderpress-nav-menu.css +88 -0
  38. package/dist/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
  39. package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
  40. package/dist/theme/components/nav/floating-branch-indicator.tsx +7 -7
  41. package/dist/theme/components/nav/header-icon.css +24 -0
  42. package/dist/theme/components/nav/header-icon.tsx +119 -0
  43. package/dist/theme/components/nav/header-logo.css +16 -0
  44. package/dist/theme/components/nav/header-logo.tsx +200 -0
  45. package/dist/theme/components/nav/layout.tsx +117 -16
  46. package/dist/theme/components/nav/nav-logo.tsx +16 -19
  47. package/dist/theme/components/openapi/index.ts +0 -2
  48. package/dist/theme/components/openapi/openapi.css +0 -14
  49. package/dist/theme/components/shared/card-icon.tsx +37 -0
  50. package/dist/theme/components/shared/ciderpress-logo.tsx +5 -1
  51. package/dist/theme/components/shared/icon.tsx +120 -35
  52. package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
  53. package/dist/theme/components/shared/section-card.tsx +37 -6
  54. package/dist/theme/components/sidebar/sidebar-badge.css +50 -0
  55. package/dist/theme/components/sidebar/sidebar-badge.tsx +153 -0
  56. package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
  57. package/dist/theme/components/sidebar/sidebar-scope.tsx +24 -1
  58. package/dist/theme/components/theme-provider.tsx +27 -11
  59. package/dist/theme/components/workspaces/card.css +4 -0
  60. package/dist/theme/components/workspaces/card.tsx +10 -12
  61. package/dist/theme/components/workspaces/grid.tsx +18 -6
  62. package/dist/theme/hooks/use-ciderpress.ts +65 -5
  63. package/dist/theme/hooks/use-nav-items.ts +82 -11
  64. package/dist/theme/index.tsx +10 -3
  65. package/dist/theme/lib/read-social-links.ts +4 -3
  66. package/dist/theme/lib/rich-text-parse.ts +740 -0
  67. package/dist/theme/lib/rich-text.test.tsx +342 -0
  68. package/dist/theme/lib/rich-text.tsx +86 -0
  69. package/dist/theme/lib/theme-favicon.ts +1 -1
  70. package/dist/theme/lib/with-mount-base.ts +31 -0
  71. package/dist/theme/styles/overrides/fonts.css +7 -4
  72. package/dist/theme/styles/overrides/footnotes.css +54 -0
  73. package/dist/theme/styles/overrides/home.css +11 -2
  74. package/dist/theme/styles/overrides/lists.css +83 -0
  75. package/dist/theme/styles/overrides/rail.css +27 -10
  76. package/dist/theme/styles/overrides/rspress.css +10 -5
  77. package/dist/theme/styles/overrides/section-card.css +9 -0
  78. package/dist/theme/styles/overrides/sidebar.css +22 -0
  79. package/dist/theme/styles/overrides/tokens.css +41 -23
  80. package/dist/theme/styles/rich-text.css +106 -0
  81. package/dist/theme/styles/themes/amber.css +32 -2
  82. package/dist/theme/styles/themes/arcade.css +16 -1
  83. package/dist/theme/styles/themes/grannysmith.css +32 -2
  84. package/dist/theme/styles/themes/honeycrisp.css +37 -7
  85. package/dist/theme/styles/themes/midnight.css +16 -1
  86. package/dist/theme/styles/themes/mulled.css +54 -9
  87. package/package.json +18 -16
  88. package/src/theme/components/footer/site-footer.tsx +68 -9
  89. package/src/theme/components/home/cta.css +15 -0
  90. package/src/theme/components/home/cta.tsx +10 -2
  91. package/src/theme/components/home/feature-card.css +1 -1
  92. package/src/theme/components/home/feature-card.tsx +8 -10
  93. package/src/theme/components/home/feature.tsx +54 -31
  94. package/src/theme/components/home/hero-demo.css +38 -0
  95. package/src/theme/components/home/home-visual.tsx +226 -0
  96. package/src/theme/components/home/layout.tsx +410 -108
  97. package/src/theme/components/home/split.css +46 -19
  98. package/src/theme/components/home/split.tsx +28 -8
  99. package/src/theme/components/home/tabs.css +299 -0
  100. package/src/theme/components/home/tabs.tsx +351 -0
  101. package/src/theme/components/home/trust-strip.css +41 -0
  102. package/src/theme/components/home/trust-strip.tsx +168 -11
  103. package/src/theme/components/home/workspaces.tsx +176 -38
  104. package/src/theme/components/nav/branch-tag.tsx +7 -7
  105. package/src/theme/components/nav/ciderpress-docs-bar.css +13 -1
  106. package/src/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
  107. package/src/theme/components/nav/ciderpress-header.css +2 -1
  108. package/src/theme/components/nav/ciderpress-header.tsx +26 -3
  109. package/src/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
  110. package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
  111. package/src/theme/components/nav/ciderpress-nav-menu.css +88 -0
  112. package/src/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
  113. package/src/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
  114. package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
  115. package/src/theme/components/nav/header-icon.css +24 -0
  116. package/src/theme/components/nav/header-icon.tsx +119 -0
  117. package/src/theme/components/nav/header-logo.css +16 -0
  118. package/src/theme/components/nav/header-logo.tsx +200 -0
  119. package/src/theme/components/nav/layout.tsx +117 -16
  120. package/src/theme/components/nav/nav-logo.tsx +16 -19
  121. package/src/theme/components/openapi/index.ts +0 -2
  122. package/src/theme/components/openapi/openapi.css +0 -14
  123. package/src/theme/components/shared/card-icon.tsx +37 -0
  124. package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
  125. package/src/theme/components/shared/icon.tsx +120 -35
  126. package/src/theme/components/shared/resolve-card-icon.ts +53 -12
  127. package/src/theme/components/shared/section-card.tsx +37 -6
  128. package/src/theme/components/sidebar/sidebar-badge.css +50 -0
  129. package/src/theme/components/sidebar/sidebar-badge.tsx +153 -0
  130. package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
  131. package/src/theme/components/sidebar/sidebar-scope.tsx +24 -1
  132. package/src/theme/components/theme-provider.tsx +27 -11
  133. package/src/theme/components/workspaces/card.css +4 -0
  134. package/src/theme/components/workspaces/card.tsx +10 -12
  135. package/src/theme/components/workspaces/grid.tsx +18 -6
  136. package/src/theme/hooks/use-ciderpress.ts +65 -5
  137. package/src/theme/hooks/use-nav-items.ts +82 -11
  138. package/src/theme/index.tsx +10 -3
  139. package/src/theme/lib/read-social-links.ts +4 -3
  140. package/src/theme/lib/rich-text-parse.ts +740 -0
  141. package/src/theme/lib/rich-text.test.tsx +342 -0
  142. package/src/theme/lib/rich-text.tsx +86 -0
  143. package/src/theme/lib/theme-favicon.ts +1 -1
  144. package/src/theme/lib/with-mount-base.ts +31 -0
  145. package/src/theme/styles/overrides/fonts.css +7 -4
  146. package/src/theme/styles/overrides/footnotes.css +54 -0
  147. package/src/theme/styles/overrides/home.css +11 -2
  148. package/src/theme/styles/overrides/lists.css +83 -0
  149. package/src/theme/styles/overrides/rail.css +27 -10
  150. package/src/theme/styles/overrides/rspress.css +10 -5
  151. package/src/theme/styles/overrides/section-card.css +9 -0
  152. package/src/theme/styles/overrides/sidebar.css +22 -0
  153. package/src/theme/styles/overrides/tokens.css +41 -23
  154. package/src/theme/styles/rich-text.css +106 -0
  155. package/src/theme/styles/themes/amber.css +32 -2
  156. package/src/theme/styles/themes/arcade.css +16 -1
  157. package/src/theme/styles/themes/grannysmith.css +32 -2
  158. package/src/theme/styles/themes/honeycrisp.css +37 -7
  159. package/src/theme/styles/themes/midnight.css +16 -1
  160. package/src/theme/styles/themes/mulled.css +54 -9
  161. package/dist/theme/components/openapi/copy-markdown-button.tsx +0 -41
  162. package/src/theme/components/openapi/copy-markdown-button.tsx +0 -41
@@ -1,33 +1,181 @@
1
- import type { HomeGridConfig } from '@ciderpress/config'
1
+ import type { TruncateConfig } from '@ciderpress/config'
2
2
  import { match, P } from 'massaman/match'
3
3
  import type React from 'react'
4
4
 
5
5
  import { useCiderpress } from '../../hooks/use-ciderpress'
6
- import type { WorkspaceGroupData } from '../../hooks/use-ciderpress'
6
+ import type { WorkspaceCardData, WorkspaceGroupData } from '../../hooks/use-ciderpress'
7
+ import { renderRichText } from '../../lib/rich-text.tsx'
7
8
  import { WorkspaceCard } from '../workspaces/card'
8
9
  import { WorkspaceGrid } from '../workspaces/grid'
9
10
 
10
11
  /**
11
- * Smart orchestrator that reads workspace data from themeConfig
12
- * and renders workspace groups with the correct card component per type.
12
+ * A single card compiled from `showcase.source` — the same shape as a
13
+ * workspace card, minus the grouping.
14
+ */
15
+ export type ShowcaseCard = WorkspaceCardData
16
+
17
+ interface FrontmatterWorkspacesHeading {
18
+ readonly label?: string
19
+ readonly title?: string
20
+ readonly body?: string
21
+ }
22
+
23
+ interface HomeWorkspacesProps {
24
+ readonly heading?: FrontmatterWorkspacesHeading
25
+ /**
26
+ * Cards compiled from an explicit `showcase.source` path list. When
27
+ * present they render as one ungrouped grid; otherwise the grouped
28
+ * workspace data from the theme config is used.
29
+ */
30
+ readonly cards?: readonly ShowcaseCard[]
31
+ readonly columns?: 1 | 2 | 3 | 4
32
+ readonly truncate?: TruncateConfig
33
+ }
34
+
35
+ const DEFAULT_COLUMNS: 1 | 2 | 3 | 4 = 2
36
+
37
+ /**
38
+ * Showcase grid block. Renders either the cards compiled from an
39
+ * explicit `showcase.source` path list (one ungrouped grid) or the
40
+ * grouped workspace data serialized from the repo at build time. Grid
41
+ * layout — heading, columns, truncation — comes from props.
13
42
  *
14
- * @returns React element with workspace groups or null
43
+ * @param props - Optional heading, cards, column count, and truncation limits.
44
+ * @returns React element with the showcase grid, or null when empty.
15
45
  */
16
- export function HomeWorkspaces(): React.ReactElement | null {
17
- const { workspaces, home } = useCiderpress()
18
- const gridConfig = home && home.workspaces
46
+ export function HomeWorkspaces(props: HomeWorkspacesProps): React.ReactElement | null {
47
+ const { workspaces } = useCiderpress()
48
+ const { heading, cards, columns, truncate } = props
49
+ const resolvedColumns = match(columns)
50
+ .with(P.nonNullable, (c) => c)
51
+ .otherwise(() => DEFAULT_COLUMNS)
19
52
 
20
- return match(workspaces)
53
+ // A `cards` key means the block declared an explicit `showcase.source`
54
+ // — an empty array is "that source resolved to nothing", not "fall back
55
+ // to the workspace deck". Only an absent key takes the fallback.
56
+ const body = match(cards)
21
57
  .with(
22
- P.when((w): w is readonly WorkspaceGroupData[] => Array.isArray(w) && w.length > 0),
23
- (groups) => (
24
- <div className="cp-workspace-section">
25
- <hr className="cp-divider" />
26
- {groups.map((group) => renderGroup(group, gridConfig))}
27
- </div>
28
- )
58
+ P.when((c): c is readonly ShowcaseCard[] => Array.isArray(c)),
59
+ (list) =>
60
+ match(list.length === 0)
61
+ .with(true, () => null)
62
+ .otherwise(() => [renderCardGrid(list, resolvedColumns, truncate)])
29
63
  )
30
- .otherwise(() => null)
64
+ .otherwise(() =>
65
+ match(workspaces)
66
+ .with(
67
+ P.when((w): w is readonly WorkspaceGroupData[] => Array.isArray(w) && w.length > 0),
68
+ (groups) => groups.map((group, i) => renderGroup(group, resolvedColumns, truncate, i))
69
+ )
70
+ .otherwise(() => null)
71
+ )
72
+
73
+ return match(body)
74
+ .with(null, () => null)
75
+ .otherwise((content) => (
76
+ <div className="cp-workspace-section">
77
+ {/* The rule separates this band from the heading above it, so a
78
+ block with no heading of its own would render it as an orphan
79
+ line floating over the cards. */}
80
+ {match(renderHeading(heading))
81
+ .with(P.nullish, () => null)
82
+ .otherwise((head) => (
83
+ <>
84
+ <hr className="cp-divider" />
85
+ {head}
86
+ </>
87
+ ))}
88
+ {content}
89
+ </div>
90
+ ))
91
+ }
92
+
93
+ /**
94
+ * Render the block heading, or nothing when every field is absent.
95
+ *
96
+ * @private
97
+ * @param heading - Flat heading trio from the block
98
+ * @returns Heading element, or null
99
+ */
100
+ function renderHeading(
101
+ heading: FrontmatterWorkspacesHeading | undefined
102
+ ): React.ReactElement | null {
103
+ if (heading === undefined) {
104
+ return null
105
+ }
106
+ const { label, title, body } = heading
107
+ const hasContent = [label, title, body].some((value) => typeof value === 'string')
108
+ if (!hasContent) {
109
+ return null
110
+ }
111
+ return (
112
+ <div className="cp-feature-section-head">
113
+ {match(label)
114
+ .with(P.string, (e) => (
115
+ <div className="cp-feature-section-head__eyebrow">{renderRichText(e)}</div>
116
+ ))
117
+ .otherwise(() => null)}
118
+ {match(title)
119
+ .with(P.string, (t) => (
120
+ <h2 className="cp-feature-section-head__title">{renderRichText(t)}</h2>
121
+ ))
122
+ .otherwise(() => null)}
123
+ {match(body)
124
+ .with(P.string, (s) => <p className="cp-feature-section-head__sub">{renderRichText(s)}</p>)
125
+ .otherwise(() => null)}
126
+ </div>
127
+ )
128
+ }
129
+
130
+ /**
131
+ * Render an ungrouped grid of cards compiled from `showcase.source`.
132
+ *
133
+ * @private
134
+ * @param cards - Resolved showcase cards
135
+ * @param columns - Column count for the grid
136
+ * @param truncate - Optional line-clamp limits for card text
137
+ * @returns Ungrouped workspace grid element
138
+ */
139
+ function renderCardGrid(
140
+ cards: readonly ShowcaseCard[],
141
+ columns: 1 | 2 | 3 | 4,
142
+ truncate: TruncateConfig | undefined
143
+ ): React.ReactElement {
144
+ return (
145
+ <WorkspaceGrid key="showcase-source" columns={columns}>
146
+ {cards.map((card, i) => renderCard(card, i, truncate))}
147
+ </WorkspaceGrid>
148
+ )
149
+ }
150
+
151
+ /**
152
+ * Render a single showcase / workspace card.
153
+ *
154
+ * @private
155
+ * @param card - Card data
156
+ * @param index - Array index for key generation
157
+ * @param truncate - Optional line-clamp limits for card text
158
+ * @returns Workspace card element
159
+ */
160
+ function renderCard(
161
+ card: WorkspaceCardData,
162
+ index: number,
163
+ truncate: TruncateConfig | undefined
164
+ ): React.ReactElement {
165
+ return (
166
+ <WorkspaceCard
167
+ key={`${card.title}-${index}`}
168
+ title={card.title}
169
+ href={card.href}
170
+ icon={card.icon}
171
+ scope={card.scope}
172
+ description={card.description}
173
+ tags={card.tags}
174
+ badge={card.badge}
175
+ titleLines={truncate && truncate.title}
176
+ descriptionLines={truncate && truncate.description}
177
+ />
178
+ )
31
179
  }
32
180
 
33
181
  /**
@@ -35,37 +183,27 @@ export function HomeWorkspaces(): React.ReactElement | null {
35
183
  *
36
184
  * @private
37
185
  * @param group - Workspace group data with heading, description, and cards
38
- * @param gridConfig - Optional grid layout config for columns and truncation
186
+ * @param columns - Column count for the grid
187
+ * @param truncate - Optional line-clamp limits for card text
188
+ * @param index - Position in the group list, to disambiguate the key
39
189
  * @returns Workspace grid element
40
190
  */
41
191
  function renderGroup(
42
192
  group: WorkspaceGroupData,
43
- gridConfig: HomeGridConfig | undefined
193
+ columns: 1 | 2 | 3 | 4,
194
+ truncate: TruncateConfig | undefined,
195
+ index: number
44
196
  ): React.ReactElement {
45
- const titleLines = gridConfig && gridConfig.truncate && gridConfig.truncate.title
46
- const descLines = gridConfig && gridConfig.truncate && gridConfig.truncate.description
47
-
48
197
  return (
198
+ // Heading alone is not unique — two configured workspace groups may
199
+ // share a title, which collided into one React key.
49
200
  <WorkspaceGrid
50
- key={group.heading}
201
+ key={`${group.heading}-${index}`}
51
202
  heading={group.heading}
52
203
  description={group.description}
53
- columns={gridConfig && gridConfig.columns}
204
+ columns={columns}
54
205
  >
55
- {group.cards.map((card, i) => (
56
- <WorkspaceCard
57
- key={`${card.title}-${i}`}
58
- title={card.title}
59
- href={card.href}
60
- icon={card.icon}
61
- scope={card.scope}
62
- description={card.description}
63
- tags={card.tags}
64
- badge={card.badge}
65
- titleLines={titleLines}
66
- descriptionLines={descLines}
67
- />
68
- ))}
206
+ {group.cards.map((card, i) => renderCard(card, i, truncate))}
69
207
  </WorkspaceGrid>
70
208
  )
71
209
  }
@@ -1,7 +1,7 @@
1
- import type { SiteConfig } from '@ciderpress/config'
2
1
  import { match, P } from 'massaman/match'
3
2
  import type React from 'react'
4
3
 
4
+ import type { CiderpressSiteBlock } from '../../hooks/use-ciderpress'
5
5
  import { useCiderpress } from '../../hooks/use-ciderpress'
6
6
 
7
7
  import './branch-tag.css'
@@ -12,7 +12,7 @@ declare const __CIDERPRESS_GIT_BRANCH__: string | undefined
12
12
  /**
13
13
  * Git branch tag — pill-shaped badge rendered via the `beforeNavMenu`
14
14
  * layout slot. Hidden when on default branches (`main` or `master`) or
15
- * when no `site.edit.repo` is configured. Uses the pixelarticons:git-branch
15
+ * when no `editLink.repo` is configured. Uses the pixelarticons:git-branch
16
16
  * icon.
17
17
  *
18
18
  * @returns React element, or `null` when on a default branch / unconfigured.
@@ -61,7 +61,7 @@ function resolveBranch(): string {
61
61
  }
62
62
 
63
63
  /**
64
- * Build a GitHub `/tree/<branch>` URL from the configured `site.edit.repo`.
64
+ * Build a GitHub `/tree/<branch>` URL from the configured `editLink.repo`.
65
65
  *
66
66
  * Accepts either a full URL (used as-is) or an `org/repo` slug
67
67
  * (prefixed with `https://github.com/`). Returns `null` when `repo` is
@@ -83,14 +83,14 @@ function buildBranchHref(params: {
83
83
  }
84
84
 
85
85
  /**
86
- * Pull the configured edit-repo slug off the site config, expressed
87
- * with explicit null checks rather than optional chaining.
86
+ * Pull the configured edit-repo slug off the serialised site block,
87
+ * expressed with explicit null checks rather than optional chaining.
88
88
  *
89
89
  * @private
90
- * @param site - The ciderpress site config (may be undefined).
90
+ * @param site - The runtime site block (may be undefined).
91
91
  * @returns The configured `edit.repo` value or `undefined`.
92
92
  */
93
- function resolveRepo(site: SiteConfig | undefined): string | undefined {
93
+ function resolveRepo(site: CiderpressSiteBlock | undefined): string | undefined {
94
94
  if (site === undefined) {
95
95
  return undefined
96
96
  }
@@ -90,11 +90,21 @@
90
90
  display: flex;
91
91
  align-items: center;
92
92
  gap: 6px;
93
- flex: 1 1 auto;
93
+ /* Shrink-to-content (not grow) so page badges sit snug to the right of
94
+ the trail; long trails still truncate via `overflow: hidden`. */
95
+ flex: 0 1 auto;
94
96
  min-width: 0;
95
97
  overflow: hidden;
96
98
  }
97
99
 
100
+ /* Page badges (ALPHA / WIP …) immediately right of the breadcrumb trail. */
101
+ .cp-docs-bar__badges {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ gap: 4px;
105
+ flex: 0 0 auto;
106
+ }
107
+
98
108
  .cp-docs-bar__crumb-wrap {
99
109
  display: inline-flex;
100
110
  align-items: center;
@@ -358,6 +368,8 @@ html[data-cp-outline-collapsed='true'] .cp-docs-bar__chev {
358
368
  `styles/overrides/rspress.css`. ──────────────────────────────── */
359
369
  .cp-docs-bar__llms {
360
370
  flex-shrink: 0;
371
+ /* Crumbs no longer grow, so push the LLMs cluster to the far right. */
372
+ margin-left: auto;
361
373
  }
362
374
 
363
375
  .cp-docs-bar__llms .rp-llms-container {
@@ -1,3 +1,4 @@
1
+ import type { BadgeConfig } from '@ciderpress/config'
1
2
  import { useFrontmatter, useLocation } from '@rspress/core/runtime'
2
3
  import {
3
4
  LlmsContainer,
@@ -13,8 +14,10 @@ import { match, P } from 'massaman/match'
13
14
  import { useCallback, useEffect, useRef, useState } from 'react'
14
15
  import type React from 'react'
15
16
 
17
+ import { useCiderpress } from '../../hooks/use-ciderpress'
16
18
  import { RouteLink } from '../../lib/route-link.tsx'
17
19
  import { Icon } from '../shared/icon.tsx'
20
+ import { BadgeChips } from '../sidebar/sidebar-badge'
18
21
  import { useCiderpressLayout } from './ciderpress-layout-context'
19
22
 
20
23
  import './ciderpress-docs-bar.css'
@@ -117,6 +120,9 @@ export function CiderpressDocsBar(): React.ReactElement | null {
117
120
 
118
121
  const pageTitle = readStringField(fmRecord.title)
119
122
 
123
+ const { pageBadges } = useCiderpress()
124
+ const badges = lookupPageBadges({ pageBadges, pathname })
125
+
120
126
  const breadcrumbs = buildBreadcrumbs(pathname, pageTitle)
121
127
  const outlineEnabled = match(fmRecord.outline)
122
128
  .with(false, () => false)
@@ -174,12 +180,12 @@ export function CiderpressDocsBar(): React.ReactElement | null {
174
180
  <span className="cp-docs-bar__sep">›</span>
175
181
  {match(crumb.current)
176
182
  .with(true, () => (
177
- <span className="cp-docs-bar__crumb" aria-current="page">
183
+ <span className="cp-docs-bar__crumb" aria-current="page" title={crumb.label}>
178
184
  {crumb.label}
179
185
  </span>
180
186
  ))
181
187
  .otherwise(() => (
182
- <RouteLink href={crumb.href} className="cp-docs-bar__crumb">
188
+ <RouteLink href={crumb.href} className="cp-docs-bar__crumb" title={crumb.label}>
183
189
  {crumb.label}
184
190
  </RouteLink>
185
191
  ))}
@@ -187,6 +193,11 @@ export function CiderpressDocsBar(): React.ReactElement | null {
187
193
  ))}
188
194
  </nav>
189
195
  )}
196
+ {badges.length > 0 && (
197
+ <span className="cp-docs-bar__badges">
198
+ <BadgeChips badges={badges} />
199
+ </span>
200
+ )}
190
201
  <div className="cp-docs-bar__llms">
191
202
  <LlmsContainer>
192
203
  <LlmsCopyButton />
@@ -254,6 +265,26 @@ interface Crumb {
254
265
  readonly current: boolean
255
266
  }
256
267
 
268
+ /**
269
+ * Look up the current page's badges from the route→badges map, tolerating
270
+ * a trailing slash on the pathname (map keys are stored without one).
271
+ *
272
+ * @private
273
+ * @param params - The route→badges map (if present) and current pathname.
274
+ * @returns The page's badges, or an empty array when none apply.
275
+ */
276
+ function lookupPageBadges(params: {
277
+ readonly pageBadges: Record<string, readonly BadgeConfig[]> | undefined
278
+ readonly pathname: string
279
+ }): readonly BadgeConfig[] {
280
+ const { pageBadges, pathname } = params
281
+ if (pageBadges === undefined) {
282
+ return []
283
+ }
284
+ const stripped = pathname.replace(/\/$/, '')
285
+ return pageBadges[pathname] ?? pageBadges[stripped] ?? []
286
+ }
287
+
257
288
  /**
258
289
  * Build a 1- or 2-level breadcrumb trail from the current path and the
259
290
  * page's frontmatter title. The section label comes from titlecasing
@@ -76,7 +76,8 @@
76
76
  text-decoration: none;
77
77
  }
78
78
 
79
- .cp-header-logo > svg {
79
+ .cp-header-logo > svg,
80
+ .cp-header-logo > img {
80
81
  height: var(--cp-header-logo-height, 28px);
81
82
  width: auto;
82
83
  }
@@ -2,19 +2,29 @@ import { clsx } from 'clsx'
2
2
  import type React from 'react'
3
3
 
4
4
  import { RouteLink } from '../../lib/route-link.tsx'
5
- import { CiderpressLogo } from '../shared/ciderpress-logo'
6
5
  import { CiderpressNavHamburger } from './ciderpress-nav-hamburger'
7
6
  import { CiderpressNavMenu } from './ciderpress-nav-menu'
8
7
  import type { CiderpressNavMenuItem } from './ciderpress-nav-menu'
9
8
  import { CiderpressNavSearch } from './ciderpress-nav-search'
10
9
  import { CiderpressNavSocialLinks } from './ciderpress-nav-social-links'
11
10
  import type { CiderpressSocialLink } from './ciderpress-nav-social-links'
11
+ import { HeaderIcon } from './header-icon'
12
+ import { HeaderLogo } from './header-logo'
12
13
  import { NavDivider } from './nav-divider'
13
14
  import { TopbarCTA } from './topbar-cta'
14
15
  import { VariantToggle } from './variant-toggle'
15
16
 
16
17
  import './ciderpress-header.css'
17
18
 
19
+ /**
20
+ * Build-time flag: `true` when the active theme declares more than one
21
+ * variant AND `data-cp-variants` won't collapse the `<VariantToggle />`
22
+ * to `display: none`. Drives whether the trailing-cluster divider is
23
+ * rendered — when the toggle is hidden and neither socials nor CTA
24
+ * follow, the divider has nothing to separate and is omitted.
25
+ */
26
+ declare const __CIDERPRESS_HAS_VARIANT_TOGGLE__: boolean
27
+
18
28
  /**
19
29
  * Props for the ciderpress site header.
20
30
  */
@@ -57,6 +67,18 @@ export interface CiderpressHeaderProps {
57
67
  * @returns Sticky header element
58
68
  */
59
69
  export function CiderpressHeader(props: CiderpressHeaderProps): React.ReactElement {
70
+ // Dividers only render when they actually separate two visible
71
+ // clusters — orphan dividers (nothing on the right) are visually
72
+ // noisy. `<VariantToggle />` is hidden by CSS for single-variant
73
+ // themes; the build-time flag captures that decision so the React
74
+ // tree matches the painted output. The search/menu inner divider is
75
+ // unconditional — both clusters always render some chrome, and
76
+ // gating on `navItems.length` would flip after hydration (Rspress's
77
+ // nav data isn't populated at SSG) and produce a hydration warning.
78
+ const hasSocials = props.socialLinks.length > 0
79
+ const hasCta = props.topbarCta !== undefined
80
+ const hasTrailingCluster = __CIDERPRESS_HAS_VARIANT_TOGGLE__ || hasSocials || hasCta
81
+
60
82
  // Landing / home pages live inside a constrained max-width container;
61
83
  // doc pages stretch full-width (sidebar + article). The header mirrors
62
84
  // the content shell so it never floats outside the page rhythm.
@@ -78,7 +100,8 @@ export function CiderpressHeader(props: CiderpressHeaderProps): React.ReactEleme
78
100
  topbarCta={props.topbarCta}
79
101
  />
80
102
  <RouteLink href="/" className="cp-header-logo" aria-label="Home">
81
- <CiderpressLogo />
103
+ <HeaderIcon />
104
+ <HeaderLogo />
82
105
  </RouteLink>
83
106
 
84
107
  {/* Right cluster wrapper. Grows from a zero basis so its width
@@ -96,7 +119,7 @@ export function CiderpressHeader(props: CiderpressHeaderProps): React.ReactEleme
96
119
  <CiderpressNavMenu items={props.navItems} />
97
120
  </div>
98
121
 
99
- <NavDivider />
122
+ {hasTrailingCluster && <NavDivider />}
100
123
  <VariantToggle />
101
124
  <CiderpressNavSocialLinks links={props.socialLinks} />
102
125
  {props.topbarCta !== undefined && (
@@ -141,6 +141,56 @@
141
141
  text-decoration: none;
142
142
  }
143
143
 
144
+ .cp-nav-drawer__group {
145
+ display: flex;
146
+ flex-direction: column;
147
+ }
148
+
149
+ .cp-nav-drawer__group-toggle {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: space-between;
153
+ width: 100%;
154
+ padding: 12px 16px;
155
+ background: transparent;
156
+ border: none;
157
+ border-radius: 8px;
158
+ color: var(--cp-c-text-1);
159
+ font-family: inherit;
160
+ font-size: 15px;
161
+ font-weight: 500;
162
+ text-align: left;
163
+ cursor: pointer;
164
+ transition:
165
+ background 0.12s ease,
166
+ color 0.12s ease;
167
+ }
168
+
169
+ .cp-nav-drawer__group-toggle:hover {
170
+ background: var(--cp-c-bg-soft);
171
+ color: var(--cp-c-brand-1);
172
+ }
173
+
174
+ .cp-nav-drawer__group-chevron {
175
+ flex-shrink: 0;
176
+ color: var(--cp-c-text-2);
177
+ transition: transform 0.18s ease;
178
+ }
179
+
180
+ .cp-nav-drawer__group-toggle[aria-expanded='true'] .cp-nav-drawer__group-chevron {
181
+ transform: rotate(180deg);
182
+ }
183
+
184
+ .cp-nav-drawer__group-items {
185
+ display: flex;
186
+ flex-direction: column;
187
+ }
188
+
189
+ .cp-nav-drawer__link--nested {
190
+ padding-left: 28px;
191
+ font-size: 14px;
192
+ }
193
+
144
194
  .cp-nav-drawer__divider {
145
195
  height: 1px;
146
196
  margin: 4px 20px;
@@ -1,3 +1,4 @@
1
+ import { match } from 'massaman/match'
1
2
  import { useState } from 'react'
2
3
  import type React from 'react'
3
4
  import { Dialog, Modal, ModalOverlay } from 'react-aria-components'
@@ -71,15 +72,12 @@ export function CiderpressNavHamburger(props: CiderpressNavHamburgerProps): Reac
71
72
  </header>
72
73
 
73
74
  <nav className="cp-nav-drawer__nav" aria-label="Primary">
74
- {props.navItems.map((item) => (
75
- <RouteLink
76
- key={item.link}
77
- href={item.link}
78
- className="cp-nav-drawer__link"
79
- onClick={() => setOpen(false)}
80
- >
81
- {item.text}
82
- </RouteLink>
75
+ {props.navItems.map((item, index) => (
76
+ <DrawerNavEntry
77
+ key={`${drawerItemKey(item)}::${index}`}
78
+ item={item}
79
+ onNavigate={() => setOpen(false)}
80
+ />
83
81
  ))}
84
82
  </nav>
85
83
 
@@ -110,3 +108,91 @@ export function CiderpressNavHamburger(props: CiderpressNavHamburgerProps): Reac
110
108
  }
111
109
 
112
110
  export { CiderpressNavHamburger as default }
111
+
112
+ /**
113
+ * Render one entry in the mobile drawer nav. Leaf items are a single
114
+ * link; dropdown parents become a collapsible {@link DrawerNavGroup}.
115
+ *
116
+ * @private
117
+ * @param props - The nav item and a callback to close the drawer.
118
+ * @returns The drawer entry element.
119
+ */
120
+ function DrawerNavEntry(props: {
121
+ readonly item: CiderpressNavMenuItem
122
+ readonly onNavigate: () => void
123
+ }): React.ReactElement {
124
+ const { item, onNavigate } = props
125
+ const children = item.items ?? []
126
+ return match(children.length > 0)
127
+ .with(true, () => <DrawerNavGroup item={item} onNavigate={onNavigate} />)
128
+ .otherwise(() => (
129
+ <RouteLink href={item.link ?? '#'} className="cp-nav-drawer__link" onClick={onNavigate}>
130
+ {item.text}
131
+ </RouteLink>
132
+ ))
133
+ }
134
+
135
+ /**
136
+ * A collapsible dropdown section in the mobile drawer: a tappable header
137
+ * that toggles its child links open/closed. Collapsed by default so a
138
+ * deep nav doesn't fill the drawer on open.
139
+ *
140
+ * @private
141
+ * @param props - The dropdown item and a callback to close the drawer.
142
+ * @returns The collapsible group element.
143
+ */
144
+ function DrawerNavGroup(props: {
145
+ readonly item: CiderpressNavMenuItem
146
+ readonly onNavigate: () => void
147
+ }): React.ReactElement {
148
+ const { item, onNavigate } = props
149
+ const [expanded, setExpanded] = useState(false)
150
+ const children = item.items ?? []
151
+ return (
152
+ <div className="cp-nav-drawer__group" role="group" aria-label={item.text}>
153
+ <button
154
+ type="button"
155
+ className="cp-nav-drawer__group-toggle"
156
+ onClick={() => setExpanded((current) => !current)}
157
+ aria-expanded={expanded}
158
+ >
159
+ <span>{item.text}</span>
160
+ <Icon
161
+ className="cp-nav-drawer__group-chevron"
162
+ icon="pixelarticons:chevron-down"
163
+ width={16}
164
+ height={16}
165
+ />
166
+ </button>
167
+ {expanded && (
168
+ <div className="cp-nav-drawer__group-items">
169
+ {children.map((child, index) => (
170
+ <RouteLink
171
+ key={`${drawerItemKey(child)}::${index}`}
172
+ href={child.link ?? '#'}
173
+ className="cp-nav-drawer__link cp-nav-drawer__link--nested"
174
+ onClick={onNavigate}
175
+ >
176
+ {child.text}
177
+ </RouteLink>
178
+ ))}
179
+ </div>
180
+ )}
181
+ </div>
182
+ )
183
+ }
184
+
185
+ /**
186
+ * Stable React key for a drawer nav item — its link when present,
187
+ * otherwise its label (dropdown parents may have no link of their own).
188
+ *
189
+ * @private
190
+ * @param item - Nav item to key.
191
+ * @returns Key string.
192
+ */
193
+ function drawerItemKey(item: CiderpressNavMenuItem): string {
194
+ if (item.link !== undefined && item.link !== '') {
195
+ return item.link
196
+ }
197
+ return item.text
198
+ }