@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,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
@@ -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
+ }
@@ -50,6 +50,13 @@
50
50
  color: var(--cp-nav-menu-item-color-active, var(--cp-c-brand-1, currentColor));
51
51
  }
52
52
 
53
+ /* Measurement-only: dropdown items render a label→chevron gap in the
54
+ live toggle, so the hidden measure span must include it to size
55
+ correctly. Applied only in the measure row. */
56
+ .cp-nav-menu__item--measured-dropdown {
57
+ gap: 4px;
58
+ }
59
+
53
60
  /* ── Hidden measurement layer ──────────────────────────────────────────
54
61
  Renders every item at full natural width so the JS layer always has
55
62
  a stable per-item measurement no matter how narrow the live menu
@@ -128,3 +135,84 @@
128
135
  .cp-nav-menu__overflow-item--active {
129
136
  color: var(--cp-nav-menu-item-color-active, var(--cp-c-brand-1, currentColor));
130
137
  }
138
+
139
+ /* ── Inline dropdown (nav item with children) ──────────────────────── */
140
+ .cp-nav-menu__dropdown {
141
+ position: relative;
142
+ display: inline-flex;
143
+ align-items: center;
144
+ }
145
+
146
+ .cp-nav-menu__dropdown-toggle {
147
+ gap: 4px;
148
+ background: transparent;
149
+ border: none;
150
+ cursor: pointer;
151
+ font-family: inherit;
152
+ }
153
+
154
+ .cp-nav-menu__dropdown-popover {
155
+ position: absolute;
156
+ /* Sit below the toggle with breathing room. The visual gap is bridged
157
+ by the transparent `::before` band below, so the whole toggle→menu
158
+ path stays inside the hover region and moving toward the menu never
159
+ crosses empty space that would trigger a close. */
160
+ top: calc(100% + 12px);
161
+ /* Anchor to the toggle's right edge. The primary nav is right-aligned
162
+ in the topbar, so opening leftward keeps the popover on-screen
163
+ instead of overflowing the viewport's right edge. */
164
+ right: 0;
165
+ z-index: 60;
166
+ min-width: 200px;
167
+ margin: 0;
168
+ padding: 6px;
169
+ list-style: none;
170
+ background: var(--cp-nav-menu-overflow-bg, var(--cp-c-bg, #1e1e2e));
171
+ border: 1px solid var(--cp-nav-menu-overflow-border, var(--cp-c-border, #313244));
172
+ border-radius: 8px;
173
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.24);
174
+ }
175
+
176
+ /* Invisible bridge: a transparent band spanning the visual gap between
177
+ the toggle and the popover surface (and a little to each side), so the
178
+ pointer stays within the dropdown's hover region the whole way down.
179
+ Without it, the 6px gap is dead space and mouse-leave fires mid-move. */
180
+ .cp-nav-menu__dropdown-popover::before {
181
+ content: '';
182
+ position: absolute;
183
+ bottom: 100%;
184
+ left: -12px;
185
+ right: -12px;
186
+ /* Taller than the 12px offset so the band overlaps both the toggle and
187
+ the popover surface — no hairline seam for the pointer to slip through. */
188
+ height: 16px;
189
+ }
190
+
191
+ /* ── Dropdown parents collapsed into the "More" popover ────────────── */
192
+ .cp-nav-menu__overflow-group {
193
+ padding: 4px 0;
194
+ }
195
+
196
+ .cp-nav-menu__overflow-group + .cp-nav-menu__overflow-group,
197
+ .cp-nav-menu__overflow-item + .cp-nav-menu__overflow-group {
198
+ margin-top: 4px;
199
+ border-top: 1px solid var(--cp-nav-menu-overflow-border, var(--cp-c-border, #313244));
200
+ }
201
+
202
+ .cp-nav-menu__overflow-group-label {
203
+ display: block;
204
+ padding: 6px 12px 2px;
205
+ /* text-2 (not text-3): these small uppercase labels need AA contrast,
206
+ and text-3's low alpha fails 4.5:1 on the light themes. */
207
+ color: var(--cp-c-text-2, #9399b2);
208
+ font-size: 11px;
209
+ font-weight: 600;
210
+ letter-spacing: 0.04em;
211
+ text-transform: uppercase;
212
+ }
213
+
214
+ .cp-nav-menu__overflow-sublist {
215
+ margin: 0;
216
+ padding: 0;
217
+ list-style: none;
218
+ }