@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.
- package/dist/node.mjs +20 -2
- package/dist/plugins/mermaid/MermaidRenderer.tsx +24 -6
- 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-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 +18 -15
- package/dist/theme/components/nav/layout.tsx +72 -9
- package/dist/theme/components/shared/icon.tsx +2 -1
- package/dist/theme/components/shared/section-card.tsx +28 -0
- 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-scope.tsx +24 -1
- package/dist/theme/hooks/use-ciderpress.ts +6 -1
- package/dist/theme/hooks/use-nav-items.ts +82 -11
- package/dist/theme/index.tsx +3 -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 +13 -12
- 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-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 +18 -15
- package/src/theme/components/nav/layout.tsx +72 -9
- package/src/theme/components/shared/icon.tsx +2 -1
- package/src/theme/components/shared/section-card.tsx +28 -0
- 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-scope.tsx +24 -1
- package/src/theme/hooks/use-ciderpress.ts +6 -1
- package/src/theme/hooks/use-nav-items.ts +82 -11
- package/src/theme/index.tsx +3 -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/node.mjs
CHANGED
|
@@ -17469,6 +17469,11 @@ function createRspressConfig(options) {
|
|
|
17469
17469
|
name: 'scopes.json',
|
|
17470
17470
|
fallback: []
|
|
17471
17471
|
});
|
|
17472
|
+
const pageBadges = loadGenerated({
|
|
17473
|
+
contentDir: paths.contentDir,
|
|
17474
|
+
name: 'badges.json',
|
|
17475
|
+
fallback: {}
|
|
17476
|
+
});
|
|
17472
17477
|
const gitBranch = detectGitBranch();
|
|
17473
17478
|
const brand = config.brand;
|
|
17474
17479
|
const home = config.home;
|
|
@@ -17528,6 +17533,7 @@ function createRspressConfig(options) {
|
|
|
17528
17533
|
config,
|
|
17529
17534
|
editLink,
|
|
17530
17535
|
reportLink,
|
|
17536
|
+
feedback: config.feedback,
|
|
17531
17537
|
topbar,
|
|
17532
17538
|
sidebar,
|
|
17533
17539
|
footer
|
|
@@ -17636,7 +17642,8 @@ function createRspressConfig(options) {
|
|
|
17636
17642
|
},
|
|
17637
17643
|
...{
|
|
17638
17644
|
workspaces,
|
|
17639
|
-
standaloneScopePaths
|
|
17645
|
+
standaloneScopePaths,
|
|
17646
|
+
pageBadges
|
|
17640
17647
|
},
|
|
17641
17648
|
...{
|
|
17642
17649
|
socialLinks: serializeSocials(config.socials),
|
|
@@ -17831,6 +17838,16 @@ function buildSiteBlock(params) {
|
|
|
17831
17838
|
tagline: f.tagline,
|
|
17832
17839
|
brandMark: f.brandMark
|
|
17833
17840
|
}));
|
|
17841
|
+
const feedbackBlock = match_match(params.feedback).with(true, ()=>({
|
|
17842
|
+
enabled: true,
|
|
17843
|
+
question: void 0
|
|
17844
|
+
})).with(P.union(false, void 0), ()=>({
|
|
17845
|
+
enabled: false,
|
|
17846
|
+
question: void 0
|
|
17847
|
+
})).otherwise((f)=>({
|
|
17848
|
+
enabled: true,
|
|
17849
|
+
question: f.question
|
|
17850
|
+
}));
|
|
17834
17851
|
return {
|
|
17835
17852
|
version: params.config.version,
|
|
17836
17853
|
edit: editBlock,
|
|
@@ -17838,7 +17855,8 @@ function buildSiteBlock(params) {
|
|
|
17838
17855
|
topbarCta,
|
|
17839
17856
|
sidebarPromo,
|
|
17840
17857
|
announcement,
|
|
17841
|
-
footer: footerBlock
|
|
17858
|
+
footer: footerBlock,
|
|
17859
|
+
feedback: feedbackBlock
|
|
17842
17860
|
};
|
|
17843
17861
|
}
|
|
17844
17862
|
function resolveHomeConfig(home) {
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
// workspace dependencies here — they will cause "factory is undefined" runtime
|
|
5
5
|
// errors. See packages/ui/AGENTS.md for details.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// MERMAID v11: works fine here — no pin needed. The earlier v10 pin blamed
|
|
8
|
+
// langium/lazy-loading, but mermaid.render() actually resolves correctly. The
|
|
9
|
+
// real bug was local: `config` defaults to a fresh {} each render, so keying
|
|
10
|
+
// renderMermaid on it re-created the callback every render and re-fired the
|
|
11
|
+
// render effect in a loop. Each loop iteration called mermaid.render() into the
|
|
12
|
+
// same element id, clobbering the just-injected SVG — the diagram went blank
|
|
13
|
+
// until an unrelated re-render happened to run last. Fixed by keying on a stable
|
|
14
|
+
// serialized config value (see configKey below).
|
|
11
15
|
|
|
12
16
|
// oxlint-disable no-ternary
|
|
13
17
|
|
|
@@ -214,6 +218,11 @@ function MermaidRenderer(props: MermaidRendererProps): React.ReactElement | null
|
|
|
214
218
|
|
|
215
219
|
const [svg, setSvg] = useState('')
|
|
216
220
|
const [renderError, setRenderError] = useState(false)
|
|
221
|
+
// Monotonic counter for per-render element ids. mermaid.render() renders into a
|
|
222
|
+
// node keyed by the id we pass; reusing one id across calls (React StrictMode
|
|
223
|
+
// double-invokes the mount effect) lets a later call clobber the SVG an earlier
|
|
224
|
+
// call already injected. A fresh id per call makes every render self-contained.
|
|
225
|
+
const renderSeq = useRef(0)
|
|
217
226
|
const [transform, setTransform] = useState<Transform>(INITIAL_TRANSFORM)
|
|
218
227
|
const transformRef = useRef<Transform>(INITIAL_TRANSFORM)
|
|
219
228
|
const [fullscreen, setFullscreen] = useState(false)
|
|
@@ -221,6 +230,14 @@ function MermaidRenderer(props: MermaidRendererProps): React.ReactElement | null
|
|
|
221
230
|
|
|
222
231
|
const highlightedCode = useMemo(() => highlightMermaid(code), [code])
|
|
223
232
|
|
|
233
|
+
// Stable value-key for the config object. `config` defaults to a fresh {} on
|
|
234
|
+
// every render, so depending on it directly re-creates renderMermaid each
|
|
235
|
+
// render, which re-fires the render effect in a loop. Each re-render calls
|
|
236
|
+
// mermaid.render() into the same element id — which clobbers the SVG we just
|
|
237
|
+
// injected, leaving the diagram blank until an unrelated re-render happens to
|
|
238
|
+
// land last. Keying on the serialized value keeps renderMermaid stable.
|
|
239
|
+
const configKey = JSON.stringify(config)
|
|
240
|
+
|
|
224
241
|
transformRef.current = transform
|
|
225
242
|
|
|
226
243
|
const isPreview = tab === 'preview'
|
|
@@ -235,14 +252,15 @@ function MermaidRenderer(props: MermaidRendererProps): React.ReactElement | null
|
|
|
235
252
|
...config,
|
|
236
253
|
}
|
|
237
254
|
|
|
255
|
+
const renderId = `${id.replaceAll(':', '')}-${(renderSeq.current += 1)}`
|
|
238
256
|
try {
|
|
239
257
|
mermaid.initialize(mermaidConfig)
|
|
240
|
-
const result = await mermaid.render(
|
|
258
|
+
const result = await mermaid.render(renderId, code as string)
|
|
241
259
|
setSvg(result.svg)
|
|
242
260
|
} catch {
|
|
243
261
|
setRenderError(true)
|
|
244
262
|
}
|
|
245
|
-
}, [code,
|
|
263
|
+
}, [code, configKey, id])
|
|
246
264
|
|
|
247
265
|
useEffect(() => {
|
|
248
266
|
renderMermaid()
|
|
@@ -90,11 +90,21 @@
|
|
|
90
90
|
display: flex;
|
|
91
91
|
align-items: center;
|
|
92
92
|
gap: 6px;
|
|
93
|
-
|
|
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
|
|
@@ -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
|
-
<
|
|
76
|
-
key={item
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|