@conduction/docusaurus-preset 2.0.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 +38 -10
- package/src/components/PartnerCard/PartnerCard.module.css +33 -0
- package/src/components/PartnerSidecard/PartnerSidecard.jsx +10 -4
- package/src/components/PartnerSidecard/PartnerSidecard.module.css +9 -0
- package/src/data/apps-registry.js +23 -0
package/package.json
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
import React from 'react';
|
|
42
42
|
import HexBullet from '../primitives/HexBullet';
|
|
43
|
+
import {appHrefByName} from '../../data/apps-registry';
|
|
43
44
|
import styles from './PartnerCard.module.css';
|
|
44
45
|
|
|
45
46
|
const TIER_LABELS = {host: 'Host', service: 'Service', certified: 'Certified'};
|
|
@@ -88,17 +89,30 @@ export default function PartnerCard({
|
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
/* 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. */
|
|
92
100
|
const composed = [
|
|
93
101
|
styles.card,
|
|
94
102
|
styles['tier-' + tier],
|
|
95
103
|
className,
|
|
96
104
|
].filter(Boolean).join(' ');
|
|
97
|
-
const Tag = href ? 'a' : 'div';
|
|
98
105
|
const bulletColor = tier === 'certified' ? 'var(--c-orange-knvb)' : 'var(--c-blue-cobalt)';
|
|
99
106
|
|
|
100
107
|
return (
|
|
101
|
-
<
|
|
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
|
+
)}
|
|
102
116
|
<span className={styles.tier}>{TIER_LABELS[tier] || tier}</span>
|
|
103
117
|
{logo && (
|
|
104
118
|
<div className={styles.avatar}>
|
|
@@ -109,15 +123,29 @@ export default function PartnerCard({
|
|
|
109
123
|
{summary && <div className={styles.summary}>{summary}</div>}
|
|
110
124
|
{apps.length > 0 && (
|
|
111
125
|
<div className={styles.apps}>
|
|
112
|
-
{apps.map((app, i) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
})}
|
|
118
146
|
</div>
|
|
119
147
|
)}
|
|
120
|
-
</
|
|
148
|
+
</div>
|
|
121
149
|
);
|
|
122
150
|
}
|
|
123
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;
|
|
@@ -107,6 +122,24 @@
|
|
|
107
122
|
border-radius: var(--radius-pill);
|
|
108
123
|
font-size: 12px;
|
|
109
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);
|
|
110
143
|
}
|
|
111
144
|
.tier-certified .appPill { background: rgba(255, 255, 255, 0.12); color: white; }
|
|
112
145
|
|
|
@@ -37,6 +37,7 @@
|
|
|
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 = {
|
|
@@ -71,7 +72,7 @@ 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"
|
|
@@ -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
|
)}
|
|
@@ -103,6 +103,15 @@
|
|
|
103
103
|
border-radius: var(--radius-pill);
|
|
104
104
|
font-size: 12px;
|
|
105
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;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
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
|
+
}
|