@ciderpress/ui 1.0.0-rc.1 → 1.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/node.d.ts +1 -1
- package/dist/node.mjs +335 -51
- package/dist/plugins/mermaid/MermaidRenderer.tsx +49 -6
- package/dist/theme/components/footer/site-footer.tsx +68 -9
- package/dist/theme/components/home/feature-card.tsx +5 -8
- package/dist/theme/components/home/feature.tsx +31 -11
- package/dist/theme/components/home/hero-demo-custom.tsx +116 -0
- package/dist/theme/components/home/hero-demo.css +27 -0
- package/dist/theme/components/home/layout.tsx +149 -26
- package/dist/theme/components/home/split-visual-custom.tsx +26 -0
- package/dist/theme/components/home/workspaces.tsx +39 -3
- package/dist/theme/components/nav/branch-tag.tsx +7 -7
- package/dist/theme/components/nav/ciderpress-docs-bar.css +13 -1
- package/dist/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
- package/dist/theme/components/nav/ciderpress-header.css +2 -1
- package/dist/theme/components/nav/ciderpress-header.tsx +26 -3
- package/dist/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
- package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
- package/dist/theme/components/nav/ciderpress-nav-menu.css +88 -0
- package/dist/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
- package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
- package/dist/theme/components/nav/floating-branch-indicator.tsx +7 -7
- package/dist/theme/components/nav/header-icon.css +24 -0
- package/dist/theme/components/nav/header-icon.tsx +119 -0
- package/dist/theme/components/nav/header-logo.css +16 -0
- package/dist/theme/components/nav/header-logo.tsx +200 -0
- package/dist/theme/components/nav/layout.tsx +117 -16
- package/dist/theme/components/nav/nav-logo.tsx +16 -19
- package/dist/theme/components/openapi/index.ts +0 -2
- package/dist/theme/components/openapi/openapi.css +0 -14
- package/dist/theme/components/shared/card-icon.tsx +37 -0
- package/dist/theme/components/shared/ciderpress-logo.tsx +5 -1
- package/dist/theme/components/shared/icon.tsx +120 -35
- package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
- package/dist/theme/components/shared/section-card.tsx +37 -6
- package/dist/theme/components/sidebar/sidebar-badge.css +50 -0
- package/dist/theme/components/sidebar/sidebar-badge.tsx +153 -0
- package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/dist/theme/components/sidebar/sidebar-scope.tsx +24 -1
- package/dist/theme/components/theme-provider.tsx +27 -11
- package/dist/theme/components/workspaces/card.tsx +7 -9
- package/dist/theme/hooks/use-ciderpress.ts +65 -5
- package/dist/theme/hooks/use-nav-items.ts +82 -11
- package/dist/theme/index.tsx +5 -3
- package/dist/theme/lib/read-social-links.ts +4 -3
- package/dist/theme/lib/theme-favicon.ts +1 -1
- package/dist/theme/lib/with-mount-base.ts +31 -0
- package/dist/theme/styles/overrides/footnotes.css +54 -0
- package/dist/theme/styles/overrides/lists.css +83 -0
- package/dist/theme/styles/overrides/rail.css +27 -10
- package/dist/theme/styles/overrides/section-card.css +9 -0
- package/dist/theme/styles/overrides/sidebar.css +22 -0
- package/package.json +18 -16
- package/src/theme/components/footer/site-footer.tsx +68 -9
- package/src/theme/components/home/feature-card.tsx +5 -8
- package/src/theme/components/home/feature.tsx +31 -11
- package/src/theme/components/home/hero-demo-custom.tsx +116 -0
- package/src/theme/components/home/hero-demo.css +27 -0
- package/src/theme/components/home/layout.tsx +149 -26
- package/src/theme/components/home/split-visual-custom.tsx +26 -0
- package/src/theme/components/home/workspaces.tsx +39 -3
- package/src/theme/components/nav/branch-tag.tsx +7 -7
- package/src/theme/components/nav/ciderpress-docs-bar.css +13 -1
- package/src/theme/components/nav/ciderpress-docs-bar.tsx +33 -2
- package/src/theme/components/nav/ciderpress-header.css +2 -1
- package/src/theme/components/nav/ciderpress-header.tsx +26 -3
- package/src/theme/components/nav/ciderpress-nav-hamburger.css +50 -0
- package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +95 -9
- package/src/theme/components/nav/ciderpress-nav-menu.css +88 -0
- package/src/theme/components/nav/ciderpress-nav-menu.tsx +397 -38
- package/src/theme/components/nav/ciderpress-nav-social-links.tsx +27 -22
- package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
- package/src/theme/components/nav/header-icon.css +24 -0
- package/src/theme/components/nav/header-icon.tsx +119 -0
- package/src/theme/components/nav/header-logo.css +16 -0
- package/src/theme/components/nav/header-logo.tsx +200 -0
- package/src/theme/components/nav/layout.tsx +117 -16
- package/src/theme/components/nav/nav-logo.tsx +16 -19
- package/src/theme/components/openapi/index.ts +0 -2
- package/src/theme/components/openapi/openapi.css +0 -14
- package/src/theme/components/shared/card-icon.tsx +37 -0
- package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
- package/src/theme/components/shared/icon.tsx +120 -35
- package/src/theme/components/shared/resolve-card-icon.ts +53 -12
- package/src/theme/components/shared/section-card.tsx +37 -6
- package/src/theme/components/sidebar/sidebar-badge.css +50 -0
- package/src/theme/components/sidebar/sidebar-badge.tsx +153 -0
- package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
- package/src/theme/components/sidebar/sidebar-scope.tsx +24 -1
- package/src/theme/components/theme-provider.tsx +27 -11
- package/src/theme/components/workspaces/card.tsx +7 -9
- package/src/theme/hooks/use-ciderpress.ts +65 -5
- package/src/theme/hooks/use-nav-items.ts +82 -11
- package/src/theme/index.tsx +5 -3
- package/src/theme/lib/read-social-links.ts +4 -3
- package/src/theme/lib/theme-favicon.ts +1 -1
- package/src/theme/lib/with-mount-base.ts +31 -0
- package/src/theme/styles/overrides/footnotes.css +54 -0
- package/src/theme/styles/overrides/lists.css +83 -0
- package/src/theme/styles/overrides/rail.css +27 -10
- package/src/theme/styles/overrides/section-card.css +9 -0
- package/src/theme/styles/overrides/sidebar.css +22 -0
- package/dist/theme/components/openapi/copy-markdown-button.tsx +0 -41
- package/src/theme/components/openapi/copy-markdown-button.tsx +0 -41
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useLocation } from '@rspress/core/runtime'
|
|
1
|
+
import { removeBase, useLocation } from '@rspress/core/runtime'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { match } from 'massaman/match'
|
|
4
4
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
@@ -22,13 +22,26 @@ const OVERFLOW_TOGGLE_WIDTH = 96
|
|
|
22
22
|
*/
|
|
23
23
|
const DEFAULT_GAP_PX = 16
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Grace period before a hover-opened dropdown closes on mouse-leave.
|
|
27
|
+
* Long enough to cross the gap into the popover without it snapping
|
|
28
|
+
* shut, short enough not to feel sticky.
|
|
29
|
+
*/
|
|
30
|
+
const CLOSE_DELAY_MS = 220
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* Single primary-nav entry — matches the shape of `site.nav[*]` in
|
|
27
34
|
* `ciderpress.config.ts`.
|
|
35
|
+
*
|
|
36
|
+
* An entry is either a leaf (has `link`, no `items`) or a dropdown
|
|
37
|
+
* parent (has `items`, `link` optional). When a parent has children,
|
|
38
|
+
* its own `link` is ignored — the label toggles the submenu rather
|
|
39
|
+
* than navigating.
|
|
28
40
|
*/
|
|
29
41
|
export interface CiderpressNavMenuItem {
|
|
30
42
|
readonly text: string
|
|
31
|
-
readonly link
|
|
43
|
+
readonly link?: string
|
|
44
|
+
readonly items?: readonly CiderpressNavMenuItem[]
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
/**
|
|
@@ -151,24 +164,25 @@ export function CiderpressNavMenu(props: CiderpressNavMenuProps): React.ReactEle
|
|
|
151
164
|
return (
|
|
152
165
|
<>
|
|
153
166
|
<div ref={measureRef} className="cp-nav-menu-measure" aria-hidden="true">
|
|
154
|
-
{items.map((item) => (
|
|
155
|
-
<span
|
|
167
|
+
{items.map((item, index) => (
|
|
168
|
+
<span
|
|
169
|
+
key={`${itemKey(item)}::${index}`}
|
|
170
|
+
data-cp-menu-item
|
|
171
|
+
className={clsx('cp-nav-menu__item', {
|
|
172
|
+
// Mirror the live dropdown toggle's label→chevron gap so the
|
|
173
|
+
// measured width matches what actually renders inline.
|
|
174
|
+
'cp-nav-menu__item--measured-dropdown': hasChildren(item),
|
|
175
|
+
})}
|
|
176
|
+
>
|
|
156
177
|
{item.text}
|
|
178
|
+
{hasChildren(item) && <Icon icon="pixelarticons:chevron-down" width={12} height={12} />}
|
|
157
179
|
</span>
|
|
158
180
|
))}
|
|
159
181
|
</div>
|
|
160
182
|
|
|
161
183
|
<nav ref={containerRef} className="cp-nav-menu" aria-label="Primary">
|
|
162
|
-
{visible.map((item) => (
|
|
163
|
-
<
|
|
164
|
-
key={item.link}
|
|
165
|
-
href={item.link}
|
|
166
|
-
className={clsx('cp-nav-menu__item', {
|
|
167
|
-
'cp-nav-menu__item--active': isActive(pathname, item.link),
|
|
168
|
-
})}
|
|
169
|
-
>
|
|
170
|
-
{item.text}
|
|
171
|
-
</RouteLink>
|
|
184
|
+
{visible.map((item, index) => (
|
|
185
|
+
<NavMenuEntry key={`${itemKey(item)}::${index}`} item={item} pathname={pathname} />
|
|
172
186
|
))}
|
|
173
187
|
{hasOverflow && (
|
|
174
188
|
<div ref={overflowRef} className="cp-nav-menu__overflow">
|
|
@@ -184,19 +198,13 @@ export function CiderpressNavMenu(props: CiderpressNavMenuProps): React.ReactEle
|
|
|
184
198
|
</button>
|
|
185
199
|
{overflowOpen && (
|
|
186
200
|
<ul className="cp-nav-menu__overflow-popover" role="menu">
|
|
187
|
-
{overflow.map((item) => (
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
})}
|
|
195
|
-
onClick={() => setOverflowOpen(false)}
|
|
196
|
-
>
|
|
197
|
-
{item.text}
|
|
198
|
-
</RouteLink>
|
|
199
|
-
</li>
|
|
201
|
+
{overflow.map((item, index) => (
|
|
202
|
+
<OverflowEntry
|
|
203
|
+
key={`${itemKey(item)}::${index}`}
|
|
204
|
+
item={item}
|
|
205
|
+
pathname={pathname}
|
|
206
|
+
onNavigate={() => setOverflowOpen(false)}
|
|
207
|
+
/>
|
|
200
208
|
))}
|
|
201
209
|
</ul>
|
|
202
210
|
)}
|
|
@@ -209,6 +217,286 @@ export function CiderpressNavMenu(props: CiderpressNavMenuProps): React.ReactEle
|
|
|
209
217
|
|
|
210
218
|
export { CiderpressNavMenu as default }
|
|
211
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Render a single inline nav entry — a plain link when the item is a
|
|
222
|
+
* leaf, or a hover/click dropdown when it carries child `items`.
|
|
223
|
+
*
|
|
224
|
+
* @private
|
|
225
|
+
* @param props - The nav item and the current pathname.
|
|
226
|
+
* @returns The entry element.
|
|
227
|
+
*/
|
|
228
|
+
function NavMenuEntry(props: {
|
|
229
|
+
readonly item: CiderpressNavMenuItem
|
|
230
|
+
readonly pathname: string
|
|
231
|
+
}): React.ReactElement {
|
|
232
|
+
const { item, pathname } = props
|
|
233
|
+
return match(hasChildren(item))
|
|
234
|
+
.with(true, () => <NavMenuDropdown item={item} pathname={pathname} />)
|
|
235
|
+
.otherwise(() => (
|
|
236
|
+
<RouteLink
|
|
237
|
+
href={item.link ?? '#'}
|
|
238
|
+
className={clsx('cp-nav-menu__item', {
|
|
239
|
+
'cp-nav-menu__item--active': isActiveLink(pathname, item.link),
|
|
240
|
+
})}
|
|
241
|
+
>
|
|
242
|
+
{item.text}
|
|
243
|
+
</RouteLink>
|
|
244
|
+
))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A topbar dropdown: a toggle button plus a popover of child links.
|
|
249
|
+
* Opens on hover and on click, closes on outside click, on child
|
|
250
|
+
* navigation, or on `Escape`. The toggle is marked active when the
|
|
251
|
+
* current route matches any child link.
|
|
252
|
+
*
|
|
253
|
+
* @private
|
|
254
|
+
* @param props - The dropdown item and the current pathname.
|
|
255
|
+
* @returns The dropdown element.
|
|
256
|
+
*/
|
|
257
|
+
function NavMenuDropdown(props: {
|
|
258
|
+
readonly item: CiderpressNavMenuItem
|
|
259
|
+
readonly pathname: string
|
|
260
|
+
}): React.ReactElement {
|
|
261
|
+
const { item, pathname } = props
|
|
262
|
+
// Two independent inputs: `hovering` (pointer preview) and `pinned`
|
|
263
|
+
// (an explicit click/tap/Enter latch). The menu is open when either is
|
|
264
|
+
// set. This keeps hover-to-preview and click-to-toggle from fighting —
|
|
265
|
+
// clicking an already-hover-open menu pins it instead of closing it.
|
|
266
|
+
const [hovering, setHovering] = useState(false)
|
|
267
|
+
const [pinned, setPinned] = useState(false)
|
|
268
|
+
const open = hovering || pinned
|
|
269
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
270
|
+
const toggleRef = useRef<HTMLButtonElement>(null)
|
|
271
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
272
|
+
const children = item.items ?? []
|
|
273
|
+
|
|
274
|
+
// Cancel any pending hover-close (mouse re-entered, or an explicit action).
|
|
275
|
+
function cancelClose(): void {
|
|
276
|
+
if (closeTimer.current !== null) {
|
|
277
|
+
clearTimeout(closeTimer.current)
|
|
278
|
+
closeTimer.current = null
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Fully close: drop both hover and pin. Used by Escape, outside-click,
|
|
283
|
+
// and child navigation.
|
|
284
|
+
function close(): void {
|
|
285
|
+
cancelClose()
|
|
286
|
+
setHovering(false)
|
|
287
|
+
setPinned(false)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Drop the hover preview after a short grace period so brief excursions
|
|
291
|
+
// off the toggle (crossing into the popover, a jittery pointer) don't
|
|
292
|
+
// snap it shut. A pinned menu stays open regardless.
|
|
293
|
+
function scheduleClose(): void {
|
|
294
|
+
cancelClose()
|
|
295
|
+
closeTimer.current = setTimeout(() => setHovering(false), CLOSE_DELAY_MS)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
useEffect(() => () => cancelClose(), [])
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (!open) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
function onDocClick(event: MouseEvent): void {
|
|
305
|
+
const target = event.target as Node | null
|
|
306
|
+
if (target !== null && ref.current !== null && !ref.current.contains(target)) {
|
|
307
|
+
close()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
311
|
+
if (event.key === 'Escape') {
|
|
312
|
+
close()
|
|
313
|
+
// Return focus to the toggle so a keyboard user isn't stranded
|
|
314
|
+
// on a link that just unmounted.
|
|
315
|
+
if (toggleRef.current !== null) {
|
|
316
|
+
toggleRef.current.focus()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
document.addEventListener('mousedown', onDocClick)
|
|
321
|
+
document.addEventListener('keydown', onKeyDown)
|
|
322
|
+
return () => {
|
|
323
|
+
document.removeEventListener('mousedown', onDocClick)
|
|
324
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
325
|
+
}
|
|
326
|
+
}, [open])
|
|
327
|
+
|
|
328
|
+
const active = children.some((child) => isActiveLink(pathname, child.link))
|
|
329
|
+
|
|
330
|
+
function handleMouseEnter(): void {
|
|
331
|
+
cancelClose()
|
|
332
|
+
setHovering(true)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Close when focus leaves the dropdown entirely (Tab past the last
|
|
336
|
+
// link). Keep it open while focus moves between the toggle and items.
|
|
337
|
+
function handleBlur(event: React.FocusEvent<HTMLDivElement>): void {
|
|
338
|
+
const next = event.relatedTarget
|
|
339
|
+
if (ref.current !== null && next instanceof Node && ref.current.contains(next)) {
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
close()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Click/tap/Enter is an explicit latch: pin it open, or unpin (and drop
|
|
346
|
+
// any lingering hover) to close. Works identically for mouse, touch,
|
|
347
|
+
// and keyboard.
|
|
348
|
+
function handleToggleClick(): void {
|
|
349
|
+
cancelClose()
|
|
350
|
+
if (pinned) {
|
|
351
|
+
setPinned(false)
|
|
352
|
+
setHovering(false)
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
setPinned(true)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<div
|
|
360
|
+
ref={ref}
|
|
361
|
+
className="cp-nav-menu__dropdown"
|
|
362
|
+
onMouseEnter={handleMouseEnter}
|
|
363
|
+
onMouseLeave={scheduleClose}
|
|
364
|
+
onBlur={handleBlur}
|
|
365
|
+
>
|
|
366
|
+
<button
|
|
367
|
+
ref={toggleRef}
|
|
368
|
+
type="button"
|
|
369
|
+
className={clsx('cp-nav-menu__item', 'cp-nav-menu__dropdown-toggle', {
|
|
370
|
+
'cp-nav-menu__item--active': active,
|
|
371
|
+
})}
|
|
372
|
+
onClick={handleToggleClick}
|
|
373
|
+
aria-haspopup="menu"
|
|
374
|
+
aria-expanded={open}
|
|
375
|
+
>
|
|
376
|
+
<span>{item.text}</span>
|
|
377
|
+
<Icon icon="pixelarticons:chevron-down" width={12} height={12} />
|
|
378
|
+
</button>
|
|
379
|
+
{open && (
|
|
380
|
+
<ul className="cp-nav-menu__dropdown-popover" role="menu" aria-label={item.text}>
|
|
381
|
+
{children.map((child, index) => (
|
|
382
|
+
<li key={`${itemKey(child)}::${index}`} role="none">
|
|
383
|
+
<RouteLink
|
|
384
|
+
href={child.link ?? '#'}
|
|
385
|
+
role="menuitem"
|
|
386
|
+
className={clsx('cp-nav-menu__overflow-item', {
|
|
387
|
+
'cp-nav-menu__overflow-item--active': isActiveLink(pathname, child.link),
|
|
388
|
+
})}
|
|
389
|
+
onClick={close}
|
|
390
|
+
>
|
|
391
|
+
{child.text}
|
|
392
|
+
</RouteLink>
|
|
393
|
+
</li>
|
|
394
|
+
))}
|
|
395
|
+
</ul>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Render an entry inside the "More" overflow popover. Leaf items become
|
|
403
|
+
* a single menu link; dropdown parents become a labelled group with
|
|
404
|
+
* their children listed beneath.
|
|
405
|
+
*
|
|
406
|
+
* @private
|
|
407
|
+
* @param props - The item, current pathname, and a navigate callback.
|
|
408
|
+
* @returns The overflow entry list element.
|
|
409
|
+
*/
|
|
410
|
+
function OverflowEntry(props: {
|
|
411
|
+
readonly item: CiderpressNavMenuItem
|
|
412
|
+
readonly pathname: string
|
|
413
|
+
readonly onNavigate: () => void
|
|
414
|
+
}): React.ReactElement {
|
|
415
|
+
const { item, pathname, onNavigate } = props
|
|
416
|
+
const children = item.items ?? []
|
|
417
|
+
return match(children.length > 0)
|
|
418
|
+
.with(true, () => (
|
|
419
|
+
<li className="cp-nav-menu__overflow-group" role="none">
|
|
420
|
+
<span className="cp-nav-menu__overflow-group-label" aria-hidden="true">
|
|
421
|
+
{item.text}
|
|
422
|
+
</span>
|
|
423
|
+
<ul className="cp-nav-menu__overflow-sublist" role="menu" aria-label={item.text}>
|
|
424
|
+
{children.map((child, index) => (
|
|
425
|
+
<li key={`${itemKey(child)}::${index}`} role="none">
|
|
426
|
+
<RouteLink
|
|
427
|
+
href={child.link ?? '#'}
|
|
428
|
+
role="menuitem"
|
|
429
|
+
className={clsx('cp-nav-menu__overflow-item', {
|
|
430
|
+
'cp-nav-menu__overflow-item--active': isActiveLink(pathname, child.link),
|
|
431
|
+
})}
|
|
432
|
+
onClick={onNavigate}
|
|
433
|
+
>
|
|
434
|
+
{child.text}
|
|
435
|
+
</RouteLink>
|
|
436
|
+
</li>
|
|
437
|
+
))}
|
|
438
|
+
</ul>
|
|
439
|
+
</li>
|
|
440
|
+
))
|
|
441
|
+
.otherwise(() => (
|
|
442
|
+
<li role="none">
|
|
443
|
+
<RouteLink
|
|
444
|
+
href={item.link ?? '#'}
|
|
445
|
+
role="menuitem"
|
|
446
|
+
className={clsx('cp-nav-menu__overflow-item', {
|
|
447
|
+
'cp-nav-menu__overflow-item--active': isActiveLink(pathname, item.link),
|
|
448
|
+
})}
|
|
449
|
+
onClick={onNavigate}
|
|
450
|
+
>
|
|
451
|
+
{item.text}
|
|
452
|
+
</RouteLink>
|
|
453
|
+
</li>
|
|
454
|
+
))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Whether a nav item carries a non-empty `items` array (making it a
|
|
459
|
+
* dropdown parent rather than a leaf).
|
|
460
|
+
*
|
|
461
|
+
* @private
|
|
462
|
+
* @param item - Nav item to test.
|
|
463
|
+
* @returns True when the item has at least one child.
|
|
464
|
+
*/
|
|
465
|
+
function hasChildren(item: CiderpressNavMenuItem): boolean {
|
|
466
|
+
return item.items !== undefined && item.items.length > 0
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Stable React key for a nav item — its link when present, otherwise
|
|
471
|
+
* its label (dropdown parents may have no link of their own).
|
|
472
|
+
*
|
|
473
|
+
* @private
|
|
474
|
+
* @param item - Nav item to key.
|
|
475
|
+
* @returns Key string.
|
|
476
|
+
*/
|
|
477
|
+
function itemKey(item: CiderpressNavMenuItem): string {
|
|
478
|
+
if (item.link !== undefined && item.link !== '') {
|
|
479
|
+
return item.link
|
|
480
|
+
}
|
|
481
|
+
return item.text
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Active-route test that tolerates an absent link (dropdown parents),
|
|
486
|
+
* delegating to {@link isActive} only when a link is present.
|
|
487
|
+
*
|
|
488
|
+
* @private
|
|
489
|
+
* @param pathname - Current route pathname.
|
|
490
|
+
* @param link - Item link, possibly undefined.
|
|
491
|
+
* @returns True when the link is present and matches the route.
|
|
492
|
+
*/
|
|
493
|
+
function isActiveLink(pathname: string, link: string | undefined): boolean {
|
|
494
|
+
if (link === undefined) {
|
|
495
|
+
return false
|
|
496
|
+
}
|
|
497
|
+
return isActive(pathname, link)
|
|
498
|
+
}
|
|
499
|
+
|
|
212
500
|
/**
|
|
213
501
|
* Walk the per-item widths left-to-right, accumulating until we'd
|
|
214
502
|
* exceed the available width. When the accumulator already exceeds the
|
|
@@ -275,34 +563,105 @@ function gapAt(index: number, gap: number): number {
|
|
|
275
563
|
}
|
|
276
564
|
|
|
277
565
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
566
|
+
* Reconstruct the primary nav from Rspress's hidden `.rp-nav-menu`,
|
|
567
|
+
* preserving dropdowns. Each top-level `.rp-nav-menu__item` is either a
|
|
568
|
+
* leaf (its container is an anchor) or a dropdown parent (it wraps a
|
|
569
|
+
* `.rp-hover-group` of child links). Items with empty text, or dropdown
|
|
570
|
+
* parents with no usable children, are dropped.
|
|
281
571
|
*
|
|
282
572
|
* @private
|
|
283
573
|
* @returns Nav items currently in the DOM (empty array when not mounted).
|
|
284
574
|
*/
|
|
285
575
|
function scrapeNavItems(): readonly CiderpressNavMenuItem[] {
|
|
286
|
-
|
|
576
|
+
return navMenuRoots()
|
|
577
|
+
.map(scrapeNavItem)
|
|
578
|
+
.filter((item): item is CiderpressNavMenuItem => item !== null)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Collect the top-level `.rp-nav-menu__item` `<li>`s to scrape. Rspress
|
|
583
|
+
* renders separate left and right nav `<ul>`s; the ciderpress topbar is
|
|
584
|
+
* right-aligned, so we read the right menu and only fall back to the
|
|
585
|
+
* unscoped selector when it isn't present.
|
|
586
|
+
*
|
|
587
|
+
* @private
|
|
588
|
+
* @returns Top-level nav item elements.
|
|
589
|
+
*/
|
|
590
|
+
function navMenuRoots(): readonly HTMLElement[] {
|
|
591
|
+
const right = document.querySelectorAll<HTMLElement>('.rp-nav-menu--right > .rp-nav-menu__item')
|
|
592
|
+
if (right.length > 0) {
|
|
593
|
+
return [...right]
|
|
594
|
+
}
|
|
595
|
+
return [...document.querySelectorAll<HTMLElement>('.rp-nav-menu > .rp-nav-menu__item')]
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Project a single top-level `.rp-nav-menu__item` element into a nav
|
|
600
|
+
* item, recursing one level into its `.rp-hover-group` dropdown when
|
|
601
|
+
* present.
|
|
602
|
+
*
|
|
603
|
+
* @private
|
|
604
|
+
* @param root - Top-level nav `<li>` element.
|
|
605
|
+
* @returns Parsed nav item, or `null` when unusable.
|
|
606
|
+
*/
|
|
607
|
+
function scrapeNavItem(root: HTMLElement): CiderpressNavMenuItem | null {
|
|
608
|
+
const container = root.querySelector(':scope > .rp-nav-menu__item__container')
|
|
609
|
+
if (container === null) {
|
|
610
|
+
return null
|
|
611
|
+
}
|
|
612
|
+
const text = readElementText(container)
|
|
613
|
+
if (text === '') {
|
|
614
|
+
return null
|
|
615
|
+
}
|
|
616
|
+
const group = root.querySelector(':scope > .rp-hover-group')
|
|
617
|
+
if (group !== null) {
|
|
618
|
+
const items = scrapeGroupItems(group)
|
|
619
|
+
if (items.length === 0) {
|
|
620
|
+
return null
|
|
621
|
+
}
|
|
622
|
+
return { text, items }
|
|
623
|
+
}
|
|
624
|
+
const href = container.getAttribute('href')
|
|
625
|
+
if (href === null || href === '') {
|
|
626
|
+
return null
|
|
627
|
+
}
|
|
628
|
+
// Un-base the scraped href so `<Link>` re-applies the site `base` once
|
|
629
|
+
// rather than doubling the mount prefix on subpath deploys.
|
|
630
|
+
return { text, link: removeBase(href) }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Read the child links out of a Rspress `.rp-hover-group` dropdown.
|
|
635
|
+
*
|
|
636
|
+
* @private
|
|
637
|
+
* @param group - The `.rp-hover-group` element.
|
|
638
|
+
* @returns Child nav items with text + link (empties dropped).
|
|
639
|
+
*/
|
|
640
|
+
function scrapeGroupItems(group: Element): readonly CiderpressNavMenuItem[] {
|
|
641
|
+
const anchors = group.querySelectorAll<HTMLAnchorElement>('.rp-hover-group__item__link')
|
|
287
642
|
return [...anchors]
|
|
288
643
|
.map((anchor) => ({
|
|
289
|
-
text:
|
|
290
|
-
|
|
644
|
+
text: readElementText(anchor),
|
|
645
|
+
// Rspress's rendered `.rp-nav-menu` hrefs already carry the site `base`.
|
|
646
|
+
// Strip it here so `RouteLink` (→ Rspress `<Link>`) can re-apply it once
|
|
647
|
+
// rather than doubling the mount prefix on a subpath deploy (the
|
|
648
|
+
// `/examples/<slug>/examples/<slug>/…` 404 on mounted example sites).
|
|
649
|
+
link: removeBase(anchor.getAttribute('href') ?? ''),
|
|
291
650
|
}))
|
|
292
651
|
.filter((item) => item.text !== '' && item.link !== '')
|
|
293
652
|
}
|
|
294
653
|
|
|
295
654
|
/**
|
|
296
|
-
* Pull the trimmed text content from an
|
|
655
|
+
* Pull the trimmed text content from an element. Returns an empty
|
|
297
656
|
* string when `textContent` is missing — callers treat empty as "skip
|
|
298
|
-
* this
|
|
657
|
+
* this element".
|
|
299
658
|
*
|
|
300
659
|
* @private
|
|
301
|
-
* @param
|
|
660
|
+
* @param element - Element to read.
|
|
302
661
|
* @returns Trimmed inner text, or empty string when absent.
|
|
303
662
|
*/
|
|
304
|
-
function
|
|
305
|
-
const text =
|
|
663
|
+
function readElementText(element: Element): string {
|
|
664
|
+
const text = element.textContent
|
|
306
665
|
if (text === null) {
|
|
307
666
|
return ''
|
|
308
667
|
}
|
|
@@ -6,13 +6,15 @@ import { Icon } from '../shared/icon.tsx'
|
|
|
6
6
|
import './ciderpress-nav-social-links.css'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Single social-link entry — matches the
|
|
10
|
-
* `
|
|
9
|
+
* Single social-link entry — matches the serialised `socials` shape
|
|
10
|
+
* written into `themeConfig.socialLinks` by `packages/ui/src/config.ts`.
|
|
11
|
+
*
|
|
12
|
+
* The Rspress `mode` / `content` discriminator was killed in the
|
|
13
|
+
* `rc.4` API overhaul; every link is now a plain anchor.
|
|
11
14
|
*/
|
|
12
15
|
export interface CiderpressSocialLink {
|
|
13
16
|
readonly icon: string
|
|
14
|
-
readonly
|
|
15
|
-
readonly content: string
|
|
17
|
+
readonly url: string
|
|
16
18
|
readonly label?: string
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -21,25 +23,32 @@ export interface CiderpressNavSocialLinksProps {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
* Maps Rspress's social-link `icon` slugs to
|
|
25
|
-
*
|
|
26
|
+
* Maps Rspress's social-link `icon` slugs to `pixel` icon ids — one pixel-art
|
|
27
|
+
* glyph per {@link SocialLinkIcon} the config enum accepts, in the same
|
|
28
|
+
* aesthetic as the rest of the theme. Unmapped slugs fall back to the generic
|
|
29
|
+
* `pixel:link` chain glyph.
|
|
26
30
|
*/
|
|
27
31
|
const ICON_MAP: Readonly<Record<string, string>> = Object.freeze({
|
|
28
|
-
github: '
|
|
29
|
-
npm: '
|
|
30
|
-
twitter: '
|
|
31
|
-
x: '
|
|
32
|
-
discord: '
|
|
33
|
-
youtube: '
|
|
34
|
-
bluesky: '
|
|
35
|
-
mastodon: '
|
|
32
|
+
github: 'pixel:github',
|
|
33
|
+
npm: 'pixel:npm',
|
|
34
|
+
twitter: 'pixel:twitter',
|
|
35
|
+
x: 'pixel:twitter',
|
|
36
|
+
discord: 'pixel:discord',
|
|
37
|
+
youtube: 'pixel:youtube',
|
|
38
|
+
bluesky: 'pixel:bluesky',
|
|
39
|
+
mastodon: 'pixel:mastodon',
|
|
40
|
+
slack: 'pixel:slack',
|
|
41
|
+
linkedin: 'pixel:linkedin',
|
|
42
|
+
gitlab: 'pixel:gitlab',
|
|
43
|
+
instagram: 'pixel:instagram',
|
|
44
|
+
facebook: 'pixel:facebook-round',
|
|
36
45
|
})
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
48
|
* Renders the configured social links as a cluster of icon buttons in
|
|
40
49
|
* the topbar. Returns `null` when no links are configured.
|
|
41
50
|
*
|
|
42
|
-
* @param props - List of social links from `site.
|
|
51
|
+
* @param props - List of social links from `site.socials`
|
|
43
52
|
* @returns Cluster of icon links, or `null`
|
|
44
53
|
*/
|
|
45
54
|
export function CiderpressNavSocialLinks(
|
|
@@ -50,7 +59,7 @@ export function CiderpressNavSocialLinks(
|
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
const safeLinks = props.links
|
|
53
|
-
.map((link) => ({ link, href: safeUrl(link.
|
|
62
|
+
.map((link) => ({ link, href: safeUrl(link.url) }))
|
|
54
63
|
.filter((entry): entry is { readonly link: CiderpressSocialLink; readonly href: string } => {
|
|
55
64
|
if (entry.href === null) {
|
|
56
65
|
return false
|
|
@@ -66,18 +75,14 @@ export function CiderpressNavSocialLinks(
|
|
|
66
75
|
<div className="cp-nav-social">
|
|
67
76
|
{safeLinks.map(({ link, href }) => (
|
|
68
77
|
<a
|
|
69
|
-
key={link.
|
|
78
|
+
key={link.url}
|
|
70
79
|
href={href}
|
|
71
80
|
target="_blank"
|
|
72
81
|
rel="noopener noreferrer"
|
|
73
82
|
className="cp-nav-social__item"
|
|
74
83
|
aria-label={link.label ?? link.icon}
|
|
75
84
|
>
|
|
76
|
-
<Icon
|
|
77
|
-
icon={ICON_MAP[link.icon.toLowerCase()] ?? 'pixelarticons:link'}
|
|
78
|
-
width={20}
|
|
79
|
-
height={20}
|
|
80
|
-
/>
|
|
85
|
+
<Icon icon={ICON_MAP[link.icon.toLowerCase()] ?? 'pixel:link'} width={20} height={20} />
|
|
81
86
|
</a>
|
|
82
87
|
))}
|
|
83
88
|
</div>
|
|
@@ -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 './floating-branch-indicator.css'
|
|
@@ -15,7 +15,7 @@ declare const __CIDERPRESS_GIT_BRANCH__: string | undefined
|
|
|
15
15
|
* the branch name. Click anywhere on the pill opens the branch on GitHub.
|
|
16
16
|
*
|
|
17
17
|
* Hidden when on default branches (`main` / `master`), when the
|
|
18
|
-
* build-time branch global is undefined, or when `
|
|
18
|
+
* build-time branch global is undefined, or when `editLink.repo` is
|
|
19
19
|
* not configured.
|
|
20
20
|
*
|
|
21
21
|
* @returns React element or null when hidden
|
|
@@ -69,7 +69,7 @@ function resolveBranch(): string {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* Build a GitHub `/tree/<branch>` URL from the configured `
|
|
72
|
+
* Build a GitHub `/tree/<branch>` URL from the configured `editLink.repo`.
|
|
73
73
|
*
|
|
74
74
|
* Accepts either a full URL (used as-is) or an `org/repo` slug
|
|
75
75
|
* (prefixed with `https://github.com/`). Returns `null` when `repo` is
|
|
@@ -91,14 +91,14 @@ function buildBranchHref(params: {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Pull the configured edit-repo slug off the site
|
|
95
|
-
* with explicit null checks rather than optional chaining.
|
|
94
|
+
* Pull the configured edit-repo slug off the serialised site block,
|
|
95
|
+
* expressed with explicit null checks rather than optional chaining.
|
|
96
96
|
*
|
|
97
97
|
* @private
|
|
98
|
-
* @param site - The
|
|
98
|
+
* @param site - The runtime site block (may be undefined).
|
|
99
99
|
* @returns The configured `edit.repo` value or `undefined`.
|
|
100
100
|
*/
|
|
101
|
-
function resolveRepo(site:
|
|
101
|
+
function resolveRepo(site: CiderpressSiteBlock | undefined): string | undefined {
|
|
102
102
|
if (site === undefined) {
|
|
103
103
|
return undefined
|
|
104
104
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small brand chip rendered immediately before `<HeaderLogo />` inside
|
|
3
|
+
* `cp-header-logo`. Sized to match `--cp-header-icon-size` (default
|
|
4
|
+
* 28px — same as the logo height token) so the two slots line up
|
|
5
|
+
* vertically by default. Themes can override either token to break
|
|
6
|
+
* the lockstep.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
.cp-header-icon {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
height: var(--cp-header-icon-size, var(--cp-header-logo-height, 28px));
|
|
14
|
+
width: var(--cp-header-icon-size, var(--cp-header-logo-height, 28px));
|
|
15
|
+
margin-right: var(--cp-header-icon-gap, 8px);
|
|
16
|
+
color: var(--cp-header-icon-color, currentColor);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.cp-header-icon > svg,
|
|
20
|
+
.cp-header-icon__img {
|
|
21
|
+
height: 100%;
|
|
22
|
+
width: 100%;
|
|
23
|
+
display: block;
|
|
24
|
+
}
|