@conduction/docusaurus-preset 3.24.0 → 3.25.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": "3.24.0",
3
+ "version": "3.25.0",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
@@ -47,20 +47,29 @@ import styles from './FeatureGrid.module.css';
47
47
 
48
48
  const STATUS_CLASSES = {stable: '', beta: styles.beta, soon: styles.soon};
49
49
 
50
- export function FeatureItem({label, tip, status = 'stable', href, className}) {
50
+ export function FeatureItem({label, tip, status = 'stable', href, className, showDescription = false}) {
51
51
  const hexClass = [styles.h, STATUS_CLASSES[status]].filter(Boolean).join(' ');
52
- const body = (
52
+ const body = showDescription ? (
53
+ <>
54
+ <span className={styles.head}>
55
+ <span className={hexClass} aria-hidden="true" />
56
+ <span className={styles.label}>{label}</span>
57
+ </span>
58
+ {tip && <span className={styles.desc}>{tip}</span>}
59
+ </>
60
+ ) : (
53
61
  <>
54
62
  <span className={hexClass} aria-hidden="true" />
55
63
  <span className={styles.label}>{label}</span>
56
64
  {tip && <span className={styles.tip}>{tip}</span>}
57
65
  </>
58
66
  );
67
+ const cardClass = showDescription ? styles.itemCard : null;
59
68
  if (href) {
60
69
  const isExternal = /^https?:\/\//.test(href);
61
70
  return (
62
71
  <a
63
- className={[styles.item, styles.itemLink, className].filter(Boolean).join(' ')}
72
+ className={[styles.item, cardClass, styles.itemLink, className].filter(Boolean).join(' ')}
64
73
  href={href}
65
74
  title={tip || label}
66
75
  {...(isExternal ? {target: '_blank', rel: 'noopener noreferrer'} : {})}
@@ -71,7 +80,7 @@ export function FeatureItem({label, tip, status = 'stable', href, className}) {
71
80
  }
72
81
  return (
73
82
  <div
74
- className={[styles.item, className].filter(Boolean).join(' ')}
83
+ className={[styles.item, cardClass, className].filter(Boolean).join(' ')}
75
84
  tabIndex={0}
76
85
  title={tip || label}
77
86
  >
@@ -84,16 +93,17 @@ export function FeatureGridGroup({label, className}) {
84
93
  return <h4 className={[styles.group, className].filter(Boolean).join(' ')}>{label}</h4>;
85
94
  }
86
95
 
87
- export default function FeatureGrid({items, legend = false, children, className}) {
96
+ export default function FeatureGrid({items, legend = false, children, className, withDescriptions = false}) {
97
+ const gridClass = [styles.grid, withDescriptions ? styles.gridCards : null].filter(Boolean).join(' ');
88
98
  return (
89
99
  <div className={className}>
90
100
  {legend && <Legend />}
91
- <div className={styles.grid}>
101
+ <div className={gridClass}>
92
102
  {items
93
103
  ? items.map((it, i) => (
94
104
  it.group
95
105
  ? <FeatureGridGroup key={i} label={it.group} />
96
- : <FeatureItem key={i} {...it} />
106
+ : <FeatureItem key={i} showDescription={withDescriptions} {...it} />
97
107
  ))
98
108
  : children}
99
109
  </div>
@@ -70,14 +70,50 @@
70
70
  background: var(--c-mint-500);
71
71
  flex-shrink: 0;
72
72
  }
73
- .beta { background: var(--c-orange-knvb); }
74
- .soon { background: var(--c-cobalt-300); }
73
+ .beta { background: var(--c-cobalt-500); }
74
+ .soon { background: var(--c-orange-knvb); }
75
75
 
76
76
  .label {
77
77
  flex: 1;
78
78
  line-height: 1.3;
79
79
  }
80
80
 
81
+ /* Card layout (withDescriptions): hex + label on a header row, the full
82
+ description shown inline below instead of as a hover tooltip. */
83
+ .gridCards {
84
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
85
+ gap: 16px 24px;
86
+ }
87
+ .itemCard {
88
+ flex-direction: column;
89
+ align-items: stretch;
90
+ gap: 6px;
91
+ padding: 14px 16px;
92
+ cursor: default;
93
+ border: 1px solid var(--c-cobalt-100);
94
+ border-radius: var(--radius-md);
95
+ background: white;
96
+ }
97
+ .itemCard.itemLink { cursor: pointer; }
98
+ .itemCard:hover,
99
+ .itemCard:focus-visible {
100
+ background: var(--c-cobalt-50);
101
+ border-color: var(--c-cobalt-200);
102
+ padding: 14px 16px;
103
+ margin: 0;
104
+ }
105
+ .head {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 12px;
109
+ }
110
+ .desc {
111
+ font-size: 13px;
112
+ line-height: 1.55;
113
+ color: var(--c-cobalt-700);
114
+ margin-left: 22px;
115
+ }
116
+
81
117
  /* Tooltip: appears above the item on hover or keyboard focus */
82
118
  .tip {
83
119
  position: absolute;
@@ -34,7 +34,7 @@ export default function FeaturesPage({data}) {
34
34
  const items = features.map((f) => ({
35
35
  label: f.title || f.slug,
36
36
  tip: f.summary || '',
37
- status: 'stable',
37
+ status: f.status || 'stable',
38
38
  href: f.docsUrl || undefined,
39
39
  }));
40
40
 
@@ -49,7 +49,7 @@ export default function FeaturesPage({data}) {
49
49
  {items.length === 0 ? (
50
50
  <p>No features documented yet.</p>
51
51
  ) : (
52
- <FeatureGrid items={items} legend />
52
+ <FeatureGrid items={items} legend withDescriptions />
53
53
  )}
54
54
  </main>
55
55
  </Layout>
@@ -22,6 +22,36 @@ const fs = require('fs');
22
22
  const path = require('path');
23
23
 
24
24
  const FRONTMATTER_RE = /^---\s*\n([\s\S]*?\n)---\s*\n([\s\S]*)$/;
25
+ const SLUGGY_RE = /^[a-z0-9]+(?:-[a-z0-9]+)+$/;
26
+
27
+ // Map a spec's frontmatter status to a roadmap kind: stable (mint),
28
+ // beta (cobalt blue), soon (orange). Unmapped statuses are skipped.
29
+ const STATUS_KIND = {
30
+ done: 'stable', implemented: 'stable', reviewed: 'stable', active: 'stable', stable: 'stable',
31
+ 'in-progress': 'beta', implementing: 'beta', partial: 'beta', beta: 'beta',
32
+ draft: 'soon', specified: 'soon', proposed: 'soon', planned: 'soon', 'coming-soon': 'soon', soon: 'soon',
33
+ };
34
+
35
+ const ACRONYMS = new Set([
36
+ 'ai', 'api', 'ui', 'ux', 'or', 'bi', 'mcp', 'tmlo', 'mdto', 'dcat', 'woo',
37
+ 'vth', 'kcc', 'crm', 'pdf', 'csv', 'sepa', 'zgw', 'ztc', 'dso', 'rbac',
38
+ 'gdpr', 'avg', 'kvk', 'brp', 'sso', 'jwt', 'cli', 'ocr', 'ner', 'kpi',
39
+ 'saas', 'oas', 'json', 'xml', 'html', 'css', 'url', 'http', 'https', 'id',
40
+ 'pwa', 'sip', 'eml', 'sla', 'llm', 'rag', 'e2e', 'qr', 'vng',
41
+ ]);
42
+
43
+ function titlecaseSlug(slug) {
44
+ return slug
45
+ .split('-')
46
+ .map((w) => (ACRONYMS.has(w) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)))
47
+ .join(' ');
48
+ }
49
+
50
+ function cleanTitle(rawTitle, slug) {
51
+ let title = rawTitle.replace(/^\s*spec:\s*/i, '').replace(/\s+specification\s*$/i, '').trim();
52
+ if (!title || title === slug || SLUGGY_RE.test(title)) title = titlecaseSlug(slug);
53
+ return title;
54
+ }
25
55
 
26
56
  function parseSpec(specPath, slug) {
27
57
  let text;
@@ -38,11 +68,12 @@ function parseSpec(specPath, slug) {
38
68
 
39
69
  const statusMatch = front.match(/^status:\s*(.+?)\s*$/m);
40
70
  const status = statusMatch ? statusMatch[1].trim().replace(/^["']|["']$/g, '').toLowerCase() : '';
41
- if (status !== 'done') return null;
71
+ const kind = STATUS_KIND[status];
72
+ if (!kind) return null;
42
73
 
43
74
  const titleMatch = body.match(/^#\s+(.+?)\s*$/m);
44
75
  const rawTitle = titleMatch ? titleMatch[1].trim() : slug;
45
- const title = rawTitle.replace(/\s+specification\s*$/i, '').trim();
76
+ const title = cleanTitle(rawTitle, slug);
46
77
 
47
78
  let summary = '';
48
79
  const purposeHeading = body.match(/^##\s+Purpose\s*$/m);
@@ -58,6 +89,7 @@ function parseSpec(specPath, slug) {
58
89
  slug,
59
90
  title,
60
91
  summary,
92
+ status: kind,
61
93
  docsUrl: `openspec/specs/${slug}/spec.md`,
62
94
  };
63
95
  }