@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/docusaurus-preset",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
@@ -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
- <Tag href={href} className={composed}>
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
- <span key={i} className={styles.appPill}>
114
- <HexBullet size="sm" color={bulletColor} />
115
- {app}
116
- </span>
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
- </Tag>
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-on-white.svg"
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
- <span key={app} className={styles.chip}>{app}</span>
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
+ }