@conduction/docusaurus-preset 1.7.0 → 2.0.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": "1.7.0",
3
+ "version": "2.0.0",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Partner card from preview/components/partner-cards.html.
5
5
  *
6
- * Three tiers:
7
- * - partner (default, white panel + cobalt-50 tier pill)
8
- * - certified (white panel + gold tier pill)
9
- * - strategic (cobalt-fill inverse + white tier pill)
6
+ * Three tiers (low → high):
7
+ * - host (default, white panel + cobalt-50 tier pill)
8
+ * ships our apps, no SLA with Conduction.
9
+ * - service (white panel + gold tier pill) — SLA, third-line
10
+ * support, roadmap input.
11
+ * - certified (cobalt-fill inverse + white tier pill) — trained,
12
+ * joint roadmap, tender-eligible.
10
13
  *
11
14
  * Visual structure:
12
15
  * ┌─────────────────────────────┐
@@ -25,7 +28,7 @@
25
28
  * <PartnerGrid>
26
29
  * <PartnerCard
27
30
  * href="/partners/yard"
28
- * tier="partner"
31
+ * tier="host"
29
32
  * name="YARD"
30
33
  * logo="/img/partners/yard.png"
31
34
  * summary={<>Digital design- en development-bureau uit Utrecht...</>}
@@ -39,7 +42,7 @@ import React from 'react';
39
42
  import HexBullet from '../primitives/HexBullet';
40
43
  import styles from './PartnerCard.module.css';
41
44
 
42
- const TIER_LABELS = {partner: 'Partner', certified: 'Certified', strategic: 'Strategic'};
45
+ const TIER_LABELS = {host: 'Host', service: 'Service', certified: 'Certified'};
43
46
 
44
47
  export function PartnerGrid({columns = 3, children, className}) {
45
48
  const composed = [styles.grid, styles['cols-' + columns], className].filter(Boolean).join(' ');
@@ -49,7 +52,7 @@ export function PartnerGrid({columns = 3, children, className}) {
49
52
  export default function PartnerCard({
50
53
  variant = 'full',
51
54
  href,
52
- tier = 'partner',
55
+ tier = 'host',
53
56
  name,
54
57
  logo,
55
58
  logoAlt,
@@ -92,7 +95,7 @@ export default function PartnerCard({
92
95
  className,
93
96
  ].filter(Boolean).join(' ');
94
97
  const Tag = href ? 'a' : 'div';
95
- const bulletColor = tier === 'strategic' ? 'var(--c-orange-knvb)' : 'var(--c-blue-cobalt)';
98
+ const bulletColor = tier === 'certified' ? 'var(--c-orange-knvb)' : 'var(--c-blue-cobalt)';
96
99
 
97
100
  return (
98
101
  <Tag href={href} className={composed}>
@@ -49,9 +49,16 @@
49
49
  color: var(--c-cobalt-700);
50
50
  }
51
51
 
52
- .tier-certified .tier { background: var(--c-gold-500); color: var(--c-cobalt-900); }
53
- .tier-strategic { background: var(--c-blue-cobalt); color: white; border-color: var(--c-blue-cobalt); }
54
- .tier-strategic .tier { background: white; color: var(--c-blue-cobalt); }
52
+ /* Service tier: gold pill on a white card. */
53
+ .tier-service .tier { background: var(--c-gold-500); color: var(--c-cobalt-900); }
54
+
55
+ /* Certified tier: cobalt-fill inverse card. The compound .card.tier-certified
56
+ selector is deliberate — it lifts specificity above the plain .card rule
57
+ so the cobalt fill reliably wins over .card { background: white } even
58
+ after CSS-module bundlers reshuffle declaration order. Same for the
59
+ nested colour rules below. */
60
+ .card.tier-certified { background: var(--c-blue-cobalt); color: white; border-color: var(--c-blue-cobalt); }
61
+ .tier-certified .tier { background: white; color: var(--c-blue-cobalt); }
55
62
 
56
63
  .avatar {
57
64
  display: inline-flex;
@@ -73,12 +80,12 @@
73
80
  }
74
81
 
75
82
  .name { font-size: 18px; font-weight: 700; color: var(--c-cobalt-900); }
76
- .tier-strategic .name { color: white; }
83
+ .tier-certified .name { color: white; }
77
84
 
78
85
  .summary { font-size: 14px; line-height: 1.55; color: var(--c-cobalt-700); }
79
86
  .summary :global(.next-blue) { color: var(--c-nextcloud-blue); }
80
- .tier-strategic .summary { color: rgba(255, 255, 255, 0.78); }
81
- .tier-strategic .summary :global(.next-blue) { color: var(--c-nextcloud-cyan); }
87
+ .tier-certified .summary { color: rgba(255, 255, 255, 0.78); }
88
+ .tier-certified .summary :global(.next-blue) { color: var(--c-nextcloud-cyan); }
82
89
 
83
90
  .apps {
84
91
  margin-top: auto;
@@ -88,7 +95,7 @@
88
95
  gap: 6px;
89
96
  flex-wrap: wrap;
90
97
  }
91
- .tier-strategic .apps { border-top-color: rgba(255, 255, 255, 0.18); }
98
+ .tier-certified .apps { border-top-color: rgba(255, 255, 255, 0.18); }
92
99
 
93
100
  .appPill {
94
101
  display: inline-flex;
@@ -101,7 +108,7 @@
101
108
  font-size: 12px;
102
109
  font-family: var(--conduction-typography-font-family-code);
103
110
  }
104
- .tier-strategic .appPill { background: rgba(255, 255, 255, 0.12); color: white; }
111
+ .tier-certified .appPill { background: rgba(255, 255, 255, 0.12); color: white; }
105
112
 
106
113
  /* Become-a-partner CTA tile */
107
114
  .become {
@@ -182,8 +189,8 @@
182
189
  max-height: 28px;
183
190
  object-fit: contain;
184
191
  }
185
- .other.tier-certified .miniAvatar { box-shadow: inset 0 0 0 2px var(--c-gold-500); }
186
- .other.tier-strategic .miniAvatar { box-shadow: inset 0 0 0 2px var(--c-orange-knvb); }
192
+ .other.tier-service .miniAvatar { box-shadow: inset 0 0 0 2px var(--c-gold-500); }
193
+ .other.tier-certified .miniAvatar { box-shadow: inset 0 0 0 2px var(--c-orange-knvb); }
187
194
 
188
195
  .otherName {
189
196
  font-size: 15px;
@@ -5,27 +5,32 @@
5
5
  * <PartnerGrid/>, <PartnerCard/>, and an optional <BecomePartner/> CTA.
6
6
  *
7
7
  * Facets are auto-derived from the partners array:
8
- * - tier: Strategic / Certified / Partner (only shown when ≥1 partner)
9
- * - app: union of every partners[].apps[] value, sorted by count
8
+ * - tier: Certified / Service / Host (only shown when ≥1 partner)
9
+ * - offering: union of every partners[].apps[] AND .solutions[]
10
+ * value, sorted by count. Solutions get pretty labels;
11
+ * unknown slugs fall back to the slug itself.
10
12
  *
11
13
  * Filter logic: a partner is shown when it matches every active facet.
12
14
  * Within a facet, multiple selections are OR-ed (any-of). Across
13
- * facets, selections are AND-ed (all-of).
15
+ * facets, selections are AND-ed (all-of). The offering facet matches
16
+ * against the union of the partner's apps and solutions, so picking
17
+ * "Woo" shows every partner that ships the Woo solution.
14
18
  *
15
19
  * Usage in MDX:
16
20
  *
17
21
  * <PartnerDirectory
18
22
  * partners={[
19
- * {href: '/partners/acato', tier: 'strategic', name: 'Acato',
23
+ * {href: '/partners/acato', tier: 'certified', name: 'Acato',
20
24
  * logo: '/img/partners/acato.png',
21
- * summary: <>Nederlandse open-source specialist…</>,
22
- * apps: ['OpenRegister', 'OpenCatalogi', 'DocuDesk']},
25
+ * summary: <>Digital agency uit Almere…</>,
26
+ * apps: ['OpenCatalogi', 'OpenRegister', 'OpenConnector'],
27
+ * solutions: ['woo']},
23
28
  * …
24
29
  * ]}
25
30
  * becomePartner={{
26
31
  * href: '#become-a-partner',
27
32
  * title: 'Ship Conduction to your customers.',
28
- * body: 'Join as Partner, Certified, or Strategic.',
33
+ * body: 'Three tiers: Host, Service, Certified.',
29
34
  * ctaLabel: 'Apply below',
30
35
  * }}
31
36
  * />
@@ -36,8 +41,21 @@ import FacetedFilters from '../FacetedFilters/FacetedFilters';
36
41
  import PartnerCard, {PartnerGrid, BecomePartner} from '../PartnerCard/PartnerCard';
37
42
  import styles from './PartnerDirectory.module.css';
38
43
 
39
- const TIER_LABELS = {partner: 'Partner', certified: 'Certified', strategic: 'Strategic'};
40
- const TIER_ORDER = ['strategic', 'certified', 'partner'];
44
+ const TIER_LABELS = {host: 'Host', service: 'Service', certified: 'Certified'};
45
+ const TIER_ORDER = ['certified', 'service', 'host'];
46
+
47
+ // Pretty labels for solution slugs that the catalog uses. Anything not
48
+ // in this map falls back to the raw slug, so a new solution keeps
49
+ // working without an explicit registration.
50
+ const SOLUTION_LABELS = {
51
+ woo: 'Woo',
52
+ archief: 'Archief',
53
+ 'software-catalog': 'Software catalog',
54
+ 'mkb-workspace': 'MKB workspace',
55
+ zaakafhandeling: 'Zaakafhandeling',
56
+ 'legacy-erp': 'Legacy ERP',
57
+ 'nen-7510': 'NEN 7510',
58
+ };
41
59
 
42
60
  function deriveFacets(partners) {
43
61
  const tierItems = TIER_ORDER
@@ -48,19 +66,29 @@ function deriveFacets(partners) {
48
66
  }))
49
67
  .filter(item => item.count > 0);
50
68
 
51
- const appCounts = {};
69
+ // Combined offering facet: apps + solutions. Each entry records its
70
+ // kind so the filter can match against the right partner field.
71
+ // Items are stored under a stable value namespaced by kind
72
+ // ('app:OpenCatalogi', 'solution:woo') so an app and a solution
73
+ // with the same name never collide.
74
+ const offeringCounts = new Map(); // value → {label, count, kind, raw}
75
+ const bump = (kind, raw, label) => {
76
+ const value = `${kind}:${raw}`;
77
+ const existing = offeringCounts.get(value);
78
+ if (existing) existing.count += 1;
79
+ else offeringCounts.set(value, {value, label, count: 1, kind, raw});
80
+ };
52
81
  for (const p of partners) {
53
- for (const a of p.apps || []) {
54
- appCounts[a] = (appCounts[a] || 0) + 1;
55
- }
82
+ for (const a of p.apps || []) bump('app', a, a);
83
+ for (const s of p.solutions || []) bump('solution', s, SOLUTION_LABELS[s] || s);
56
84
  }
57
- const appItems = Object.entries(appCounts)
58
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
59
- .map(([value, count]) => ({value, label: value, count}));
85
+ const offeringItems = Array.from(offeringCounts.values())
86
+ .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label))
87
+ .map(({value, label, count}) => ({value, label, count}));
60
88
 
61
89
  const facets = [];
62
- if (tierItems.length > 0) facets.push({key: 'tier', label: 'Partner tier', items: tierItems});
63
- if (appItems.length > 0) facets.push({key: 'app', label: 'Apps & services', items: appItems});
90
+ if (tierItems.length > 0) facets.push({key: 'tier', label: 'Partner tier', items: tierItems});
91
+ if (offeringItems.length > 0) facets.push({key: 'offering', label: 'Apps and solutions', items: offeringItems});
64
92
  return facets;
65
93
  }
66
94
 
@@ -68,10 +96,17 @@ function partnerMatches(partner, selected) {
68
96
  const tierFilter = selected.tier || [];
69
97
  if (tierFilter.length > 0 && !tierFilter.includes(partner.tier)) return false;
70
98
 
71
- const appFilter = selected.app || [];
72
- if (appFilter.length > 0) {
99
+ const offeringFilter = selected.offering || [];
100
+ if (offeringFilter.length > 0) {
73
101
  const partnerApps = partner.apps || [];
74
- if (!appFilter.some(a => partnerApps.includes(a))) return false;
102
+ const partnerSolutions = partner.solutions || [];
103
+ const ok = offeringFilter.some(value => {
104
+ const [kind, raw] = value.split(':');
105
+ if (kind === 'app') return partnerApps.includes(raw);
106
+ if (kind === 'solution') return partnerSolutions.includes(raw);
107
+ return false;
108
+ });
109
+ if (!ok) return false;
75
110
  }
76
111
 
77
112
  return true;
@@ -4,10 +4,10 @@
4
4
  * Narrow info panel for the partner detail page right column. Three
5
5
  * stacked sections:
6
6
  * 1. Tier badge. Pulls the same colour treatment as <PartnerCard>
7
- * (Partner = cobalt-50 pill, Certified = gold pill
8
- * with a small gold Conduction avatar to read as a
9
- * Conduction-issued credential, Strategic = white
10
- * pill on a cobalt-fill card).
7
+ * (Host = cobalt-50 pill, Service = gold pill,
8
+ * Certified = white pill on a cobalt-fill card,
9
+ * with a small gold Conduction avatar to read as
10
+ * a Conduction-issued credential).
11
11
  * 2. Apps supported. Chip row from partner.apps.
12
12
  * 3. Solutions shipped. Chip row from solutionsBySlugs(partner.solutions),
13
13
  * each linking to its /solutions/<slug> page.
@@ -40,18 +40,18 @@ import React from 'react';
40
40
  import styles from './PartnerSidecard.module.css';
41
41
 
42
42
  const TIER_LABELS = {
43
- partner: 'Partner',
43
+ host: 'Host',
44
+ service: 'Service',
44
45
  certified: 'Certified',
45
- strategic: 'Strategic',
46
46
  };
47
47
 
48
48
  const TIER_BLURB = {
49
- partner:
50
- 'Partner-tier teams have shipped Conduction in production at one or two clients. Listed in the directory, on the community Slack, joining quarterly office hours.',
49
+ host:
50
+ 'Host partners ship our open-source apps to their customers. They sell, host, and resell Nextcloud-supported variants through their own channels. No SLA with Conduction, no direct line to our support, not eligible to respond to tenders with our products.',
51
+ service:
52
+ 'Service partners hold an SLA with Conduction. Their team can call us directly for third-line support, with named contacts and input on bug priority and feature requests. They run rollouts; we back them up.',
51
53
  certified:
52
- 'Certified per app by Conduction. Joint go-to-market on tenders and a higher SLA on shared customers. Carries the Conduction credential.',
53
- strategic:
54
- 'Strategic partner with shared roadmap commitments and joint engineering. By invitation. Co-sale on government tenders, co-branded hosting offerings.',
54
+ 'Certified partners are trained, audited, and on the joint roadmap with Conduction. Eligible to co-sale on public tenders with our products, and the Conduction-credential mark on their own collateral.',
55
55
  };
56
56
 
57
57
  export default function PartnerSidecard({
@@ -61,7 +61,7 @@ export default function PartnerSidecard({
61
61
  className,
62
62
  }) {
63
63
  if (!partner) return null;
64
- const tier = partner.tier || 'partner';
64
+ const tier = partner.tier || 'host';
65
65
  const tierLabel = TIER_LABELS[tier] || tier;
66
66
  const composed = [styles.rail, styles[`tier-${tier}`], className].filter(Boolean).join(' ');
67
67
 
@@ -77,7 +77,7 @@ export default function PartnerSidecard({
77
77
  height="44"
78
78
  />
79
79
  )}
80
- <div className={styles.tierLabel}>{tier === 'partner' ? 'Partner' : `${tierLabel} partner`}</div>
80
+ <div className={styles.tierLabel}>{`${tierLabel} partner`}</div>
81
81
  <p className={styles.tierBlurb}>{TIER_BLURB[tier]}</p>
82
82
  </div>
83
83
 
@@ -25,15 +25,17 @@
25
25
  box-shadow: var(--shadow-1);
26
26
  }
27
27
 
28
- .tier-strategic .tierCard {
28
+ .tier-certified .tierCard {
29
+ /* Cobalt-fill card carries the Conduction-issued credential treatment
30
+ for top-tier partners. */
29
31
  background: var(--c-blue-cobalt);
30
32
  border-color: var(--c-blue-cobalt);
31
33
  color: white;
32
34
  }
33
35
 
34
- .tier-certified .tierCard {
35
- /* white card, but a hairline gold stripe + the gold avatar reads it as
36
- "Conduction-issued credential" rather than just another Partner. */
36
+ .tier-service .tierCard {
37
+ /* white card, but a hairline gold stripe reads it as a service-level
38
+ SLA partner rather than a plain Host. */
37
39
  border-top: 4px solid var(--c-gold-500);
38
40
  }
39
41
 
@@ -53,8 +55,8 @@
53
55
  margin-bottom: var(--space-2);
54
56
  font-weight: 600;
55
57
  }
56
- .tier-strategic .tierLabel { color: rgba(255, 255, 255, 0.78); }
57
- .tier-certified .tierLabel { color: var(--c-gold-600, #8B6914); }
58
+ .tier-certified .tierLabel { color: rgba(255, 255, 255, 0.78); }
59
+ .tier-service .tierLabel { color: var(--c-gold-600, #8B6914); }
58
60
 
59
61
  .tierBlurb {
60
62
  font-size: 13px;
@@ -62,7 +64,7 @@
62
64
  color: var(--c-cobalt-700);
63
65
  margin: 0;
64
66
  }
65
- .tier-strategic .tierBlurb { color: rgba(255, 255, 255, 0.85); }
67
+ .tier-certified .tierBlurb { color: rgba(255, 255, 255, 0.85); }
66
68
 
67
69
  /* ---------- Generic section ---------- */
68
70