@conduction/docusaurus-preset 2.7.0-beta.3 → 2.7.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/docusaurus-preset",
3
- "version": "2.7.0-beta.3",
3
+ "version": "2.7.0",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
@@ -147,7 +147,6 @@ export default function DetailHero({
147
147
  variant="primary"
148
148
  tone={primaryCta.tone}
149
149
  href={primaryCta.href}
150
- icon={primaryCta.icon}
151
150
  >
152
151
  {primaryCta.label}
153
152
  </Button>
@@ -157,26 +156,11 @@ export default function DetailHero({
157
156
  variant="secondary"
158
157
  tone={secondaryCta.tone}
159
158
  href={secondaryCta.href}
160
- icon={secondaryCta.icon}
161
159
  >
162
160
  {secondaryCta.label}
163
161
  </Button>
164
162
  )}
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
- )}
163
+ {tertiaryCta && <Button variant="ghost" href={tertiaryCta.href}>{tertiaryCta.label} →</Button>}
180
164
  </div>
181
165
  )}
182
166
  </div>
@@ -1,45 +1,33 @@
1
1
  /**
2
2
  * <Button />
3
3
  *
4
- * Brand variants used across hero, cta-banner, top-navbar, game-modal,
5
- * and per-page CTA rows.
4
+ * Three brand variants (primary / secondary / ghost) used across hero,
5
+ * cta-banner, top-navbar, game-modal, 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: 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
12
+ * - primary: cobalt fill, white text
13
+ * - secondary: white bg, cobalt-200 border, cobalt text
14
+ * - ghost: plain text + arrow
15
+ * - on-dark: primary variant inverted for use inside cobalt CTA panels
19
16
  *
20
17
  * Tone (optional): override the primary/secondary fill colour. The
21
18
  * brand default is cobalt; product pages with an orange-accent identity
22
19
  * (e.g. mydash) pass `tone="orange"` to flip the primary CTA to
23
20
  * KNVB-orange while keeping the rest of the brand chrome intact.
24
21
  *
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
- *
29
22
  * Usage:
30
23
  *
31
24
  * <Button href="/apps">Install</Button>
32
25
  * <Button variant="secondary" href="/partners">Get a demo</Button>
33
26
  * <Button variant="ghost" href="https://github.com/...">View on GitHub →</Button>
34
- * <Button
35
- * variant="on-dark-tertiary"
36
- * icon="github"
37
- * href="https://github.com/ConductionNL/shillinq"
38
- * >View on GitHub</Button>
27
+ * <Button tone="orange" href="/install">Install from app store</Button>
39
28
  */
40
29
 
41
30
  import React from 'react';
42
- import {ICONS} from './icons';
43
31
  import styles from './Button.module.css';
44
32
 
45
33
  export default function Button({
@@ -47,7 +35,6 @@ export default function Button({
47
35
  tone,
48
36
  href,
49
37
  size = 'md',
50
- icon,
51
38
  className,
52
39
  children,
53
40
  ...rest
@@ -60,18 +47,8 @@ export default function Button({
60
47
  className,
61
48
  ].filter(Boolean).join(' ');
62
49
 
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
-
73
50
  if (href) {
74
- return <a href={href} className={composed} {...rest}>{body}</a>;
51
+ return <a href={href} className={composed} {...rest}>{children}</a>;
75
52
  }
76
- return <button type="button" className={composed} {...rest}>{body}</button>;
53
+ return <button type="button" className={composed} {...rest}>{children}</button>;
77
54
  }
@@ -17,22 +17,6 @@
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
-
36
20
  .s-sm { padding: 9px 14px; font-size: 13px; }
37
21
  .s-md { padding: 13px 22px; font-size: 15px; }
38
22
  .s-lg { padding: 16px 28px; font-size: 16px; }
@@ -100,24 +84,6 @@
100
84
  color: white;
101
85
  }
102
86
 
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
-
121
87
  .v-on-dark-primary :global(.next-blue),
122
88
  .v-primary :global(.next-blue) { color: var(--c-nextcloud-cyan); }
123
89
 
package/src/css/brand.css CHANGED
@@ -156,157 +156,3 @@ 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/index.js CHANGED
@@ -21,56 +21,6 @@
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
- }
74
24
 
75
25
  /**
76
26
  * Brand-default i18n block. Nederlands at the URL root, others at /en/, /de/, /fr/.
@@ -89,40 +39,17 @@ const I18N = {
89
39
  /**
90
40
  * Brand-default navbar. Sites pass their own items[] and logo; the chrome
91
41
  * 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.
107
42
  */
108
- const baseNavbar = (siteName, repoUrl) => ({
43
+ const baseNavbar = (siteName) => ({
109
44
  title: siteName,
110
45
  logo: {
111
46
  alt: `${siteName} avatar`,
112
47
  src: 'img/logo.svg',
113
48
  srcDark: 'img/logo-dark.svg',
114
49
  },
115
- /* `custom-*` prefix is the Docusaurus convention for theme-defined
116
- navbar item types — items prefixed this way bypass the strict Joi
117
- schema validator in @docusaurus/theme-classic, so the brand Navbar
118
- swizzle can dispatch on them without registering each shape with
119
- core. The swizzle accepts both `custom-github` and the bare
120
- `github` (etc.) for forward-compat. */
121
50
  items: [
122
- { type: 'custom-versionPill', position: 'right' },
123
- { type: 'custom-apiDocs', position: 'right' },
124
- { type: 'custom-github', href: repoUrl || 'https://github.com/ConductionNL', position: 'right' },
125
51
  { type: 'localeDropdown', position: 'right' },
52
+ { href: 'https://github.com/ConductionNL', label: 'GitHub', position: 'right' },
126
53
  ],
127
54
  });
128
55
 
@@ -201,11 +128,6 @@ const baseFooter = () => ({
201
128
  * minigames (default true; set false to drop the brand canal-footer's
202
129
  * boat-sinking + kade-cyclist mini-games on product pages while
203
130
  * keeping the static skyline + canal decoration),
204
- * appVersion (explicit override for the navbar "Stable · v{x.y.z}"
205
- * pill; defaults to appinfo/info.xml then package.json — see
206
- * resolveAppVersion above),
207
- * repoUrl (target of the navbar GitHub icon; defaults to the
208
- * ConductionNL org root),
209
131
  * customCss[] (appended to brand.css), plugins[], presets,
210
132
  * i18n (overrides defaults)
211
133
  */
@@ -220,11 +142,6 @@ function createConfig(opts) {
220
142
  getClientModules(), so customCss carries site-specific CSS only. */
221
143
  const customCss = opts.customCss || [];
222
144
 
223
- /* App version drives the navbar "Stable · v{x.y.z}" pill (resolved
224
- once at config time). Pulled from appinfo/info.xml, then
225
- package.json — see resolveAppVersion above. */
226
- const appVersion = resolveAppVersion(opts);
227
-
228
145
  return {
229
146
  title: opts.title,
230
147
  tagline: opts.tagline || '',
@@ -236,17 +153,6 @@ function createConfig(opts) {
236
153
  organizationName: opts.organizationName || 'ConductionNL',
237
154
  projectName: opts.projectName || 'design-system',
238
155
 
239
- /* customFields is the canonical Docusaurus channel for build-time
240
- data the theme reads at runtime via useDocusaurusContext().
241
- appVersion drives the navbar "Stable · v{x.y.z}" pill; sites
242
- extend this by passing their own customFields in opts. */
243
- customFields: Object.assign(
244
- {
245
- ...(appVersion && {appVersion}),
246
- },
247
- opts.customFields || {}
248
- ),
249
-
250
156
  onBrokenLinks: 'warn',
251
157
  onBrokenMarkdownLinks: 'warn',
252
158
 
@@ -295,7 +201,7 @@ function createConfig(opts) {
295
201
  disableSwitch: false,
296
202
  respectPrefersColorScheme: true,
297
203
  },
298
- navbar: Object.assign(baseNavbar(opts.title, opts.repoUrl), opts.navbar || {}),
204
+ navbar: Object.assign(baseNavbar(opts.title), opts.navbar || {}),
299
205
  /* Per-property fallback so a site can override one slice of the
300
206
  footer (e.g. just `links`) and inherit the rest from the brand.
301
207
  Previously `opts.footer` replaced the whole footer wholesale,
@@ -329,6 +235,24 @@ function createConfig(opts) {
329
235
  built jointly with a partner (mydash + Sendent
330
236
  is the first case). */
331
237
  footerBrand: opts.footerBrand || null,
238
+ /* Legal-bar links (Privacy / Terms / ISO) plus the two ISO
239
+ 9001 + 27001 certification badges on the right side of the
240
+ canal-footer. Default keeps prior behaviour (pages live at
241
+ /privacy, /terms, /iso on docs.conduction.nl + www.conduction.nl).
242
+ Consumer sites that don't ship those pages can opt out per
243
+ slot to silence broken-link warnings:
244
+
245
+ legalLinks: {
246
+ privacy: false, // hide the Privacy link
247
+ terms: false, // hide the Terms link
248
+ iso: false, // hide the ISO link AND the cert badges
249
+ // (badges follow iso link by default)
250
+ // any slot can also take a string for an external URL:
251
+ privacy: 'https://docs.conduction.nl/privacy',
252
+ // certs default-follow iso, override here:
253
+ isoCertifications: true | false,
254
+ } */
255
+ legalLinks: opts.legalLinks || {},
332
256
  },
333
257
  opts.themeConfig || {}
334
258
  ),
@@ -52,6 +52,32 @@ export default function Footer() {
52
52
  ../../index.js for the option semantics. */
53
53
  const minigamesOn = themeConfig.minigames !== false;
54
54
  const footerBrand = themeConfig.footerBrand || null;
55
+ /* legalLinks: opt-in/out of the Privacy / Terms / ISO links inside
56
+ the legal-bar, and the two ISO 9001/27001 certification badges on
57
+ the right. Defaults preserve current behaviour for sites that ship
58
+ those pages (docs.conduction.nl, www.conduction.nl). Consumer
59
+ sites that don't ship them can pass { privacy: false, terms: false,
60
+ iso: false, isoCertifications: false } via createConfig opts to
61
+ silence broken-link warnings. Each link slot also accepts a string
62
+ to override the destination (e.g. an external URL pointing at the
63
+ canonical Conduction legal page). */
64
+ const legalLinks = themeConfig.legalLinks || {};
65
+ const legalLink = (key, fallback) => {
66
+ if (legalLinks[key] === false) return null;
67
+ if (typeof legalLinks[key] === 'string' && legalLinks[key]) return legalLinks[key];
68
+ return fallback;
69
+ };
70
+ const privacyTo = legalLink('privacy', '/privacy');
71
+ const termsTo = legalLink('terms', '/terms');
72
+ const isoTo = legalLink('iso', '/iso');
73
+ /* ISO certification badges (the two 9001 + 27001 pills on the right
74
+ of the legal-bar) default to following the iso link's visibility —
75
+ no point claiming certifications when the linked detail page
76
+ doesn't ship. Sites can force a state with
77
+ legalLinks.isoCertifications: true | false. */
78
+ const showIsoCerts = legalLinks.isoCertifications !== undefined
79
+ ? Boolean(legalLinks.isoCertifications)
80
+ : Boolean(isoTo);
55
81
 
56
82
  const location = useLocation();
57
83
  /* Brand switch follows the pathname: /connext or /commonground sections
@@ -316,15 +342,6 @@ export default function Footer() {
316
342
  Open-source apps for <span className="next-blue">Nextcloud</span>. Built and
317
343
  maintained by Conduction in Amsterdam, released under EUPL-1.2.
318
344
  </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
- */}
328
345
  <div className="triad">
329
346
  <span>
330
347
  <span className="h"></span>
@@ -335,17 +352,7 @@ export default function Footer() {
335
352
  .map((b, i) => (
336
353
  <React.Fragment key={i}> · {b.wordmark}</React.Fragment>
337
354
  ))}
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>
355
+ {' '}· <span className="next-blue">Nextcloud</span>
349
356
  </span>
350
357
  </div>
351
358
  <div className="socials">
@@ -397,24 +404,28 @@ export default function Footer() {
397
404
  <div className="legal-bar">
398
405
  <div className="left">
399
406
  <span>{copyright}</span>
400
- <span className="legal-links">
401
- <Link to="/privacy">Privacy</Link>
402
- <span className="sep">·</span>
403
- <Link to="/terms">Terms</Link>
404
- <span className="sep">·</span>
405
- <Link to="/iso">ISO</Link>
406
- </span>
407
- </div>
408
- <div className="right">
409
- <Link to="/iso" className="iso-badge" aria-label="ISO 9001:2015 certified, see details">
410
- <span className="iso-mark">ISO</span>
411
- <span className="iso-num">9001:2015</span>
412
- </Link>
413
- <Link to="/iso" className="iso-badge" aria-label="ISO 27001:2022 certified, see details">
414
- <span className="iso-mark">ISO</span>
415
- <span className="iso-num">27001:2022</span>
416
- </Link>
407
+ {(privacyTo || termsTo || isoTo) && (
408
+ <span className="legal-links">
409
+ {privacyTo && <Link to={privacyTo}>Privacy</Link>}
410
+ {privacyTo && termsTo && <span className="sep">·</span>}
411
+ {termsTo && <Link to={termsTo}>Terms</Link>}
412
+ {(privacyTo || termsTo) && isoTo && <span className="sep">·</span>}
413
+ {isoTo && <Link to={isoTo}>ISO</Link>}
414
+ </span>
415
+ )}
417
416
  </div>
417
+ {showIsoCerts && (
418
+ <div className="right">
419
+ <Link to={isoTo || '/iso'} className="iso-badge" aria-label="ISO 9001:2015 certified, see details">
420
+ <span className="iso-mark">ISO</span>
421
+ <span className="iso-num">9001:2015</span>
422
+ </Link>
423
+ <Link to={isoTo || '/iso'} className="iso-badge" aria-label="ISO 27001:2022 certified, see details">
424
+ <span className="iso-mark">ISO</span>
425
+ <span className="iso-num">27001:2022</span>
426
+ </Link>
427
+ </div>
428
+ )}
418
429
  </div>
419
430
  )}
420
431
  </div>
@@ -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 + locale + chrome.
5
+ * top-navbar pattern: brand wordmark + nav links + Partners + Install.
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,65 +18,24 @@
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: 'custom-github', href } icon-only GitHub mark
28
- * { type: 'custom-apiDocs', label?, to } icon + "API Documentation"
29
- * { type: 'custom-versionPill', prefix? } "Stable · v{x.y.z}" pill
30
- * reads customFields.appVersion;
31
- * hidden when no version
32
- *
33
- * The `custom-` prefix is required so Docusaurus's themeConfig schema
34
- * validator passes (`@docusaurus/theme-classic` rejects unknown bare
35
- * type names). The swizzle below accepts both the prefixed and the
36
- * bare names so 2.7.0-beta.1 sites that wired the bare names keep
37
- * working after the upgrade.
38
- *
39
- * The pill prefix defaults to "Stable" but can be overridden per site
40
- * (e.g. prefix="Beta" while on a pre-1.0 release line).
41
- *
42
21
  * Mirrors preview/components/top-navbar.html in the design-system kit.
43
22
  */
44
23
 
45
24
  import React from 'react';
46
25
  import Link from '@docusaurus/Link';
47
26
  import {useLocation} from '@docusaurus/router';
48
- import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
49
27
  import {useThemeConfig} from '@docusaurus/theme-common';
50
28
  import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
51
- import {brandFor, productWordmark} from '../brand.jsx';
52
- import {ICONS} from '../../components/primitives/icons';
29
+ import {brandFor} from '../brand.jsx';
53
30
  import styles from './styles.module.css';
54
31
 
55
32
  /**
56
- * Brand-specific navbar item types live under the `custom-` prefix
57
- * because Docusaurus's themeConfig validator (Joi schema in
58
- * @docusaurus/theme-classic) rejects unknown top-level types. The
59
- * `custom-` namespace is the documented escape hatch: items prefixed
60
- * with `custom-` bypass schema validation and are passed through to
61
- * the theme as-is. The brand Navbar then dispatches on them below.
62
- *
63
- * Sites may also use the bare names (`github`, `apiDocs`,
64
- * `versionPill`) — they render identically here but Docusaurus will
65
- * reject the config at load time. Accept both forms so the migration
66
- * from 2.7.0-beta.1 to .beta.2 doesn't break sites that already
67
- * configured the bare names.
68
- */
69
- function typeIs(item, kind) {
70
- return item.type === kind || item.type === 'custom-' + kind;
71
- }
72
-
73
- /**
74
- * Render a single navbar item. The brand navbar supports a small
75
- * subset of Docusaurus item types plus the three brand-specific types
76
- * (github, apiDocs, versionPill); everything else falls back to a
77
- * plain link for forward-compatibility.
33
+ * Render a single navbar item. The brand top-navbar supports a small
34
+ * subset of Docusaurus item types (link, locale dropdown, optional
35
+ * primary CTA via items[].cta = true). Everything else falls back to
36
+ * a plain link for forward-compatibility.
78
37
  */
79
- function NavItem({item, location, appVersion}) {
38
+ function NavItem({item, location}) {
80
39
  if (item.type === 'localeDropdown') {
81
40
  return (
82
41
  <div className={styles.localeWrapper}>
@@ -85,61 +44,6 @@ function NavItem({item, location, appVersion}) {
85
44
  );
86
45
  }
87
46
 
88
- /* GitHub: icon-only link with an accessible label. The aria-label
89
- gives screen-readers + browser tooltips a name without rendering
90
- a visible text label in the navbar. */
91
- if (typeIs(item, 'github')) {
92
- return (
93
- <a
94
- href={item.href || 'https://github.com/ConductionNL'}
95
- className={styles.iconLink}
96
- target="_blank"
97
- rel="noopener noreferrer"
98
- aria-label={item['aria-label'] || 'GitHub repository'}
99
- title="GitHub"
100
- >
101
- <span className={styles.iconGlyph} aria-hidden="true">{ICONS.github}</span>
102
- </a>
103
- );
104
- }
105
-
106
- /* API Documentation: icon + label link. Target defaults to /api
107
- (the Redocusaurus mount point used by every Conduction docs site).
108
- Sites can override via `to` or `href`. */
109
- if (typeIs(item, 'apiDocs')) {
110
- const label = item.label || 'API Documentation';
111
- const to = item.to || '/api';
112
- const href = item.href;
113
- const isActive = !href && (location?.pathname === to ||
114
- location?.pathname?.startsWith(to + '/'));
115
- const className = `${styles.link} ${styles.iconLabelLink} ${isActive ? styles.linkActive : ''}`;
116
- const content = (
117
- <>
118
- <span className={styles.iconGlyph} aria-hidden="true">{ICONS.apiDocs}</span>
119
- {label}
120
- </>
121
- );
122
- if (href) {
123
- return <a href={href} className={className} target={href.startsWith('http') ? '_blank' : undefined} rel={href.startsWith('http') ? 'noopener noreferrer' : undefined}>{content}</a>;
124
- }
125
- return <Link to={to} className={className}>{content}</Link>;
126
- }
127
-
128
- /* Version pill: code-typeface "Stable · v{version}" chip. Source is
129
- customFields.appVersion (set by createConfig() from appinfo/info.xml
130
- or package.json). Hidden when no version is available so sites
131
- without an app version (Hydra, design-system itself) get a clean
132
- navbar instead of an empty pill. */
133
- if (typeIs(item, 'versionPill')) {
134
- if (!appVersion) return null;
135
- const prefix = item.prefix || 'Stable';
136
- return (
137
- <span className={styles.versionPill} title={`${prefix} · v${appVersion}`}>
138
- {prefix} · v{appVersion}
139
- </span>
140
- );
141
- }
142
-
143
47
  /* External link, no React-router prefetch */
144
48
  if (item.href && !item.to) {
145
49
  const isCta = item.cta === true;
@@ -179,57 +83,20 @@ function NavItem({item, location, appVersion}) {
179
83
 
180
84
  export default function Navbar() {
181
85
  const {navbar} = useThemeConfig();
182
- const {siteConfig} = useDocusaurusContext();
183
- const appVersion = siteConfig?.customFields?.appVersion;
184
86
  const location = useLocation();
185
87
  const items = navbar.items || [];
186
88
  const brand = brandFor(location.pathname, navbar.title);
187
-
188
- /* Wordmark resolution order:
189
- 1. ConNext / Common Ground sub-brand → custom JSX (Con<Next>, …)
190
- 2. Conduction product app (Open*, Docu*, My*, …) → prefix-light
191
- treatment: cobalt-400 prefix + blue-cobalt rest, matching the
192
- preview/apps.html convention.
193
- 3. Plain text title (single-word wordmark or unrecognised prefix). */
194
- let wordmark;
195
- if (brand) {
196
- wordmark = brand.wordmark;
197
- } else {
198
- const split = productWordmark(navbar.title, navbar.brandPrefix);
199
- wordmark = split ? (
200
- <>
201
- <span className={styles.wordmarkPrefix}>{split.prefix}</span>{split.rest}
202
- </>
203
- ) : navbar.title;
204
- }
205
-
89
+ const wordmark = brand ? brand.wordmark : navbar.title;
206
90
  /* Path-match: keep the visitor in the sub-brand section on logo click.
207
91
  Title-match (the site's primary brand IS a sub-brand): logo goes to
208
92
  site root since the section IS the site. */
209
93
  const homeHref = brand?.source === 'path' ? brand.home : '/';
210
94
 
211
- /* App icon. The brand rule is that every product navbar shows the
212
- app's hex-glyph next to the wordmark. The icon is sourced from
213
- navbar.logo (createConfig defaults it to img/logo.svg, which every
214
- Conduction docs site ships under static/img/). Sites can opt the
215
- icon out by passing `logo: null` in their navbar config. */
216
- const logoSrc = navbar.logo?.src;
217
- const logoAlt = navbar.logo?.alt || `${navbar.title} avatar`;
218
-
219
95
  /* Split into "left links" (regular nav) and "right CTAs" (locale,
220
- external links, install button, GitHub icon, version pill).
221
- Items default to the left unless they explicitly carry
222
- position="right" but the three brand-specific item types
223
- (github, apiDocs, versionPill) live on the right by convention,
224
- mirroring the docs-shell mock. */
225
- const RIGHT_TYPES = new Set([
226
- 'localeDropdown',
227
- 'github', 'custom-github',
228
- 'apiDocs', 'custom-apiDocs',
229
- 'versionPill', 'custom-versionPill',
230
- ]);
231
- const leftItems = items.filter(i => i.position !== 'right' && !RIGHT_TYPES.has(i.type));
232
- const rightItems = items.filter(i => i.position === 'right' || RIGHT_TYPES.has(i.type));
96
+ external links, install button). The brand pattern groups them
97
+ this way; consumers control order via item.position. */
98
+ const leftItems = items.filter(i => i.position !== 'right' && i.type !== 'localeDropdown');
99
+ const rightItems = items.filter(i => i.position === 'right' || i.type === 'localeDropdown');
233
100
 
234
101
  return (
235
102
  /* `navbar` (Docusaurus's framework class) is added alongside the
@@ -241,26 +108,17 @@ export default function Navbar() {
241
108
  <nav className={`navbar ${styles.nav}`} role="navigation" aria-label="Main">
242
109
  <div className={styles.left}>
243
110
  <Link to={homeHref} className={styles.wordmark}>
244
- {logoSrc && (
245
- <img
246
- src={logoSrc}
247
- alt={logoAlt}
248
- className={styles.wordmarkIcon}
249
- width="32"
250
- height="32"
251
- />
252
- )}
253
- <span className={styles.wordmarkText}>{wordmark}</span>
111
+ {wordmark}
254
112
  </Link>
255
113
  <div className={styles.links}>
256
114
  {leftItems.map((item, i) => (
257
- <NavItem key={i} item={item} location={location} appVersion={appVersion} />
115
+ <NavItem key={i} item={item} location={location} />
258
116
  ))}
259
117
  </div>
260
118
  </div>
261
119
  <div className={styles.ctas}>
262
120
  {rightItems.map((item, i) => (
263
- <NavItem key={i} item={item} location={location} appVersion={appVersion} />
121
+ <NavItem key={i} item={item} location={location} />
264
122
  ))}
265
123
  </div>
266
124
  </nav>
@@ -24,9 +24,6 @@
24
24
  }
25
25
 
26
26
  .wordmark {
27
- display: inline-flex;
28
- align-items: center;
29
- gap: 10px;
30
27
  font-size: 22px;
31
28
  font-weight: 700;
32
29
  letter-spacing: -0.02em;
@@ -35,33 +32,6 @@
35
32
  }
36
33
  .wordmark:hover { color: var(--c-blue-cobalt); text-decoration: none; }
37
34
 
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
-
65
35
  .links {
66
36
  display: flex;
67
37
  gap: 28px;
@@ -130,69 +100,6 @@
130
100
  color: var(--c-orange-knvb);
131
101
  }
132
102
 
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
-
196
103
  /* Responsive collapse, simplified for v1.
197
104
  TODO: hamburger menu like the design-system mock. */
198
105
  @media (max-width: 900px) {
@@ -61,50 +61,3 @@ 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,25 +190,6 @@
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
- }
212
193
  .canal-footer .brand .socials {
213
194
  display: flex; gap: 10px;
214
195
  margin-top: 18px;
@@ -1,64 +0,0 @@
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
- }