@conduction/docusaurus-preset 1.7.0 → 2.1.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 +1 -1
- package/src/components/PartnerCard/PartnerCard.jsx +49 -18
- package/src/components/PartnerCard/PartnerCard.module.css +50 -10
- package/src/components/PartnerDirectory/PartnerDirectory.jsx +56 -21
- package/src/components/PartnerSidecard/PartnerSidecard.jsx +23 -17
- package/src/components/PartnerSidecard/PartnerSidecard.module.css +18 -7
- package/src/data/apps-registry.js +23 -0
package/package.json
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Partner card from preview/components/partner-cards.html.
|
|
5
5
|
*
|
|
6
|
-
* Three tiers:
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
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="
|
|
31
|
+
* tier="host"
|
|
29
32
|
* name="YARD"
|
|
30
33
|
* logo="/img/partners/yard.png"
|
|
31
34
|
* summary={<>Digital design- en development-bureau uit Utrecht...</>}
|
|
@@ -37,9 +40,10 @@
|
|
|
37
40
|
|
|
38
41
|
import React from 'react';
|
|
39
42
|
import HexBullet from '../primitives/HexBullet';
|
|
43
|
+
import {appHrefByName} from '../../data/apps-registry';
|
|
40
44
|
import styles from './PartnerCard.module.css';
|
|
41
45
|
|
|
42
|
-
const TIER_LABELS = {
|
|
46
|
+
const TIER_LABELS = {host: 'Host', service: 'Service', certified: 'Certified'};
|
|
43
47
|
|
|
44
48
|
export function PartnerGrid({columns = 3, children, className}) {
|
|
45
49
|
const composed = [styles.grid, styles['cols-' + columns], className].filter(Boolean).join(' ');
|
|
@@ -49,7 +53,7 @@ export function PartnerGrid({columns = 3, children, className}) {
|
|
|
49
53
|
export default function PartnerCard({
|
|
50
54
|
variant = 'full',
|
|
51
55
|
href,
|
|
52
|
-
tier = '
|
|
56
|
+
tier = 'host',
|
|
53
57
|
name,
|
|
54
58
|
logo,
|
|
55
59
|
logoAlt,
|
|
@@ -85,17 +89,30 @@ export default function PartnerCard({
|
|
|
85
89
|
);
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
/* Default: full partner card
|
|
92
|
+
/* Default: full partner card.
|
|
93
|
+
The card wrapper is always a <div> rather than an <a>, even when
|
|
94
|
+
a partner detail href is set, so the per-app pills can be
|
|
95
|
+
individually clickable links to /apps/<slug>. The card-wide click
|
|
96
|
+
target is rendered as a stretched-link overlay (.cardLink) that
|
|
97
|
+
covers the whole card via position:absolute; nested <a> pills
|
|
98
|
+
sit on top via z-index, so clicks reach the pill instead of the
|
|
99
|
+
overlay when they land on a pill, and reach the overlay otherwise. */
|
|
89
100
|
const composed = [
|
|
90
101
|
styles.card,
|
|
91
102
|
styles['tier-' + tier],
|
|
92
103
|
className,
|
|
93
104
|
].filter(Boolean).join(' ');
|
|
94
|
-
const
|
|
95
|
-
const bulletColor = tier === 'strategic' ? 'var(--c-orange-knvb)' : 'var(--c-blue-cobalt)';
|
|
105
|
+
const bulletColor = tier === 'certified' ? 'var(--c-orange-knvb)' : 'var(--c-blue-cobalt)';
|
|
96
106
|
|
|
97
107
|
return (
|
|
98
|
-
<
|
|
108
|
+
<div className={composed}>
|
|
109
|
+
{href && (
|
|
110
|
+
<a
|
|
111
|
+
href={href}
|
|
112
|
+
className={styles.cardLink}
|
|
113
|
+
aria-label={name ? `${name} partner page` : 'Partner page'}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
99
116
|
<span className={styles.tier}>{TIER_LABELS[tier] || tier}</span>
|
|
100
117
|
{logo && (
|
|
101
118
|
<div className={styles.avatar}>
|
|
@@ -106,15 +123,29 @@ export default function PartnerCard({
|
|
|
106
123
|
{summary && <div className={styles.summary}>{summary}</div>}
|
|
107
124
|
{apps.length > 0 && (
|
|
108
125
|
<div className={styles.apps}>
|
|
109
|
-
{apps.map((app, i) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
{apps.map((app, i) => {
|
|
127
|
+
const appUrl = appHrefByName(app);
|
|
128
|
+
const inner = (
|
|
129
|
+
<>
|
|
130
|
+
<HexBullet size="sm" color={bulletColor} />
|
|
131
|
+
{app}
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
return appUrl ? (
|
|
135
|
+
<a
|
|
136
|
+
key={app}
|
|
137
|
+
href={appUrl}
|
|
138
|
+
className={styles.appPill}
|
|
139
|
+
>
|
|
140
|
+
{inner}
|
|
141
|
+
</a>
|
|
142
|
+
) : (
|
|
143
|
+
<span key={app} className={styles.appPill}>{inner}</span>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
115
146
|
</div>
|
|
116
147
|
)}
|
|
117
|
-
</
|
|
148
|
+
</div>
|
|
118
149
|
);
|
|
119
150
|
}
|
|
120
151
|
|
|
@@ -33,6 +33,21 @@
|
|
|
33
33
|
text-decoration: none;
|
|
34
34
|
color: inherit;
|
|
35
35
|
}
|
|
36
|
+
/* Stretched-link overlay. Covers the whole card so any click on
|
|
37
|
+
non-interactive content navigates to the partner detail page,
|
|
38
|
+
while leaving room for nested links (app pills) to take precedence
|
|
39
|
+
via z-index. The overlay carries the aria-label since the card
|
|
40
|
+
itself is no longer the anchor. */
|
|
41
|
+
.cardLink {
|
|
42
|
+
position: absolute;
|
|
43
|
+
inset: 0;
|
|
44
|
+
border-radius: inherit;
|
|
45
|
+
z-index: 1;
|
|
46
|
+
}
|
|
47
|
+
.cardLink:focus-visible {
|
|
48
|
+
outline: 2px solid var(--c-blue-cobalt);
|
|
49
|
+
outline-offset: -2px;
|
|
50
|
+
}
|
|
36
51
|
|
|
37
52
|
.tier {
|
|
38
53
|
position: absolute;
|
|
@@ -49,9 +64,16 @@
|
|
|
49
64
|
color: var(--c-cobalt-700);
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
.tier-
|
|
54
|
-
|
|
67
|
+
/* Service tier: gold pill on a white card. */
|
|
68
|
+
.tier-service .tier { background: var(--c-gold-500); color: var(--c-cobalt-900); }
|
|
69
|
+
|
|
70
|
+
/* Certified tier: cobalt-fill inverse card. The compound .card.tier-certified
|
|
71
|
+
selector is deliberate — it lifts specificity above the plain .card rule
|
|
72
|
+
so the cobalt fill reliably wins over .card { background: white } even
|
|
73
|
+
after CSS-module bundlers reshuffle declaration order. Same for the
|
|
74
|
+
nested colour rules below. */
|
|
75
|
+
.card.tier-certified { background: var(--c-blue-cobalt); color: white; border-color: var(--c-blue-cobalt); }
|
|
76
|
+
.tier-certified .tier { background: white; color: var(--c-blue-cobalt); }
|
|
55
77
|
|
|
56
78
|
.avatar {
|
|
57
79
|
display: inline-flex;
|
|
@@ -73,12 +95,12 @@
|
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
.name { font-size: 18px; font-weight: 700; color: var(--c-cobalt-900); }
|
|
76
|
-
.tier-
|
|
98
|
+
.tier-certified .name { color: white; }
|
|
77
99
|
|
|
78
100
|
.summary { font-size: 14px; line-height: 1.55; color: var(--c-cobalt-700); }
|
|
79
101
|
.summary :global(.next-blue) { color: var(--c-nextcloud-blue); }
|
|
80
|
-
.tier-
|
|
81
|
-
.tier-
|
|
102
|
+
.tier-certified .summary { color: rgba(255, 255, 255, 0.78); }
|
|
103
|
+
.tier-certified .summary :global(.next-blue) { color: var(--c-nextcloud-cyan); }
|
|
82
104
|
|
|
83
105
|
.apps {
|
|
84
106
|
margin-top: auto;
|
|
@@ -88,7 +110,7 @@
|
|
|
88
110
|
gap: 6px;
|
|
89
111
|
flex-wrap: wrap;
|
|
90
112
|
}
|
|
91
|
-
.tier-
|
|
113
|
+
.tier-certified .apps { border-top-color: rgba(255, 255, 255, 0.18); }
|
|
92
114
|
|
|
93
115
|
.appPill {
|
|
94
116
|
display: inline-flex;
|
|
@@ -100,8 +122,26 @@
|
|
|
100
122
|
border-radius: var(--radius-pill);
|
|
101
123
|
font-size: 12px;
|
|
102
124
|
font-family: var(--conduction-typography-font-family-code);
|
|
125
|
+
/* When the pill is rendered as <a> it sits above the cardLink
|
|
126
|
+
overlay so clicks land on the pill, not the card-wide link. */
|
|
127
|
+
position: relative;
|
|
128
|
+
z-index: 2;
|
|
129
|
+
text-decoration: none;
|
|
130
|
+
transition: background 160ms, color 160ms;
|
|
131
|
+
}
|
|
132
|
+
/* Hover affordance only when the pill is a real link (i.e. the
|
|
133
|
+
app is in the registry). Plain <span> pills (e.g. "Nextcloud")
|
|
134
|
+
stay static. */
|
|
135
|
+
a.appPill:hover {
|
|
136
|
+
background: var(--c-blue-cobalt);
|
|
137
|
+
color: white;
|
|
138
|
+
text-decoration: none;
|
|
139
|
+
}
|
|
140
|
+
.card.tier-certified a.appPill:hover {
|
|
141
|
+
background: white;
|
|
142
|
+
color: var(--c-blue-cobalt);
|
|
103
143
|
}
|
|
104
|
-
.tier-
|
|
144
|
+
.tier-certified .appPill { background: rgba(255, 255, 255, 0.12); color: white; }
|
|
105
145
|
|
|
106
146
|
/* Become-a-partner CTA tile */
|
|
107
147
|
.become {
|
|
@@ -182,8 +222,8 @@
|
|
|
182
222
|
max-height: 28px;
|
|
183
223
|
object-fit: contain;
|
|
184
224
|
}
|
|
185
|
-
.other.tier-
|
|
186
|
-
.other.tier-
|
|
225
|
+
.other.tier-service .miniAvatar { box-shadow: inset 0 0 0 2px var(--c-gold-500); }
|
|
226
|
+
.other.tier-certified .miniAvatar { box-shadow: inset 0 0 0 2px var(--c-orange-knvb); }
|
|
187
227
|
|
|
188
228
|
.otherName {
|
|
189
229
|
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:
|
|
9
|
-
* -
|
|
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: '
|
|
23
|
+
* {href: '/partners/acato', tier: 'certified', name: 'Acato',
|
|
20
24
|
* logo: '/img/partners/acato.png',
|
|
21
|
-
* summary: <>
|
|
22
|
-
* apps: ['
|
|
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: '
|
|
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 = {
|
|
40
|
-
const TIER_ORDER = ['
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
.sort((a, b) => b
|
|
59
|
-
.map((
|
|
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
|
|
63
|
-
if (
|
|
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
|
|
72
|
-
if (
|
|
99
|
+
const offeringFilter = selected.offering || [];
|
|
100
|
+
if (offeringFilter.length > 0) {
|
|
73
101
|
const partnerApps = partner.apps || [];
|
|
74
|
-
|
|
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
|
-
* (
|
|
8
|
-
*
|
|
9
|
-
* Conduction
|
|
10
|
-
*
|
|
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.
|
|
@@ -37,21 +37,22 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import React from 'react';
|
|
40
|
+
import {appHrefByName} from '../../data/apps-registry';
|
|
40
41
|
import styles from './PartnerSidecard.module.css';
|
|
41
42
|
|
|
42
43
|
const TIER_LABELS = {
|
|
43
|
-
|
|
44
|
+
host: 'Host',
|
|
45
|
+
service: 'Service',
|
|
44
46
|
certified: 'Certified',
|
|
45
|
-
strategic: 'Strategic',
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
const TIER_BLURB = {
|
|
49
|
-
|
|
50
|
-
'
|
|
50
|
+
host:
|
|
51
|
+
'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.',
|
|
52
|
+
service:
|
|
53
|
+
'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
54
|
certified:
|
|
52
|
-
'Certified
|
|
53
|
-
strategic:
|
|
54
|
-
'Strategic partner with shared roadmap commitments and joint engineering. By invitation. Co-sale on government tenders, co-branded hosting offerings.',
|
|
55
|
+
'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
56
|
};
|
|
56
57
|
|
|
57
58
|
export default function PartnerSidecard({
|
|
@@ -61,7 +62,7 @@ export default function PartnerSidecard({
|
|
|
61
62
|
className,
|
|
62
63
|
}) {
|
|
63
64
|
if (!partner) return null;
|
|
64
|
-
const tier = partner.tier || '
|
|
65
|
+
const tier = partner.tier || 'host';
|
|
65
66
|
const tierLabel = TIER_LABELS[tier] || tier;
|
|
66
67
|
const composed = [styles.rail, styles[`tier-${tier}`], className].filter(Boolean).join(' ');
|
|
67
68
|
|
|
@@ -71,13 +72,13 @@ export default function PartnerSidecard({
|
|
|
71
72
|
{tier === 'certified' && (
|
|
72
73
|
<img
|
|
73
74
|
className={styles.tierBadge}
|
|
74
|
-
src="/img/brand/avatar-conduction-gold
|
|
75
|
+
src="/img/brand/avatar-conduction-gold.svg"
|
|
75
76
|
alt="Conduction-certified mark"
|
|
76
77
|
width="44"
|
|
77
78
|
height="44"
|
|
78
79
|
/>
|
|
79
80
|
)}
|
|
80
|
-
<div className={styles.tierLabel}>{
|
|
81
|
+
<div className={styles.tierLabel}>{`${tierLabel} partner`}</div>
|
|
81
82
|
<p className={styles.tierBlurb}>{TIER_BLURB[tier]}</p>
|
|
82
83
|
</div>
|
|
83
84
|
|
|
@@ -85,9 +86,14 @@ export default function PartnerSidecard({
|
|
|
85
86
|
<div className={styles.section}>
|
|
86
87
|
<h4 className={styles.sectionTitle}>Apps they ship</h4>
|
|
87
88
|
<div className={styles.chipRow}>
|
|
88
|
-
{partner.apps.map((app) =>
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
{partner.apps.map((app) => {
|
|
90
|
+
const url = appHrefByName(app);
|
|
91
|
+
return url ? (
|
|
92
|
+
<a key={app} href={url} className={styles.chip}>{app}</a>
|
|
93
|
+
) : (
|
|
94
|
+
<span key={app} className={styles.chip}>{app}</span>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
91
97
|
</div>
|
|
92
98
|
</div>
|
|
93
99
|
)}
|
|
@@ -25,15 +25,17 @@
|
|
|
25
25
|
box-shadow: var(--shadow-1);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
.tier-
|
|
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-
|
|
35
|
-
/* white card, but a hairline gold stripe
|
|
36
|
-
|
|
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-
|
|
57
|
-
.tier-
|
|
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-
|
|
67
|
+
.tier-certified .tierBlurb { color: rgba(255, 255, 255, 0.85); }
|
|
66
68
|
|
|
67
69
|
/* ---------- Generic section ---------- */
|
|
68
70
|
|
|
@@ -101,6 +103,15 @@
|
|
|
101
103
|
border-radius: var(--radius-pill);
|
|
102
104
|
font-size: 12px;
|
|
103
105
|
font-family: var(--conduction-typography-font-family-code);
|
|
106
|
+
text-decoration: none;
|
|
107
|
+
transition: background 160ms, color 160ms;
|
|
108
|
+
}
|
|
109
|
+
/* Only the <a> form gets hover affordance — plain <span> chips
|
|
110
|
+
(apps not in the apps-registry, e.g. Nextcloud) stay static. */
|
|
111
|
+
a.chip:hover {
|
|
112
|
+
background: var(--c-blue-cobalt);
|
|
113
|
+
color: white;
|
|
114
|
+
text-decoration: none;
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
/* ---------- Solutions list ---------- */
|
|
@@ -59,3 +59,26 @@ export function getApp(slug) {
|
|
|
59
59
|
export function getApps(slugs = []) {
|
|
60
60
|
return slugs.map(getApp).filter(Boolean);
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a display-name (e.g. "OpenCatalogi", "DocuDesk", "MyDash")
|
|
65
|
+
* to its product page href, or undefined when the name is not in the
|
|
66
|
+
* registry. Used by partner cards / sidecards to turn the apps-shipped
|
|
67
|
+
* chip row into a clickable link list. Names like "Nextcloud" that
|
|
68
|
+
* aren't ours fall through and the consumer renders a plain span.
|
|
69
|
+
*
|
|
70
|
+
* Match is case-insensitive on both name and slug so consumers can
|
|
71
|
+
* pass either form ("OpenCatalogi", "opencatalogi", or "OpenCATALOGI")
|
|
72
|
+
* without each adding their own normalisation.
|
|
73
|
+
*/
|
|
74
|
+
export function appHrefByName(name) {
|
|
75
|
+
if (!name) return undefined;
|
|
76
|
+
const target = String(name).toLowerCase();
|
|
77
|
+
for (const slug of APP_SLUGS) {
|
|
78
|
+
const entry = APPS_REGISTRY[slug];
|
|
79
|
+
if (slug === target || entry.name.toLowerCase() === target) {
|
|
80
|
+
return entry.productHref;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|