@ciderpress/ui 1.0.0-rc.6 → 1.0.0-rc.7

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 (41) hide show
  1. package/dist/node.mjs +20 -2
  2. package/dist/plugins/mermaid/MermaidRenderer.tsx +24 -6
  3. package/dist/theme/components/nav/ciderpress-docs-bar.css +13 -1
  4. package/dist/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
  5. package/dist/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
  6. package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
  7. package/dist/theme/components/nav/ciderpress-nav-menu.css +88 -0
  8. package/dist/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
  9. package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +18 -15
  10. package/dist/theme/components/nav/layout.tsx +72 -9
  11. package/dist/theme/components/shared/icon.tsx +2 -1
  12. package/dist/theme/components/shared/section-card.tsx +28 -0
  13. package/dist/theme/components/sidebar/sidebar-badge.css +50 -0
  14. package/dist/theme/components/sidebar/sidebar-badge.tsx +153 -0
  15. package/dist/theme/components/sidebar/sidebar-scope.tsx +24 -1
  16. package/dist/theme/hooks/use-ciderpress.ts +6 -1
  17. package/dist/theme/hooks/use-nav-items.ts +82 -11
  18. package/dist/theme/index.tsx +3 -0
  19. package/dist/theme/styles/overrides/rail.css +27 -10
  20. package/dist/theme/styles/overrides/section-card.css +9 -0
  21. package/dist/theme/styles/overrides/sidebar.css +22 -0
  22. package/package.json +13 -12
  23. package/src/theme/components/nav/ciderpress-docs-bar.css +13 -1
  24. package/src/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
  25. package/src/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
  26. package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
  27. package/src/theme/components/nav/ciderpress-nav-menu.css +88 -0
  28. package/src/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
  29. package/src/theme/components/nav/ciderpress-nav-social-links.tsx +18 -15
  30. package/src/theme/components/nav/layout.tsx +72 -9
  31. package/src/theme/components/shared/icon.tsx +2 -1
  32. package/src/theme/components/shared/section-card.tsx +28 -0
  33. package/src/theme/components/sidebar/sidebar-badge.css +50 -0
  34. package/src/theme/components/sidebar/sidebar-badge.tsx +153 -0
  35. package/src/theme/components/sidebar/sidebar-scope.tsx +24 -1
  36. package/src/theme/hooks/use-ciderpress.ts +6 -1
  37. package/src/theme/hooks/use-nav-items.ts +82 -11
  38. package/src/theme/index.tsx +3 -0
  39. package/src/theme/styles/overrides/rail.css +27 -10
  40. package/src/theme/styles/overrides/section-card.css +9 -0
  41. package/src/theme/styles/overrides/sidebar.css +22 -0
@@ -1,6 +1,9 @@
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
8
  import { CardIcon } from './card-icon'
6
9
  import type { CardIconInput } from './resolve-card-icon'
@@ -26,6 +29,8 @@ export function SectionCard({
26
29
  description,
27
30
  icon = 'pixelarticons:file',
28
31
  }: SectionCardProps): React.ReactElement {
32
+ const { pageBadges } = useCiderpress()
33
+ const badges = lookupBadges({ pageBadges, href })
29
34
  const resolved = resolveCardIcon(icon) ?? {
30
35
  kind: 'iconify' as const,
31
36
  id: 'pixelarticons:file',
@@ -40,8 +45,31 @@ export function SectionCard({
40
45
  <div className="cp-section-card__header">
41
46
  <CardIcon resolved={resolved} className="cp-section-card__icon" />
42
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
+ )}
43
53
  </div>
44
54
  {descEl}
45
55
  </Card>
46
56
  )
47
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
+ }
@@ -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
  *
@@ -1,4 +1,4 @@
1
- import type { FooterConfig, HomeConfig, SerializedIcon } 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 {
@@ -86,6 +86,10 @@ export interface CiderpressSiteBlock {
86
86
  readonly brandMark?: string
87
87
  }
88
88
  | undefined
89
+ readonly feedback: {
90
+ readonly enabled: boolean
91
+ readonly question: string | undefined
92
+ }
89
93
  }
90
94
 
91
95
  interface CiderpressThemeConfig {
@@ -97,6 +101,7 @@ interface CiderpressThemeConfig {
97
101
  readonly home: HomeConfig | undefined
98
102
  readonly ciderpressFooter: FooterConfig | undefined
99
103
  readonly site: CiderpressSiteBlock | undefined
104
+ readonly pageBadges: Record<string, readonly BadgeConfig[]> | undefined
100
105
  }
101
106
 
102
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
  }
@@ -53,6 +53,7 @@ import './components/content-footer/feedback.css'
53
53
  import './components/content-footer/meta-actions.css'
54
54
  import './components/content-footer/page-pager.css'
55
55
  import './components/sidebar/framework-picker.css'
56
+ import './components/sidebar/sidebar-badge.css'
56
57
  import './components/sidebar/sidebar-promo.css'
57
58
  import './components/sidebar/sidebar-toggle.css'
58
59
  import './components/home/page-rail.css'
@@ -89,6 +90,8 @@ export { HomeFeature } from './components/home/feature'
89
90
  export { HomeLayout } from './components/home/layout'
90
91
  /** @internal Sidebar override — multi-scope filtering for standalone sections */
91
92
  export { Sidebar } from './components/sidebar/sidebar-scope'
93
+ /** @internal Tag override — renders ciderpress sidebar badges, delegates other tags to Rspress */
94
+ export { Tag } from './components/sidebar/sidebar-badge'
92
95
 
93
96
  export { FeatureCard, FeatureGrid } from './components/home/feature-card'
94
97
  export type { FeatureCardProps, FeatureItem } from './components/home/feature-card'
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciderpress/ui",
3
- "version": "1.0.0-rc.6",
3
+ "version": "1.0.0-rc.7",
4
4
  "description": "Rspress plugin, theme components, and styles for ciderpress",
5
5
  "keywords": [
6
6
  "ciderpress",
@@ -48,28 +48,29 @@
48
48
  "@iconify-json/catppuccin": "^1.2.17",
49
49
  "@iconify-json/devicon": "^1.2.62",
50
50
  "@iconify-json/logos": "^1.2.11",
51
- "@iconify-json/material-icon-theme": "^1.2.68",
51
+ "@iconify-json/material-icon-theme": "^1.2.69",
52
52
  "@iconify-json/mdi": "^1.2.3",
53
- "@iconify-json/pixelarticons": "^1.2.6",
54
- "@iconify-json/simple-icons": "^1.2.87",
53
+ "@iconify-json/pixel": "^1.2.2",
54
+ "@iconify-json/pixelarticons": "^1.2.7",
55
+ "@iconify-json/simple-icons": "^1.2.89",
55
56
  "@iconify-json/skill-icons": "^1.2.4",
56
- "@iconify-json/vscode-icons": "^1.2.62",
57
+ "@iconify-json/vscode-icons": "^1.2.65",
57
58
  "@iconify/react": "^6.0.2",
58
59
  "clsx": "^2.1.1",
59
60
  "katex": "^0.17.0",
60
61
  "massaman": "^0.3.0",
61
- "mermaid": "^10.9.6",
62
+ "mermaid": "^11.16.0",
62
63
  "openapi-sampler": "^1.7.4",
63
64
  "react-aria-components": "^1.19.0",
64
65
  "ts-morph": "^28.0.0",
65
66
  "unist-util-visit": "^5.1.0",
66
- "@ciderpress/config": "1.0.0-rc.5",
67
- "@ciderpress/theme": "1.0.0-rc.3"
67
+ "@ciderpress/config": "1.0.0-rc.6",
68
+ "@ciderpress/theme": "1.0.0-rc.4"
68
69
  },
69
70
  "devDependencies": {
70
71
  "@ladle/react": "^5.1.1",
71
- "@rslib/core": "^0.23.1",
72
- "@rspress/core": "^2.0.15",
72
+ "@rslib/core": "0.23.1",
73
+ "@rspress/core": "^2.0.16",
73
74
  "@types/react": "^19.2.17",
74
75
  "@types/react-dom": "^19.2.3",
75
76
  "esbuild": "^0.28.1",
@@ -81,10 +82,10 @@
81
82
  "rspress-plugin-katex": "^1.0.1",
82
83
  "rspress-plugin-supersub": "^1.0.0",
83
84
  "typescript": "^6.0.3",
84
- "vitest": "^4.1.9"
85
+ "vitest": "^4.1.10"
85
86
  },
86
87
  "peerDependencies": {
87
- "@rspress/core": "^2.0.15",
88
+ "@rspress/core": "^2.0.16",
88
89
  "react": "^19.2.5",
89
90
  "react-dom": "^19.2.5"
90
91
  },
@@ -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 {