@conduction/docusaurus-preset 2.6.1 → 2.7.0-beta.2
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/package.json +1 -1
- package/src/components/DetailHero/DetailHero.jsx +17 -1
- package/src/components/primitives/Button.jsx +32 -9
- package/src/components/primitives/Button.module.css +34 -0
- package/src/components/primitives/icons.jsx +64 -0
- package/src/css/brand.css +154 -0
- package/src/css/tokens.css +2 -0
- package/src/index.js +91 -3
- package/src/theme/Footer/index.jsx +20 -1
- package/src/theme/Navbar/index.jsx +128 -15
- package/src/theme/Navbar/styles.module.css +93 -0
- package/src/theme/brand.jsx +47 -0
- package/static/lib/canal-footer.css +19 -0
package/package.json
CHANGED
|
@@ -147,6 +147,7 @@ export default function DetailHero({
|
|
|
147
147
|
variant="primary"
|
|
148
148
|
tone={primaryCta.tone}
|
|
149
149
|
href={primaryCta.href}
|
|
150
|
+
icon={primaryCta.icon}
|
|
150
151
|
>
|
|
151
152
|
{primaryCta.label}
|
|
152
153
|
</Button>
|
|
@@ -156,11 +157,26 @@ export default function DetailHero({
|
|
|
156
157
|
variant="secondary"
|
|
157
158
|
tone={secondaryCta.tone}
|
|
158
159
|
href={secondaryCta.href}
|
|
160
|
+
icon={secondaryCta.icon}
|
|
159
161
|
>
|
|
160
162
|
{secondaryCta.label}
|
|
161
163
|
</Button>
|
|
162
164
|
)}
|
|
163
|
-
{tertiaryCta &&
|
|
165
|
+
{tertiaryCta && (
|
|
166
|
+
/* On a cobalt-bg hero the default ghost variant
|
|
167
|
+
(cobalt-700 text) disappears against the dark panel;
|
|
168
|
+
auto-switch to on-dark-tertiary (white text + white
|
|
169
|
+
border) so the CTA reads at parity with the primary
|
|
170
|
+
and secondary buttons. Sites can still pass an
|
|
171
|
+
explicit `variant` to opt out. */
|
|
172
|
+
<Button
|
|
173
|
+
variant={tertiaryCta.variant || (background === 'cobalt' ? 'on-dark-tertiary' : 'ghost')}
|
|
174
|
+
href={tertiaryCta.href}
|
|
175
|
+
icon={tertiaryCta.icon}
|
|
176
|
+
>
|
|
177
|
+
{tertiaryCta.label} →
|
|
178
|
+
</Button>
|
|
179
|
+
)}
|
|
164
180
|
</div>
|
|
165
181
|
)}
|
|
166
182
|
</div>
|
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* <Button />
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Brand variants used across hero, cta-banner, top-navbar, game-modal,
|
|
5
|
+
* and per-page CTA rows.
|
|
6
6
|
*
|
|
7
7
|
* Renders as <a> when href is provided, <button> otherwise. Inherits
|
|
8
8
|
* the brand transition (140ms ease) and KNVB-orange focus ring from
|
|
9
9
|
* brand.css.
|
|
10
10
|
*
|
|
11
11
|
* Variants:
|
|
12
|
-
* - primary:
|
|
13
|
-
* - secondary:
|
|
14
|
-
* - ghost:
|
|
15
|
-
* - on-dark:
|
|
12
|
+
* - primary: cobalt fill, white text
|
|
13
|
+
* - secondary: white bg, cobalt-200 border, cobalt text
|
|
14
|
+
* - ghost: plain text + arrow, for on-white surfaces
|
|
15
|
+
* - on-dark-primary: primary inverted for use on cobalt CTA panels
|
|
16
|
+
* - on-dark-secondary: ghost-style with white-translucent border, on cobalt
|
|
17
|
+
* - on-dark-tertiary: transparent fill, solid white border + white text,
|
|
18
|
+
* for the "View on GitHub" CTA on cobalt-bg heroes
|
|
16
19
|
*
|
|
17
20
|
* Tone (optional): override the primary/secondary fill colour. The
|
|
18
21
|
* brand default is cobalt; product pages with an orange-accent identity
|
|
19
22
|
* (e.g. mydash) pass `tone="orange"` to flip the primary CTA to
|
|
20
23
|
* KNVB-orange while keeping the rest of the brand chrome intact.
|
|
21
24
|
*
|
|
25
|
+
* Icon (optional): pass a key from icons.jsx (e.g. `"github"`) or a
|
|
26
|
+
* React node directly. The icon sits inline before the label and
|
|
27
|
+
* inherits the button's font-size + colour.
|
|
28
|
+
*
|
|
22
29
|
* Usage:
|
|
23
30
|
*
|
|
24
31
|
* <Button href="/apps">Install</Button>
|
|
25
32
|
* <Button variant="secondary" href="/partners">Get a demo</Button>
|
|
26
33
|
* <Button variant="ghost" href="https://github.com/...">View on GitHub →</Button>
|
|
27
|
-
* <Button
|
|
34
|
+
* <Button
|
|
35
|
+
* variant="on-dark-tertiary"
|
|
36
|
+
* icon="github"
|
|
37
|
+
* href="https://github.com/ConductionNL/shillinq"
|
|
38
|
+
* >View on GitHub</Button>
|
|
28
39
|
*/
|
|
29
40
|
|
|
30
41
|
import React from 'react';
|
|
42
|
+
import {ICONS} from './icons';
|
|
31
43
|
import styles from './Button.module.css';
|
|
32
44
|
|
|
33
45
|
export default function Button({
|
|
@@ -35,6 +47,7 @@ export default function Button({
|
|
|
35
47
|
tone,
|
|
36
48
|
href,
|
|
37
49
|
size = 'md',
|
|
50
|
+
icon,
|
|
38
51
|
className,
|
|
39
52
|
children,
|
|
40
53
|
...rest
|
|
@@ -47,8 +60,18 @@ export default function Button({
|
|
|
47
60
|
className,
|
|
48
61
|
].filter(Boolean).join(' ');
|
|
49
62
|
|
|
63
|
+
/* Icon: accept a string key (looked up in ICONS) or a React node
|
|
64
|
+
directly. The wrapping span lets us apply consistent inline metrics
|
|
65
|
+
without forcing every caller to size their SVG. */
|
|
66
|
+
const iconNode = typeof icon === 'string' ? ICONS[icon] : icon;
|
|
67
|
+
const iconEl = iconNode ? (
|
|
68
|
+
<span className={styles.icon} aria-hidden="true">{iconNode}</span>
|
|
69
|
+
) : null;
|
|
70
|
+
|
|
71
|
+
const body = <>{iconEl}{children}</>;
|
|
72
|
+
|
|
50
73
|
if (href) {
|
|
51
|
-
return <a href={href} className={composed} {...rest}>{
|
|
74
|
+
return <a href={href} className={composed} {...rest}>{body}</a>;
|
|
52
75
|
}
|
|
53
|
-
return <button type="button" className={composed} {...rest}>{
|
|
76
|
+
return <button type="button" className={composed} {...rest}>{body}</button>;
|
|
54
77
|
}
|
|
@@ -17,6 +17,22 @@
|
|
|
17
17
|
font-family: inherit;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/* Inline icon slot. SVG renders at 1em × 1em via icons.jsx, so it
|
|
21
|
+
scales with the button's font-size. The wrapping span keeps the
|
|
22
|
+
icon vertically centred even when the label wraps. */
|
|
23
|
+
.icon {
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
line-height: 1;
|
|
28
|
+
font-size: 1.05em;
|
|
29
|
+
}
|
|
30
|
+
.icon :global(svg) {
|
|
31
|
+
width: 1em;
|
|
32
|
+
height: 1em;
|
|
33
|
+
display: block;
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
.s-sm { padding: 9px 14px; font-size: 13px; }
|
|
21
37
|
.s-md { padding: 13px 22px; font-size: 15px; }
|
|
22
38
|
.s-lg { padding: 16px 28px; font-size: 16px; }
|
|
@@ -84,6 +100,24 @@
|
|
|
84
100
|
color: white;
|
|
85
101
|
}
|
|
86
102
|
|
|
103
|
+
/* On-dark tertiary: the "View on GitHub" CTA that sits next to the
|
|
104
|
+
primary/secondary buttons on a cobalt-bg DetailHero. Transparent
|
|
105
|
+
fill, solid white border, white text — readable on cobalt-900 and
|
|
106
|
+
visually weighted below the primary/secondary CTAs without resorting
|
|
107
|
+
to the ghost variant (which is cobalt-700 text and disappears on
|
|
108
|
+
the dark hero). */
|
|
109
|
+
.v-on-dark-tertiary {
|
|
110
|
+
background: transparent;
|
|
111
|
+
color: white;
|
|
112
|
+
border-color: white;
|
|
113
|
+
}
|
|
114
|
+
.v-on-dark-tertiary:hover {
|
|
115
|
+
background: rgba(255, 255, 255, 0.12);
|
|
116
|
+
color: white;
|
|
117
|
+
border-color: white;
|
|
118
|
+
text-decoration: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
.v-on-dark-primary :global(.next-blue),
|
|
88
122
|
.v-primary :global(.next-blue) { color: var(--c-nextcloud-cyan); }
|
|
89
123
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand icon set.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the SVG glyphs used in the brand chrome
|
|
5
|
+
* (Button, Navbar, hero CTAs). Each icon renders at 1em × 1em with
|
|
6
|
+
* `currentColor`, so it inherits the surrounding font size and colour
|
|
7
|
+
* automatically — pass it inside a Button or link and it lines up
|
|
8
|
+
* without extra styling.
|
|
9
|
+
*
|
|
10
|
+
* <Icon name="github" />
|
|
11
|
+
* <Button icon="github" variant="on-dark-tertiary" href={...}>View on GitHub</Button>
|
|
12
|
+
*
|
|
13
|
+
* Adding a new icon: define the React node in ICONS below, keep the
|
|
14
|
+
* viewBox at 0 0 24 24, and use stroke or fill — never both with
|
|
15
|
+
* different colours, so currentColor stays the single ink.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React from 'react';
|
|
19
|
+
|
|
20
|
+
export const ICONS = {
|
|
21
|
+
/* Official GitHub mark, simplified to a single filled path so it
|
|
22
|
+
reads cleanly at 14–20px. Source: github.com/logos. */
|
|
23
|
+
github: (
|
|
24
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
25
|
+
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.57.1.78-.25.78-.55v-2.16c-3.2.69-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.69-1.27-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.68 1.25 3.34.95.1-.74.4-1.25.73-1.54-2.55-.29-5.23-1.28-5.23-5.69 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 015.8 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.81 1.18 1.83 1.18 3.09 0 4.42-2.69 5.4-5.25 5.68.41.36.78 1.06.78 2.13v3.16c0 .3.21.66.79.55C20.21 21.39 23.5 17.08 23.5 12 23.5 5.65 18.35.5 12 .5z"/>
|
|
26
|
+
</svg>
|
|
27
|
+
),
|
|
28
|
+
|
|
29
|
+
/* API / OpenAPI reference. A stylised open book with a small
|
|
30
|
+
keyhole, matches the Redocusaurus reference mock at
|
|
31
|
+
preview/product-pages/api-reference.html. */
|
|
32
|
+
apiDocs: (
|
|
33
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
34
|
+
<path d="M4 4h6a2 2 0 012 2v14a2 2 0 00-2-2H4z"/>
|
|
35
|
+
<path d="M20 4h-6a2 2 0 00-2 2v14a2 2 0 012-2h6z"/>
|
|
36
|
+
<path d="M8 9h2M8 13h2M16 9h-2M16 13h-2"/>
|
|
37
|
+
</svg>
|
|
38
|
+
),
|
|
39
|
+
|
|
40
|
+
/* Generic right arrow used by ghost CTAs. Wrapped in this set so the
|
|
41
|
+
hero CTA can compose `View on GitHub` + `→` with consistent inline
|
|
42
|
+
metrics on any font-size. */
|
|
43
|
+
arrowRight: (
|
|
44
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
45
|
+
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
46
|
+
<polyline points="13 6 19 12 13 18"/>
|
|
47
|
+
</svg>
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Render a brand icon by name (string key from ICONS). Sized 1em × 1em
|
|
53
|
+
* via inline style; pass extra CSS via `className`. If the caller wants
|
|
54
|
+
* a custom icon, they can render any React node directly — the helper
|
|
55
|
+
* is just a convenience.
|
|
56
|
+
*/
|
|
57
|
+
export default function Icon({name, className, style}) {
|
|
58
|
+
const node = ICONS[name];
|
|
59
|
+
if (!node) return null;
|
|
60
|
+
return React.cloneElement(node, {
|
|
61
|
+
className,
|
|
62
|
+
style: {width: '1em', height: '1em', display: 'inline-block', flexShrink: 0, ...style},
|
|
63
|
+
});
|
|
64
|
+
}
|
package/src/css/brand.css
CHANGED
|
@@ -156,3 +156,157 @@ a:not(.navbar__link):not(.footer__link-item):not(.button) {
|
|
|
156
156
|
a:not(.navbar__link):not(.footer__link-item):not(.button):hover {
|
|
157
157
|
text-decoration-color: var(--c-orange-knvb);
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
/* =========================================================================
|
|
161
|
+
Docs-page styling — mirror preview/product-pages/_docs-shell.css
|
|
162
|
+
|
|
163
|
+
The Docusaurus defaults (Infima) ship a cramped sidebar, Title-Case
|
|
164
|
+
menu items, and an aggressive vertical rhythm. The design-system
|
|
165
|
+
product-page mocks define a calmer treatment: a 280px sidebar, code-
|
|
166
|
+
typeface group labels, a left-border active state on cobalt-50, and a
|
|
167
|
+
900px content column with a 40/26/22 px heading scale at 1.65 body
|
|
168
|
+
line-height. These overrides pull the live docs deploys (shillinq,
|
|
169
|
+
openregister, …) into line with the mocks.
|
|
170
|
+
========================================================================= */
|
|
171
|
+
|
|
172
|
+
/* Sidebar: 280px column with a 1px cobalt-100 right edge. */
|
|
173
|
+
.theme-doc-sidebar-container {
|
|
174
|
+
width: 280px !important;
|
|
175
|
+
border-right: 1px solid var(--c-cobalt-100) !important;
|
|
176
|
+
background: white;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Menu list typography + spacing */
|
|
180
|
+
.menu {
|
|
181
|
+
padding: var(--space-6) var(--space-5) !important;
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
}
|
|
184
|
+
.menu__list .menu__list {
|
|
185
|
+
padding-left: var(--space-3);
|
|
186
|
+
margin-left: 0;
|
|
187
|
+
}
|
|
188
|
+
.menu__list-item {
|
|
189
|
+
margin: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Default link state: idle sentence-case cobalt-700 on white,
|
|
193
|
+
4-px-radius hover tint cobalt-50. */
|
|
194
|
+
.menu__link {
|
|
195
|
+
color: var(--c-cobalt-700);
|
|
196
|
+
padding: 4px 10px;
|
|
197
|
+
border-radius: var(--radius-sm);
|
|
198
|
+
margin-bottom: 2px;
|
|
199
|
+
font-weight: 400;
|
|
200
|
+
line-height: 1.4;
|
|
201
|
+
}
|
|
202
|
+
.menu__link:hover {
|
|
203
|
+
background: var(--c-cobalt-50);
|
|
204
|
+
color: var(--c-cobalt-900);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Active item: 2-px cobalt left border + cobalt-50 fill + cobalt-blue text.
|
|
208
|
+
The padding tweak compensates for the border so the label stays
|
|
209
|
+
aligned with the idle siblings above and below. */
|
|
210
|
+
.menu__link--active,
|
|
211
|
+
.menu__link--active:hover {
|
|
212
|
+
background: var(--c-cobalt-50);
|
|
213
|
+
color: var(--c-blue-cobalt);
|
|
214
|
+
font-weight: 500;
|
|
215
|
+
border-left: 2px solid var(--c-blue-cobalt);
|
|
216
|
+
padding-left: 8px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Category headers (Features / Integrations / Technical groups) get
|
|
220
|
+
the code-typeface uppercase eyebrow treatment, matching the
|
|
221
|
+
`.docs-sidebar .group` rule in the mock. */
|
|
222
|
+
.menu__list-item-collapsible > .menu__link,
|
|
223
|
+
.menu__link--sublist {
|
|
224
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
225
|
+
font-size: 10px;
|
|
226
|
+
text-transform: uppercase;
|
|
227
|
+
letter-spacing: 0.1em;
|
|
228
|
+
color: var(--c-cobalt-400);
|
|
229
|
+
font-weight: 500;
|
|
230
|
+
margin: var(--space-4) 0 var(--space-2);
|
|
231
|
+
}
|
|
232
|
+
.menu__list-item-collapsible > .menu__link:hover {
|
|
233
|
+
background: transparent;
|
|
234
|
+
color: var(--c-cobalt-700);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Caret on collapsible categories — desaturate to cobalt-300. */
|
|
238
|
+
.menu__caret::before,
|
|
239
|
+
.menu__link--sublist-caret::after {
|
|
240
|
+
filter: opacity(0.5);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Content column: 900px max width, generous padding, brand type scale.
|
|
244
|
+
We avoid touching the global .markdown selector (it'd hit MDX pages
|
|
245
|
+
too); scope to the doc-item container instead. */
|
|
246
|
+
.theme-doc-markdown {
|
|
247
|
+
max-width: 900px;
|
|
248
|
+
padding: var(--space-8) var(--space-10);
|
|
249
|
+
}
|
|
250
|
+
.theme-doc-markdown h1 {
|
|
251
|
+
font-size: 40px;
|
|
252
|
+
font-weight: 700;
|
|
253
|
+
letter-spacing: -0.02em;
|
|
254
|
+
line-height: 1.1;
|
|
255
|
+
margin: 0 0 var(--space-4);
|
|
256
|
+
color: var(--c-cobalt-900);
|
|
257
|
+
}
|
|
258
|
+
.theme-doc-markdown h2 {
|
|
259
|
+
font-size: 26px;
|
|
260
|
+
font-weight: 600;
|
|
261
|
+
letter-spacing: -0.01em;
|
|
262
|
+
margin: var(--space-8) 0 var(--space-3);
|
|
263
|
+
color: var(--c-cobalt-900);
|
|
264
|
+
}
|
|
265
|
+
.theme-doc-markdown h3 {
|
|
266
|
+
font-size: 20px;
|
|
267
|
+
font-weight: 600;
|
|
268
|
+
margin: var(--space-6) 0 var(--space-3);
|
|
269
|
+
color: var(--c-cobalt-900);
|
|
270
|
+
}
|
|
271
|
+
.theme-doc-markdown p,
|
|
272
|
+
.theme-doc-markdown ul,
|
|
273
|
+
.theme-doc-markdown ol {
|
|
274
|
+
font-size: 15px;
|
|
275
|
+
line-height: 1.65;
|
|
276
|
+
color: var(--c-cobalt-800);
|
|
277
|
+
margin: 0 0 var(--space-3);
|
|
278
|
+
}
|
|
279
|
+
.theme-doc-markdown ul,
|
|
280
|
+
.theme-doc-markdown ol {
|
|
281
|
+
padding-left: var(--space-5);
|
|
282
|
+
}
|
|
283
|
+
.theme-doc-markdown li {
|
|
284
|
+
margin-bottom: var(--space-2);
|
|
285
|
+
}
|
|
286
|
+
.theme-doc-markdown strong {
|
|
287
|
+
color: var(--c-cobalt-900);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* Code-typeface breadcrumb above the H1. Docusaurus renders this
|
|
291
|
+
via the DocBreadcrumbs component; we restyle in-place. */
|
|
292
|
+
.theme-doc-breadcrumbs {
|
|
293
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
294
|
+
font-size: 11px;
|
|
295
|
+
letter-spacing: 0.08em;
|
|
296
|
+
text-transform: uppercase;
|
|
297
|
+
color: var(--c-cobalt-400);
|
|
298
|
+
margin-bottom: var(--space-4);
|
|
299
|
+
}
|
|
300
|
+
.theme-doc-breadcrumbs .breadcrumbs__link {
|
|
301
|
+
color: var(--c-cobalt-400);
|
|
302
|
+
background: transparent;
|
|
303
|
+
padding: 0;
|
|
304
|
+
}
|
|
305
|
+
.theme-doc-breadcrumbs .breadcrumbs__link:hover {
|
|
306
|
+
color: var(--c-orange-knvb);
|
|
307
|
+
background: transparent;
|
|
308
|
+
}
|
|
309
|
+
.theme-doc-breadcrumbs .breadcrumbs__item--active .breadcrumbs__link {
|
|
310
|
+
color: var(--c-cobalt-700);
|
|
311
|
+
background: transparent;
|
|
312
|
+
}
|
package/src/css/tokens.css
CHANGED
package/src/index.js
CHANGED
|
@@ -21,6 +21,56 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const path = require('path');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the app version that drives the navbar "Stable · v{x.y.z}"
|
|
28
|
+
* pill. Order of precedence:
|
|
29
|
+
*
|
|
30
|
+
* 1. opts.appVersion (explicit override)
|
|
31
|
+
* 2. appinfo/info.xml <version> tag (Nextcloud app convention; every
|
|
32
|
+
* app in the Conduction fleet has one)
|
|
33
|
+
* 3. package.json version (sites without an info.xml — Hydra,
|
|
34
|
+
* design-system itself, conduction-website)
|
|
35
|
+
* 4. undefined (pill is hidden by the Navbar swizzle)
|
|
36
|
+
*
|
|
37
|
+
* The resolver runs at config build-time inside `process.cwd()`, which
|
|
38
|
+
* is the consuming site's repo root. Failures are swallowed silently so
|
|
39
|
+
* a missing info.xml never breaks the site build — the pill just hides.
|
|
40
|
+
*/
|
|
41
|
+
function resolveAppVersion(opts) {
|
|
42
|
+
if (opts.appVersion) return String(opts.appVersion);
|
|
43
|
+
|
|
44
|
+
/* Nextcloud apps: appinfo/info.xml carries the canonical version.
|
|
45
|
+
We avoid pulling in an XML parser for one tag — a non-greedy regex
|
|
46
|
+
against the file content is robust enough for the standard
|
|
47
|
+
`<version>x.y.z</version>` shape the app store mandates. */
|
|
48
|
+
try {
|
|
49
|
+
const infoPath = path.join(process.cwd(), 'appinfo', 'info.xml');
|
|
50
|
+
if (fs.existsSync(infoPath)) {
|
|
51
|
+
const xml = fs.readFileSync(infoPath, 'utf8');
|
|
52
|
+
const m = xml.match(/<version>\s*([^<\s]+)\s*<\/version>/);
|
|
53
|
+
if (m && m[1]) return m[1];
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
/* fall through */
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Non-Nextcloud sites: package.json version. Walks up from cwd in
|
|
60
|
+
case the site builds from a sub-directory; one level deep is
|
|
61
|
+
enough for the conduction-website layout. */
|
|
62
|
+
try {
|
|
63
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
64
|
+
if (fs.existsSync(pkgPath)) {
|
|
65
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
66
|
+
if (pkg.version) return pkg.version;
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
/* fall through */
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
24
74
|
|
|
25
75
|
/**
|
|
26
76
|
* Brand-default i18n block. Nederlands at the URL root, others at /en/, /de/, /fr/.
|
|
@@ -39,8 +89,23 @@ const I18N = {
|
|
|
39
89
|
/**
|
|
40
90
|
* Brand-default navbar. Sites pass their own items[] and logo; the chrome
|
|
41
91
|
* styling (cobalt-on-white, Plex-Mono caption) is locked.
|
|
92
|
+
*
|
|
93
|
+
* The right-side default carries the four chrome items that the brand
|
|
94
|
+
* navbar swizzle (theme/Navbar/index.jsx) renders as icons + pill:
|
|
95
|
+
*
|
|
96
|
+
* versionPill "Stable · v{x.y.z}" reading customFields.appVersion;
|
|
97
|
+
* hidden when no version is available, so non-Nextcloud
|
|
98
|
+
* sites get a clean navbar.
|
|
99
|
+
* apiDocs Book icon + "API Documentation", pointing at /api
|
|
100
|
+
* (Redocusaurus convention). Sites without an OpenAPI
|
|
101
|
+
* spec remove this item from their own config.
|
|
102
|
+
* github GitHub mark, opens the org GitHub by default.
|
|
103
|
+
* localeDropdown Existing locale switcher (nl/en/de/fr).
|
|
104
|
+
*
|
|
105
|
+
* The `opts.repoUrl` plumbing below overrides the GitHub item's href
|
|
106
|
+
* to point at the specific app repo when the site provides it.
|
|
42
107
|
*/
|
|
43
|
-
const baseNavbar = (siteName) => ({
|
|
108
|
+
const baseNavbar = (siteName, repoUrl) => ({
|
|
44
109
|
title: siteName,
|
|
45
110
|
logo: {
|
|
46
111
|
alt: `${siteName} avatar`,
|
|
@@ -48,8 +113,10 @@ const baseNavbar = (siteName) => ({
|
|
|
48
113
|
srcDark: 'img/logo-dark.svg',
|
|
49
114
|
},
|
|
50
115
|
items: [
|
|
116
|
+
{ type: 'versionPill', position: 'right' },
|
|
117
|
+
{ type: 'apiDocs', position: 'right' },
|
|
118
|
+
{ type: 'github', href: repoUrl || 'https://github.com/ConductionNL', position: 'right' },
|
|
51
119
|
{ type: 'localeDropdown', position: 'right' },
|
|
52
|
-
{ href: 'https://github.com/ConductionNL', label: 'GitHub', position: 'right' },
|
|
53
120
|
],
|
|
54
121
|
});
|
|
55
122
|
|
|
@@ -128,6 +195,11 @@ const baseFooter = () => ({
|
|
|
128
195
|
* minigames (default true; set false to drop the brand canal-footer's
|
|
129
196
|
* boat-sinking + kade-cyclist mini-games on product pages while
|
|
130
197
|
* keeping the static skyline + canal decoration),
|
|
198
|
+
* appVersion (explicit override for the navbar "Stable · v{x.y.z}"
|
|
199
|
+
* pill; defaults to appinfo/info.xml then package.json — see
|
|
200
|
+
* resolveAppVersion above),
|
|
201
|
+
* repoUrl (target of the navbar GitHub icon; defaults to the
|
|
202
|
+
* ConductionNL org root),
|
|
131
203
|
* customCss[] (appended to brand.css), plugins[], presets,
|
|
132
204
|
* i18n (overrides defaults)
|
|
133
205
|
*/
|
|
@@ -142,6 +214,11 @@ function createConfig(opts) {
|
|
|
142
214
|
getClientModules(), so customCss carries site-specific CSS only. */
|
|
143
215
|
const customCss = opts.customCss || [];
|
|
144
216
|
|
|
217
|
+
/* App version drives the navbar "Stable · v{x.y.z}" pill (resolved
|
|
218
|
+
once at config time). Pulled from appinfo/info.xml, then
|
|
219
|
+
package.json — see resolveAppVersion above. */
|
|
220
|
+
const appVersion = resolveAppVersion(opts);
|
|
221
|
+
|
|
145
222
|
return {
|
|
146
223
|
title: opts.title,
|
|
147
224
|
tagline: opts.tagline || '',
|
|
@@ -153,6 +230,17 @@ function createConfig(opts) {
|
|
|
153
230
|
organizationName: opts.organizationName || 'ConductionNL',
|
|
154
231
|
projectName: opts.projectName || 'design-system',
|
|
155
232
|
|
|
233
|
+
/* customFields is the canonical Docusaurus channel for build-time
|
|
234
|
+
data the theme reads at runtime via useDocusaurusContext().
|
|
235
|
+
appVersion drives the navbar "Stable · v{x.y.z}" pill; sites
|
|
236
|
+
extend this by passing their own customFields in opts. */
|
|
237
|
+
customFields: Object.assign(
|
|
238
|
+
{
|
|
239
|
+
...(appVersion && {appVersion}),
|
|
240
|
+
},
|
|
241
|
+
opts.customFields || {}
|
|
242
|
+
),
|
|
243
|
+
|
|
156
244
|
onBrokenLinks: 'warn',
|
|
157
245
|
onBrokenMarkdownLinks: 'warn',
|
|
158
246
|
|
|
@@ -201,7 +289,7 @@ function createConfig(opts) {
|
|
|
201
289
|
disableSwitch: false,
|
|
202
290
|
respectPrefersColorScheme: true,
|
|
203
291
|
},
|
|
204
|
-
navbar: Object.assign(baseNavbar(opts.title), opts.navbar || {}),
|
|
292
|
+
navbar: Object.assign(baseNavbar(opts.title, opts.repoUrl), opts.navbar || {}),
|
|
205
293
|
/* Per-property fallback so a site can override one slice of the
|
|
206
294
|
footer (e.g. just `links`) and inherit the rest from the brand.
|
|
207
295
|
Previously `opts.footer` replaced the whole footer wholesale,
|
|
@@ -316,6 +316,15 @@ export default function Footer() {
|
|
|
316
316
|
Open-source apps for <span className="next-blue">Nextcloud</span>. Built and
|
|
317
317
|
maintained by Conduction in Amsterdam, released under EUPL-1.2.
|
|
318
318
|
</p>
|
|
319
|
+
{/*
|
|
320
|
+
Brand citation. The producer chain stays dot-separated
|
|
321
|
+
(Conduction · sub-brand · partner) and connects to
|
|
322
|
+
Nextcloud through a vermillion-red heart — the "loves"
|
|
323
|
+
relationship is between the producer stack and the
|
|
324
|
+
platform it ships on. Nextcloud is a link to
|
|
325
|
+
nextcloud.com so visitors can verify the platform
|
|
326
|
+
upstream in one click.
|
|
327
|
+
*/}
|
|
319
328
|
<div className="triad">
|
|
320
329
|
<span>
|
|
321
330
|
<span className="h"></span>
|
|
@@ -326,7 +335,17 @@ export default function Footer() {
|
|
|
326
335
|
.map((b, i) => (
|
|
327
336
|
<React.Fragment key={i}> · {b.wordmark}</React.Fragment>
|
|
328
337
|
))}
|
|
329
|
-
{' '}
|
|
338
|
+
{' '}
|
|
339
|
+
<svg className="heart" viewBox="0 0 24 24" fill="currentColor" aria-label="loves" role="img">
|
|
340
|
+
<path d="M12 21s-6.7-4.35-9.2-8.4C.8 9.2 2 5.5 5.2 4.7c2-.5 3.8.4 4.8 1.9 1-1.5 2.8-2.4 4.8-1.9 3.2.8 4.4 4.5 2.4 7.9C18.7 16.65 12 21 12 21z"/>
|
|
341
|
+
</svg>
|
|
342
|
+
{' '}
|
|
343
|
+
<a
|
|
344
|
+
href="https://nextcloud.com"
|
|
345
|
+
target="_blank"
|
|
346
|
+
rel="noopener noreferrer"
|
|
347
|
+
className="next-blue"
|
|
348
|
+
>Nextcloud</a>
|
|
330
349
|
</span>
|
|
331
350
|
</div>
|
|
332
351
|
<div className="socials">
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Brand Navbar swizzle.
|
|
3
3
|
*
|
|
4
4
|
* Replaces Docusaurus's default Infima navbar with the Conduction
|
|
5
|
-
* top-navbar pattern: brand wordmark + nav links +
|
|
5
|
+
* top-navbar pattern: brand wordmark + nav links + locale + chrome.
|
|
6
6
|
* Navigation items come from themeConfig.navbar.items (configured by
|
|
7
7
|
* the consuming site); the chrome (typography, spacing, brand citation)
|
|
8
8
|
* stays locked in this component.
|
|
@@ -18,24 +18,41 @@
|
|
|
18
18
|
* sub-brand section keeps you in that section. Outside a sub-brand
|
|
19
19
|
* section it goes to the site root.
|
|
20
20
|
*
|
|
21
|
+
* Item types the brand navbar recognises (sites declare them in
|
|
22
|
+
* docusaurus.config.js → themeConfig.navbar.items):
|
|
23
|
+
*
|
|
24
|
+
* { type: 'doc', label, to } internal doc link
|
|
25
|
+
* { type: 'link', label, to | href } internal/external link
|
|
26
|
+
* { type: 'localeDropdown' } Docusaurus locale switcher
|
|
27
|
+
* { type: 'github', href } icon-only GitHub mark
|
|
28
|
+
* { type: 'apiDocs', label?, to } icon + "API Documentation"
|
|
29
|
+
* { type: 'versionPill', prefix? } "Stable · v{x.y.z}" pill
|
|
30
|
+
* reads customFields.appVersion;
|
|
31
|
+
* hidden when no version
|
|
32
|
+
*
|
|
33
|
+
* The pill prefix defaults to "Stable" but can be overridden per site
|
|
34
|
+
* (e.g. prefix="Beta" while on a pre-1.0 release line).
|
|
35
|
+
*
|
|
21
36
|
* Mirrors preview/components/top-navbar.html in the design-system kit.
|
|
22
37
|
*/
|
|
23
38
|
|
|
24
39
|
import React from 'react';
|
|
25
40
|
import Link from '@docusaurus/Link';
|
|
26
41
|
import {useLocation} from '@docusaurus/router';
|
|
42
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
27
43
|
import {useThemeConfig} from '@docusaurus/theme-common';
|
|
28
44
|
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
|
29
|
-
import {brandFor} from '../brand.jsx';
|
|
45
|
+
import {brandFor, productWordmark} from '../brand.jsx';
|
|
46
|
+
import {ICONS} from '../../components/primitives/icons';
|
|
30
47
|
import styles from './styles.module.css';
|
|
31
48
|
|
|
32
49
|
/**
|
|
33
|
-
* Render a single navbar item. The brand
|
|
34
|
-
* subset of Docusaurus item types
|
|
35
|
-
*
|
|
36
|
-
*
|
|
50
|
+
* Render a single navbar item. The brand navbar supports a small
|
|
51
|
+
* subset of Docusaurus item types plus the three brand-specific types
|
|
52
|
+
* (github, apiDocs, versionPill); everything else falls back to a
|
|
53
|
+
* plain link for forward-compatibility.
|
|
37
54
|
*/
|
|
38
|
-
function NavItem({item, location}) {
|
|
55
|
+
function NavItem({item, location, appVersion}) {
|
|
39
56
|
if (item.type === 'localeDropdown') {
|
|
40
57
|
return (
|
|
41
58
|
<div className={styles.localeWrapper}>
|
|
@@ -44,6 +61,61 @@ function NavItem({item, location}) {
|
|
|
44
61
|
);
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
/* GitHub: icon-only link with an accessible label. The aria-label
|
|
65
|
+
gives screen-readers + browser tooltips a name without rendering
|
|
66
|
+
a visible text label in the navbar. */
|
|
67
|
+
if (item.type === 'github') {
|
|
68
|
+
return (
|
|
69
|
+
<a
|
|
70
|
+
href={item.href || 'https://github.com/ConductionNL'}
|
|
71
|
+
className={styles.iconLink}
|
|
72
|
+
target="_blank"
|
|
73
|
+
rel="noopener noreferrer"
|
|
74
|
+
aria-label={item['aria-label'] || 'GitHub repository'}
|
|
75
|
+
title="GitHub"
|
|
76
|
+
>
|
|
77
|
+
<span className={styles.iconGlyph} aria-hidden="true">{ICONS.github}</span>
|
|
78
|
+
</a>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* API Documentation: icon + label link. Target defaults to /api
|
|
83
|
+
(the Redocusaurus mount point used by every Conduction docs site).
|
|
84
|
+
Sites can override via `to` or `href`. */
|
|
85
|
+
if (item.type === 'apiDocs') {
|
|
86
|
+
const label = item.label || 'API Documentation';
|
|
87
|
+
const to = item.to || '/api';
|
|
88
|
+
const href = item.href;
|
|
89
|
+
const isActive = !href && (location?.pathname === to ||
|
|
90
|
+
location?.pathname?.startsWith(to + '/'));
|
|
91
|
+
const className = `${styles.link} ${styles.iconLabelLink} ${isActive ? styles.linkActive : ''}`;
|
|
92
|
+
const content = (
|
|
93
|
+
<>
|
|
94
|
+
<span className={styles.iconGlyph} aria-hidden="true">{ICONS.apiDocs}</span>
|
|
95
|
+
{label}
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
if (href) {
|
|
99
|
+
return <a href={href} className={className} target={href.startsWith('http') ? '_blank' : undefined} rel={href.startsWith('http') ? 'noopener noreferrer' : undefined}>{content}</a>;
|
|
100
|
+
}
|
|
101
|
+
return <Link to={to} className={className}>{content}</Link>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Version pill: code-typeface "Stable · v{version}" chip. Source is
|
|
105
|
+
customFields.appVersion (set by createConfig() from appinfo/info.xml
|
|
106
|
+
or package.json). Hidden when no version is available so sites
|
|
107
|
+
without an app version (Hydra, design-system itself) get a clean
|
|
108
|
+
navbar instead of an empty pill. */
|
|
109
|
+
if (item.type === 'versionPill') {
|
|
110
|
+
if (!appVersion) return null;
|
|
111
|
+
const prefix = item.prefix || 'Stable';
|
|
112
|
+
return (
|
|
113
|
+
<span className={styles.versionPill} title={`${prefix} · v${appVersion}`}>
|
|
114
|
+
{prefix} · v{appVersion}
|
|
115
|
+
</span>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
47
119
|
/* External link, no React-router prefetch */
|
|
48
120
|
if (item.href && !item.to) {
|
|
49
121
|
const isCta = item.cta === true;
|
|
@@ -83,20 +155,52 @@ function NavItem({item, location}) {
|
|
|
83
155
|
|
|
84
156
|
export default function Navbar() {
|
|
85
157
|
const {navbar} = useThemeConfig();
|
|
158
|
+
const {siteConfig} = useDocusaurusContext();
|
|
159
|
+
const appVersion = siteConfig?.customFields?.appVersion;
|
|
86
160
|
const location = useLocation();
|
|
87
161
|
const items = navbar.items || [];
|
|
88
162
|
const brand = brandFor(location.pathname, navbar.title);
|
|
89
|
-
|
|
163
|
+
|
|
164
|
+
/* Wordmark resolution order:
|
|
165
|
+
1. ConNext / Common Ground sub-brand → custom JSX (Con<Next>, …)
|
|
166
|
+
2. Conduction product app (Open*, Docu*, My*, …) → prefix-light
|
|
167
|
+
treatment: cobalt-400 prefix + blue-cobalt rest, matching the
|
|
168
|
+
preview/apps.html convention.
|
|
169
|
+
3. Plain text title (single-word wordmark or unrecognised prefix). */
|
|
170
|
+
let wordmark;
|
|
171
|
+
if (brand) {
|
|
172
|
+
wordmark = brand.wordmark;
|
|
173
|
+
} else {
|
|
174
|
+
const split = productWordmark(navbar.title, navbar.brandPrefix);
|
|
175
|
+
wordmark = split ? (
|
|
176
|
+
<>
|
|
177
|
+
<span className={styles.wordmarkPrefix}>{split.prefix}</span>{split.rest}
|
|
178
|
+
</>
|
|
179
|
+
) : navbar.title;
|
|
180
|
+
}
|
|
181
|
+
|
|
90
182
|
/* Path-match: keep the visitor in the sub-brand section on logo click.
|
|
91
183
|
Title-match (the site's primary brand IS a sub-brand): logo goes to
|
|
92
184
|
site root since the section IS the site. */
|
|
93
185
|
const homeHref = brand?.source === 'path' ? brand.home : '/';
|
|
94
186
|
|
|
187
|
+
/* App icon. The brand rule is that every product navbar shows the
|
|
188
|
+
app's hex-glyph next to the wordmark. The icon is sourced from
|
|
189
|
+
navbar.logo (createConfig defaults it to img/logo.svg, which every
|
|
190
|
+
Conduction docs site ships under static/img/). Sites can opt the
|
|
191
|
+
icon out by passing `logo: null` in their navbar config. */
|
|
192
|
+
const logoSrc = navbar.logo?.src;
|
|
193
|
+
const logoAlt = navbar.logo?.alt || `${navbar.title} avatar`;
|
|
194
|
+
|
|
95
195
|
/* Split into "left links" (regular nav) and "right CTAs" (locale,
|
|
96
|
-
external links, install button
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
196
|
+
external links, install button, GitHub icon, version pill).
|
|
197
|
+
Items default to the left unless they explicitly carry
|
|
198
|
+
position="right" — but the three brand-specific item types
|
|
199
|
+
(github, apiDocs, versionPill) live on the right by convention,
|
|
200
|
+
mirroring the docs-shell mock. */
|
|
201
|
+
const RIGHT_TYPES = new Set(['localeDropdown', 'github', 'apiDocs', 'versionPill']);
|
|
202
|
+
const leftItems = items.filter(i => i.position !== 'right' && !RIGHT_TYPES.has(i.type));
|
|
203
|
+
const rightItems = items.filter(i => i.position === 'right' || RIGHT_TYPES.has(i.type));
|
|
100
204
|
|
|
101
205
|
return (
|
|
102
206
|
/* `navbar` (Docusaurus's framework class) is added alongside the
|
|
@@ -108,17 +212,26 @@ export default function Navbar() {
|
|
|
108
212
|
<nav className={`navbar ${styles.nav}`} role="navigation" aria-label="Main">
|
|
109
213
|
<div className={styles.left}>
|
|
110
214
|
<Link to={homeHref} className={styles.wordmark}>
|
|
111
|
-
{
|
|
215
|
+
{logoSrc && (
|
|
216
|
+
<img
|
|
217
|
+
src={logoSrc}
|
|
218
|
+
alt={logoAlt}
|
|
219
|
+
className={styles.wordmarkIcon}
|
|
220
|
+
width="32"
|
|
221
|
+
height="32"
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
<span className={styles.wordmarkText}>{wordmark}</span>
|
|
112
225
|
</Link>
|
|
113
226
|
<div className={styles.links}>
|
|
114
227
|
{leftItems.map((item, i) => (
|
|
115
|
-
<NavItem key={i} item={item} location={location} />
|
|
228
|
+
<NavItem key={i} item={item} location={location} appVersion={appVersion} />
|
|
116
229
|
))}
|
|
117
230
|
</div>
|
|
118
231
|
</div>
|
|
119
232
|
<div className={styles.ctas}>
|
|
120
233
|
{rightItems.map((item, i) => (
|
|
121
|
-
<NavItem key={i} item={item} location={location} />
|
|
234
|
+
<NavItem key={i} item={item} location={location} appVersion={appVersion} />
|
|
122
235
|
))}
|
|
123
236
|
</div>
|
|
124
237
|
</nav>
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.wordmark {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 10px;
|
|
27
30
|
font-size: 22px;
|
|
28
31
|
font-weight: 700;
|
|
29
32
|
letter-spacing: -0.02em;
|
|
@@ -32,6 +35,33 @@
|
|
|
32
35
|
}
|
|
33
36
|
.wordmark:hover { color: var(--c-blue-cobalt); text-decoration: none; }
|
|
34
37
|
|
|
38
|
+
/* App-glyph hex next to the wordmark. Every product navbar shows the
|
|
39
|
+
app's icon (sourced from navbar.logo); sized to match the wordmark's
|
|
40
|
+
cap height so the two land on the same baseline. */
|
|
41
|
+
.wordmarkIcon {
|
|
42
|
+
width: 32px;
|
|
43
|
+
height: 32px;
|
|
44
|
+
display: block;
|
|
45
|
+
flex-shrink: 0;
|
|
46
|
+
object-fit: contain;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Wordmark text wrapper; gives us a single hook for tweaks like
|
|
50
|
+
line-height alignment without touching the parent flex container. */
|
|
51
|
+
.wordmarkText {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
line-height: 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* "Light" prefix syllable for product wordmarks. Mirrors
|
|
57
|
+
preview/apps.html .lockup .word .light: cobalt-400 + regular weight,
|
|
58
|
+
so "OpenRegister" reads as muted-"Open" + bold-"Register" and the
|
|
59
|
+
eye lands on the noun, not the brand prefix. */
|
|
60
|
+
.wordmarkPrefix {
|
|
61
|
+
color: var(--c-cobalt-400);
|
|
62
|
+
font-weight: 400;
|
|
63
|
+
}
|
|
64
|
+
|
|
35
65
|
.links {
|
|
36
66
|
display: flex;
|
|
37
67
|
gap: 28px;
|
|
@@ -100,6 +130,69 @@
|
|
|
100
130
|
color: var(--c-orange-knvb);
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
/* Inline SVG slot used by `github` and `apiDocs` items. The icon
|
|
134
|
+
inherits the link's font-size + colour so a single `currentColor`
|
|
135
|
+
path stays on brand whether the link is active, hovered, or idle. */
|
|
136
|
+
.iconGlyph {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
line-height: 1;
|
|
141
|
+
}
|
|
142
|
+
.iconGlyph :global(svg) {
|
|
143
|
+
width: 1em;
|
|
144
|
+
height: 1em;
|
|
145
|
+
display: block;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Icon-only navbar link. Used by the github item; no visible label,
|
|
149
|
+
the aria-label gives it a name. Slightly larger icon (18px) than
|
|
150
|
+
text links so it reads as a fixed glyph rather than letterform. */
|
|
151
|
+
.iconLink {
|
|
152
|
+
display: inline-flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
width: 34px;
|
|
156
|
+
height: 34px;
|
|
157
|
+
border-radius: var(--radius-sm);
|
|
158
|
+
color: var(--c-cobalt-700);
|
|
159
|
+
font-size: 18px;
|
|
160
|
+
text-decoration: none;
|
|
161
|
+
transition: color 160ms ease, background 160ms ease;
|
|
162
|
+
}
|
|
163
|
+
.iconLink:hover {
|
|
164
|
+
color: var(--c-blue-cobalt);
|
|
165
|
+
background: var(--c-cobalt-50);
|
|
166
|
+
text-decoration: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Icon + label link. Used by the apiDocs item. Same typography as
|
|
170
|
+
plain .link but with an inline icon before the label, gap matches
|
|
171
|
+
the brand button-icon spacing. */
|
|
172
|
+
.iconLabelLink {
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 8px;
|
|
176
|
+
font-size: 14px;
|
|
177
|
+
}
|
|
178
|
+
.iconLabelLink .iconGlyph {
|
|
179
|
+
font-size: 16px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Stable-version pill. Code typeface, cobalt-50 fill, cobalt-400 text,
|
|
183
|
+
pill-radius. Source is customFields.appVersion via createConfig();
|
|
184
|
+
when undefined the pill isn't rendered at all (see NavItem). */
|
|
185
|
+
.versionPill {
|
|
186
|
+
font-family: var(--conduction-typography-font-family-code);
|
|
187
|
+
font-size: 11px;
|
|
188
|
+
letter-spacing: 0.04em;
|
|
189
|
+
color: var(--c-cobalt-400);
|
|
190
|
+
background: var(--c-cobalt-50);
|
|
191
|
+
border-radius: var(--radius-pill);
|
|
192
|
+
padding: 4px 10px;
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
}
|
|
195
|
+
|
|
103
196
|
/* Responsive collapse, simplified for v1.
|
|
104
197
|
TODO: hamburger menu like the design-system mock. */
|
|
105
198
|
@media (max-width: 900px) {
|
package/src/theme/brand.jsx
CHANGED
|
@@ -61,3 +61,50 @@ export function brandFor(pathname, title) {
|
|
|
61
61
|
}
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Conduction product-app wordmark patterns. The brand convention,
|
|
67
|
+
* codified in preview/apps.html, is to render the prefix syllable in
|
|
68
|
+
* cobalt-400 / regular weight and the rest in blue-cobalt / bold:
|
|
69
|
+
*
|
|
70
|
+
* <span class="light">Open</span>Register
|
|
71
|
+
* <span class="light">Docu</span>Desk
|
|
72
|
+
* <span class="light">My</span>Dash
|
|
73
|
+
*
|
|
74
|
+
* This list covers the prefixes used across the Conduction fleet.
|
|
75
|
+
* Sites whose wordmark doesn't start with one of these (e.g. Shillinq,
|
|
76
|
+
* Decidesk) keep the whole wordmark in blue-cobalt unless they pass
|
|
77
|
+
* an explicit `navbar.brandPrefix` to override.
|
|
78
|
+
*/
|
|
79
|
+
const PRODUCT_PREFIXES = [
|
|
80
|
+
'OpenAI', /* must precede 'Open' so 'OpenAI Bridge' splits as OpenAI/Bridge */
|
|
81
|
+
'Open',
|
|
82
|
+
'Docu',
|
|
83
|
+
'My',
|
|
84
|
+
'Pipe',
|
|
85
|
+
'Pro',
|
|
86
|
+
'Decid',
|
|
87
|
+
'Schol',
|
|
88
|
+
'Larping',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Split a wordmark into (prefix, rest) so the prefix can render in the
|
|
93
|
+
* cobalt-400 "light" treatment. `brandPrefix` (optional) overrides
|
|
94
|
+
* auto-detection — sites with a non-standard wordmark pass it
|
|
95
|
+
* explicitly. Returns `null` when no split applies (single-word
|
|
96
|
+
* wordmarks or unrecognised prefixes); callers should render the
|
|
97
|
+
* wordmark as-is in that case.
|
|
98
|
+
*/
|
|
99
|
+
export function productWordmark(title, brandPrefix) {
|
|
100
|
+
if (!title) return null;
|
|
101
|
+
if (brandPrefix && title.startsWith(brandPrefix) && title.length > brandPrefix.length) {
|
|
102
|
+
return {prefix: brandPrefix, rest: title.slice(brandPrefix.length)};
|
|
103
|
+
}
|
|
104
|
+
for (const p of PRODUCT_PREFIXES) {
|
|
105
|
+
if (title.startsWith(p) && title.length > p.length) {
|
|
106
|
+
return {prefix: p, rest: title.slice(p.length)};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
@@ -190,6 +190,25 @@
|
|
|
190
190
|
display: inline-block; margin-right: 6px;
|
|
191
191
|
vertical-align: middle;
|
|
192
192
|
}
|
|
193
|
+
/* "Conduction ♥ Nextcloud" citation: a vermillion-red heart between
|
|
194
|
+
the producer chain and the Nextcloud platform link. Sized to the
|
|
195
|
+
surrounding 11px caption with a small inline lift so the heart
|
|
196
|
+
centres on the cap-height. */
|
|
197
|
+
.canal-footer .brand .triad .heart {
|
|
198
|
+
width: 11px; height: 11px;
|
|
199
|
+
color: var(--c-red-vermillion);
|
|
200
|
+
display: inline-block;
|
|
201
|
+
vertical-align: -1px;
|
|
202
|
+
margin: 0 2px;
|
|
203
|
+
}
|
|
204
|
+
.canal-footer .brand .triad a.next-blue {
|
|
205
|
+
color: var(--c-nextcloud-blue);
|
|
206
|
+
text-decoration: none;
|
|
207
|
+
transition: color 140ms ease;
|
|
208
|
+
}
|
|
209
|
+
.canal-footer .brand .triad a.next-blue:hover {
|
|
210
|
+
color: var(--c-nextcloud-cyan);
|
|
211
|
+
}
|
|
193
212
|
.canal-footer .brand .socials {
|
|
194
213
|
display: flex; gap: 10px;
|
|
195
214
|
margin-top: 18px;
|