@conduction/docusaurus-preset 3.25.0 → 3.26.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
|
@@ -70,8 +70,11 @@
|
|
|
70
70
|
background: var(--c-mint-500);
|
|
71
71
|
flex-shrink: 0;
|
|
72
72
|
}
|
|
73
|
-
.beta
|
|
74
|
-
.
|
|
73
|
+
/* Compound selectors (.h.beta / .h.soon) so the status colour always wins
|
|
74
|
+
over the default .h mint, regardless of how the CSS minifier reorders or
|
|
75
|
+
merges the single-class rules in the bundle. */
|
|
76
|
+
.h.beta { background: var(--c-cobalt-500); }
|
|
77
|
+
.h.soon { background: var(--c-orange-knvb); }
|
|
75
78
|
|
|
76
79
|
.label {
|
|
77
80
|
flex: 1;
|
|
@@ -1,47 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* <FeaturesPage />
|
|
3
3
|
*
|
|
4
|
-
* Route component used by the `conduction-features-page` plugin. Renders
|
|
5
|
-
*
|
|
6
|
-
* `openspec/
|
|
7
|
-
*
|
|
4
|
+
* Route component used by the `conduction-features-page` plugin. Renders the
|
|
5
|
+
* app's commercial capability list, sourced (in priority order) from a curated
|
|
6
|
+
* `openspec/features.overlay.json` or, as a fallback, hardened spec derivation
|
|
7
|
+
* from `openspec/specs/` (see plugins/extractFeatures.js).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Each entry maps to a single `<FeatureItem>` inside `<FeatureGrid>`, coloured
|
|
10
|
+
* by its maturity: stable (mint), beta (blue), soon (orange).
|
|
11
|
+
*
|
|
12
|
+
* Locale-aware: on the Dutch site each entry shows `title_nl` / `summary_nl`
|
|
13
|
+
* when present, falling back to the English `title` / `summary`. A
|
|
14
|
+
* `providedBy` capability (e.g. surfaced from OpenRegister) is credited inline
|
|
15
|
+
* so cross-app functionality reads as real and attributed.
|
|
13
16
|
*
|
|
14
17
|
* Receives one prop module from the plugin via `addRoute({modules})`:
|
|
15
18
|
*
|
|
16
19
|
* data: {
|
|
17
|
-
* features: Array<{slug,
|
|
20
|
+
* features: Array<{slug,title,summary,status,docsUrl,providedBy?,title_nl?,summary_nl?}>,
|
|
18
21
|
* title: string,
|
|
19
22
|
* intro: string | null,
|
|
20
23
|
* }
|
|
21
|
-
*
|
|
22
|
-
* Sites that want a different page chrome can swizzle the plugin route
|
|
23
|
-
* to register their own component; this default is intentionally minimal
|
|
24
|
-
* (one heading, one optional intro paragraph, the grid).
|
|
25
24
|
*/
|
|
26
25
|
|
|
27
26
|
import React from 'react';
|
|
28
27
|
import Layout from '@theme/Layout';
|
|
28
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
29
29
|
import FeatureGrid from '../FeatureGrid/FeatureGrid.jsx';
|
|
30
30
|
|
|
31
|
+
// Display name + localized "powered by" lead-in for a providedBy app id.
|
|
32
|
+
const PROVIDER_NAMES = {
|
|
33
|
+
openregister: 'OpenRegister',
|
|
34
|
+
opencatalogi: 'OpenCatalogi',
|
|
35
|
+
openconnector: 'OpenConnector',
|
|
36
|
+
docudesk: 'DocuDesk',
|
|
37
|
+
nldesign: 'NLDesign',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function providerCredit(providedBy, locale) {
|
|
41
|
+
if (!providedBy) return '';
|
|
42
|
+
const name = PROVIDER_NAMES[String(providedBy).toLowerCase()] || providedBy;
|
|
43
|
+
return locale === 'nl' ? `Draait op ${name}.` : `Powered by ${name}.`;
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
export default function FeaturesPage({data}) {
|
|
32
47
|
const {features = [], title = 'Features', intro = null} = data || {};
|
|
48
|
+
const {i18n} = useDocusaurusContext();
|
|
49
|
+
const locale = (i18n && i18n.currentLocale) || 'en';
|
|
33
50
|
|
|
34
|
-
const items = features.map((f) =>
|
|
35
|
-
label
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
const items = features.map((f) => {
|
|
52
|
+
const label = (locale === 'nl' && f.title_nl) || f.title || f.slug;
|
|
53
|
+
const summary = (locale === 'nl' && f.summary_nl) || f.summary || '';
|
|
54
|
+
const credit = providerCredit(f.providedBy, locale);
|
|
55
|
+
const tip = [summary, credit].filter(Boolean).join(' ');
|
|
56
|
+
return {
|
|
57
|
+
label,
|
|
58
|
+
tip,
|
|
59
|
+
status: f.status || 'stable',
|
|
60
|
+
href: f.docsUrl || undefined,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
40
63
|
|
|
41
64
|
return (
|
|
42
65
|
<Layout
|
|
43
66
|
title={title}
|
|
44
|
-
description={intro || `Capabilities shipped in this app
|
|
67
|
+
description={intro || `Capabilities shipped in this app.`}
|
|
45
68
|
>
|
|
46
69
|
<main className="container margin-vert--lg">
|
|
47
70
|
<h1>{title}</h1>
|
|
@@ -3,19 +3,26 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Build-time port of the org-wide `scripts/extract-features.py`
|
|
5
5
|
* (https://codeberg.org/Conduction/.github). Derives the published feature
|
|
6
|
-
* list
|
|
7
|
-
* docs `/features` page is ALWAYS generated from the current specs — the
|
|
8
|
-
* committed `docs/features.json` is no longer a hand-maintained source of
|
|
9
|
-
* truth that can drift.
|
|
6
|
+
* list for the docs `/features` page from two sources, in priority order:
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* 1. Commercial overlay — `openspec/features.overlay.json` (optional). When
|
|
9
|
+
* present it is the AUTHORITATIVE curated list: honest maturity,
|
|
10
|
+
* brand-voiced NL/EN benefit copy, roadmap rows, and capabilities
|
|
11
|
+
* provided by another app (e.g. OpenRegister's AI assistants and content
|
|
12
|
+
* leaves surfaced through a consuming app, via `providedBy`).
|
|
13
|
+
* 2. Spec derivation (fallback) — `openspec/specs/<slug>/spec.md`. Hardened:
|
|
14
|
+
* internal/process specs are filtered out, engineering artifacts
|
|
15
|
+
* (`@e2e`, `#<pr>`, `Spec:`, em-dashes, `TBD`) are stripped, and optional
|
|
16
|
+
* frontmatter fields (`commercial`, `maturity`, `title`, `summary`,
|
|
17
|
+
* `provided-by`, `*_nl`) let an author curate without an overlay.
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* Maturity is one of stable (mint) / beta (blue) / soon (orange). Each entry:
|
|
20
|
+
* { slug, title, summary, status, docsUrl } plus optional { providedBy,
|
|
21
|
+
* title_nl, summary_nl }. Overlay order is preserved; spec-derived entries are
|
|
22
|
+
* sorted by slug. Mirrors the Python extractor so docs output matches CI.
|
|
23
|
+
*
|
|
24
|
+
* Returns null when nothing is available, so the caller can fall back to a
|
|
25
|
+
* committed `docs/features.json` on deploys shipped without `openspec/`.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
const fs = require('fs');
|
|
@@ -24,14 +31,16 @@ const path = require('path');
|
|
|
24
31
|
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?\n)---\s*\n([\s\S]*)$/;
|
|
25
32
|
const SLUGGY_RE = /^[a-z0-9]+(?:-[a-z0-9]+)+$/;
|
|
26
33
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
34
|
+
// Fallback map: spec lifecycle status -> buyer-facing maturity. Used only when
|
|
35
|
+
// a spec has no explicit `maturity:` and the app has no overlay.
|
|
29
36
|
const STATUS_KIND = {
|
|
30
37
|
done: 'stable', implemented: 'stable', reviewed: 'stable', active: 'stable', stable: 'stable',
|
|
31
38
|
'in-progress': 'beta', implementing: 'beta', partial: 'beta', beta: 'beta',
|
|
32
39
|
draft: 'soon', specified: 'soon', proposed: 'soon', planned: 'soon', 'coming-soon': 'soon', soon: 'soon',
|
|
33
40
|
};
|
|
34
41
|
|
|
42
|
+
const VALID_MATURITY = new Set(['stable', 'beta', 'soon']);
|
|
43
|
+
|
|
35
44
|
const ACRONYMS = new Set([
|
|
36
45
|
'ai', 'api', 'ui', 'ux', 'or', 'bi', 'mcp', 'tmlo', 'mdto', 'dcat', 'woo',
|
|
37
46
|
'vth', 'kcc', 'crm', 'pdf', 'csv', 'sepa', 'zgw', 'ztc', 'dso', 'rbac',
|
|
@@ -40,6 +49,31 @@ const ACRONYMS = new Set([
|
|
|
40
49
|
'pwa', 'sip', 'eml', 'sla', 'llm', 'rag', 'e2e', 'qr', 'vng',
|
|
41
50
|
]);
|
|
42
51
|
|
|
52
|
+
// Slug segments that mark an internal/process spec (filtered unless the spec
|
|
53
|
+
// sets `commercial: true` or an overlay re-includes it).
|
|
54
|
+
const INTERNAL_SEGMENTS = new Set([
|
|
55
|
+
'refactor', 'refactoring', 'migrate', 'migration', 'migrations', 'migrated',
|
|
56
|
+
'test', 'tests', 'testing', 'e2e', 'ci', 'cd', 'docs', 'documentation',
|
|
57
|
+
'manifest', 'plumbing', 'scaffold', 'scaffolding', 'bootstrap', 'chore',
|
|
58
|
+
'cleanup', 'rename', 'renaming', 'bump', 'deps', 'dependency',
|
|
59
|
+
'dependencies', 'lint', 'linting', 'coverage', 'gate', 'gates', 'seed',
|
|
60
|
+
'seeding', 'fixture', 'fixtures', 'spike', 'poc', 'tooling', 'devx',
|
|
61
|
+
'internal',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function field(front, key) {
|
|
65
|
+
const re = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*(.+?)\\s*$`, 'm');
|
|
66
|
+
const m = front.match(re);
|
|
67
|
+
if (!m) return null;
|
|
68
|
+
return m[1].trim().replace(/^["']|["']$/g, '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function boolField(front, key) {
|
|
72
|
+
const raw = field(front, key);
|
|
73
|
+
if (raw === null) return null;
|
|
74
|
+
return ['true', 'yes', '1'].includes(raw.trim().toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
function titlecaseSlug(slug) {
|
|
44
78
|
return slug
|
|
45
79
|
.split('-')
|
|
@@ -47,12 +81,53 @@ function titlecaseSlug(slug) {
|
|
|
47
81
|
.join(' ');
|
|
48
82
|
}
|
|
49
83
|
|
|
84
|
+
function stripArtifacts(text) {
|
|
85
|
+
if (!text) return '';
|
|
86
|
+
let t = text;
|
|
87
|
+
t = t.replace(/@e2e\b[^\n.]*/gi, '');
|
|
88
|
+
t = t.replace(/\(?#\d+\)?/g, '');
|
|
89
|
+
t = t.replace(/\b(?:delta\s+)?spec:\s*/gi, '');
|
|
90
|
+
t = t.replace(/`/g, '');
|
|
91
|
+
t = t.replace(/\s*--\s*/g, ', ');
|
|
92
|
+
t = t.replace(/\s*[—]\s*/g, '. ');
|
|
93
|
+
t = t.replace(/\b(?:TODO|TBD)\b/gi, '');
|
|
94
|
+
t = t.replace(/\s+/g, ' ');
|
|
95
|
+
t = t.replace(/\s+([.,;:])/g, '$1');
|
|
96
|
+
t = t.replace(/\.\s*\./g, '.');
|
|
97
|
+
return t.replace(/^[\s.,;:-]+|[\s.,;:-]+$/g, '');
|
|
98
|
+
}
|
|
99
|
+
|
|
50
100
|
function cleanTitle(rawTitle, slug) {
|
|
51
|
-
let title = rawTitle.replace(/^\s*spec:\s*/i, '').replace(/\s+specification\s*$/i, '').trim();
|
|
101
|
+
let title = rawTitle.replace(/^\s*(?:delta\s+)?spec:\s*/i, '').replace(/\s+specification\s*$/i, '').trim();
|
|
102
|
+
title = stripArtifacts(title);
|
|
103
|
+
title = title.split('. ')[0].trim().replace(/\.$/, '');
|
|
52
104
|
if (!title || title === slug || SLUGGY_RE.test(title)) title = titlecaseSlug(slug);
|
|
53
105
|
return title;
|
|
54
106
|
}
|
|
55
107
|
|
|
108
|
+
function firstSentence(summary) {
|
|
109
|
+
let s = stripArtifacts(summary);
|
|
110
|
+
if (!s) return '';
|
|
111
|
+
const m = s.match(/^as an? .*? so that (.+)$/i);
|
|
112
|
+
if (m) s = m[1].trim();
|
|
113
|
+
s = s.split('. ')[0].trim();
|
|
114
|
+
if (!s || ['tbd', 'todo', 'purpose'].includes(s.toLowerCase())) return '';
|
|
115
|
+
s = s.charAt(0).toUpperCase() + s.slice(1);
|
|
116
|
+
if (!s.endsWith('.')) s += '.';
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isInternalSlug(slug) {
|
|
121
|
+
return slug.split('-').some((seg) => INTERNAL_SEGMENTS.has(seg));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function attachOptional(entry, providedBy, titleNl, summaryNl) {
|
|
125
|
+
if (providedBy) entry.providedBy = providedBy;
|
|
126
|
+
if (titleNl) entry.title_nl = titleNl;
|
|
127
|
+
if (summaryNl) entry.summary_nl = summaryNl;
|
|
128
|
+
return entry;
|
|
129
|
+
}
|
|
130
|
+
|
|
56
131
|
function parseSpec(specPath, slug) {
|
|
57
132
|
let text;
|
|
58
133
|
try {
|
|
@@ -60,45 +135,108 @@ function parseSpec(specPath, slug) {
|
|
|
60
135
|
} catch (e) {
|
|
61
136
|
return null;
|
|
62
137
|
}
|
|
63
|
-
|
|
64
138
|
const fm = FRONTMATTER_RE.exec(text);
|
|
65
139
|
if (fm === null) return null;
|
|
66
140
|
const front = fm[1];
|
|
67
141
|
const body = fm[2];
|
|
68
142
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
143
|
+
const commercial = boolField(front, 'commercial');
|
|
144
|
+
if (commercial === false) return null;
|
|
145
|
+
|
|
146
|
+
const status = (field(front, 'status') || '').toLowerCase();
|
|
147
|
+
const maturity = (field(front, 'maturity') || '').toLowerCase();
|
|
148
|
+
let kind = VALID_MATURITY.has(maturity) ? maturity : STATUS_KIND[status];
|
|
149
|
+
if (!kind) kind = commercial === true ? 'beta' : null;
|
|
72
150
|
if (!kind) return null;
|
|
73
151
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
152
|
+
if (isInternalSlug(slug) && commercial !== true) return null;
|
|
153
|
+
|
|
154
|
+
const titleOverride = field(front, 'title');
|
|
155
|
+
let title;
|
|
156
|
+
if (titleOverride) {
|
|
157
|
+
title = stripArtifacts(titleOverride) || titlecaseSlug(slug);
|
|
158
|
+
} else {
|
|
159
|
+
const titleMatch = body.match(/^#\s+(.+?)\s*$/m);
|
|
160
|
+
const rawTitle = titleMatch ? titleMatch[1].trim() : slug;
|
|
161
|
+
title = cleanTitle(rawTitle, slug);
|
|
162
|
+
}
|
|
77
163
|
|
|
164
|
+
const summaryOverride = field(front, 'summary') || field(front, 'tagline');
|
|
78
165
|
let summary = '';
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
166
|
+
if (summaryOverride) {
|
|
167
|
+
summary = firstSentence(summaryOverride);
|
|
168
|
+
} else {
|
|
169
|
+
const purposeHeading = body.match(/^##\s+Purpose\s*$/m);
|
|
170
|
+
if (purposeHeading) {
|
|
171
|
+
const rest = body.slice(purposeHeading.index + purposeHeading[0].length);
|
|
172
|
+
const nextHeadingIdx = rest.search(/\n##\s/);
|
|
173
|
+
const section = (nextHeadingIdx === -1 ? rest : rest.slice(0, nextHeadingIdx)).trim();
|
|
174
|
+
const firstPara = section.split(/\n\s*\n/)[0] || '';
|
|
175
|
+
summary = firstSentence(firstPara);
|
|
176
|
+
}
|
|
86
177
|
}
|
|
87
178
|
|
|
88
|
-
|
|
179
|
+
const entry = {
|
|
89
180
|
slug,
|
|
90
181
|
title,
|
|
91
182
|
summary,
|
|
92
183
|
status: kind,
|
|
93
184
|
docsUrl: `openspec/specs/${slug}/spec.md`,
|
|
94
185
|
};
|
|
186
|
+
return attachOptional(
|
|
187
|
+
entry,
|
|
188
|
+
field(front, 'provided-by') || field(front, 'providedBy'),
|
|
189
|
+
field(front, 'title_nl'),
|
|
190
|
+
firstSentence(field(front, 'summary_nl') || field(front, 'tagline_nl') || ''),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeOverlayEntry(raw) {
|
|
195
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
196
|
+
let slug = String(raw.slug || '').trim();
|
|
197
|
+
let status = String(raw.status || '').trim().toLowerCase();
|
|
198
|
+
if (!VALID_MATURITY.has(status)) status = STATUS_KIND[status] || 'stable';
|
|
199
|
+
const title = String(raw.title || '').trim() || (slug ? titlecaseSlug(slug) : '');
|
|
200
|
+
if (!title) return null;
|
|
201
|
+
if (!slug) slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
202
|
+
const entry = {
|
|
203
|
+
slug,
|
|
204
|
+
title,
|
|
205
|
+
summary: String(raw.summary || '').trim(),
|
|
206
|
+
status,
|
|
207
|
+
docsUrl: String(raw.docsUrl || `openspec/specs/${slug}/spec.md`),
|
|
208
|
+
};
|
|
209
|
+
return attachOptional(
|
|
210
|
+
entry,
|
|
211
|
+
raw.providedBy,
|
|
212
|
+
raw.title_nl ? String(raw.title_nl).trim() : null,
|
|
213
|
+
raw.summary_nl ? String(raw.summary_nl).trim() : null,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function loadOverlay(specsDir) {
|
|
218
|
+
const overlayPath = path.resolve(specsDir, '..', 'features.overlay.json');
|
|
219
|
+
let data;
|
|
220
|
+
try {
|
|
221
|
+
if (!fs.statSync(overlayPath).isFile()) return null;
|
|
222
|
+
data = JSON.parse(fs.readFileSync(overlayPath, 'utf8'));
|
|
223
|
+
} catch (e) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const rawList = Array.isArray(data) ? data : (data && data.features);
|
|
227
|
+
if (!Array.isArray(rawList)) return null;
|
|
228
|
+
const entries = rawList.map(normalizeOverlayEntry).filter((e) => e !== null);
|
|
229
|
+
return entries.length > 0 ? entries : null;
|
|
95
230
|
}
|
|
96
231
|
|
|
97
232
|
/**
|
|
98
233
|
* @param {string} specsDir Absolute path to `<app>/openspec/specs`.
|
|
99
|
-
* @returns {Array<
|
|
234
|
+
* @returns {Array<object>|null}
|
|
100
235
|
*/
|
|
101
236
|
function extractFeatures(specsDir) {
|
|
237
|
+
const overlay = loadOverlay(specsDir);
|
|
238
|
+
if (overlay !== null) return overlay;
|
|
239
|
+
|
|
102
240
|
let slugs;
|
|
103
241
|
try {
|
|
104
242
|
if (!fs.statSync(specsDir).isDirectory()) return null;
|
|
@@ -124,4 +262,4 @@ function extractFeatures(specsDir) {
|
|
|
124
262
|
return entries.length > 0 ? entries : null;
|
|
125
263
|
}
|
|
126
264
|
|
|
127
|
-
module.exports = {
|
|
265
|
+
module.exports = {extractFeatures};
|