@kolkrabbi/kol-workshop 0.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/README.md +38 -0
- package/package.json +46 -0
- package/src/compositions/WorkshopDefaultSidebar.jsx +89 -0
- package/src/compositions/WorkshopSidebar.jsx +180 -0
- package/src/compositions/index.js +8 -0
- package/src/docs/DocsArticle.jsx +7 -0
- package/src/docs/DocsFrontmatter.jsx +74 -0
- package/src/docs/DocsHeader.jsx +19 -0
- package/src/docs/DocumentationReader.jsx +417 -0
- package/src/docs/index.js +14 -0
- package/src/docs/render-tokens.jsx +98 -0
- package/src/engine/__check.mjs +72 -0
- package/src/engine/build-inventory.js +62 -0
- package/src/engine/doc-helpers.js +132 -0
- package/src/engine/frontmatter.js +41 -0
- package/src/engine/index.js +20 -0
- package/src/engine/parse-markdown.js +352 -0
- package/src/engine/search.js +36 -0
- package/src/engine/tags.js +75 -0
- package/src/index.js +15 -0
- package/src/shell/ShellLayout.jsx +197 -0
- package/src/shell/ShellSidebar.jsx +146 -0
- package/src/shell/index.js +11 -0
- package/src/tags/TagGraph.jsx +243 -0
- package/src/tags/TagModeContext.jsx +105 -0
- package/src/tags/TagModeGate.jsx +17 -0
- package/src/tags/TagModeOverlay.jsx +172 -0
- package/src/tags/index.js +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @kolkrabbi/kol-workshop
|
|
2
|
+
|
|
3
|
+
The KOL **workshop / docs system**, lifted from the monorepo `apps/web` into the design system.
|
|
4
|
+
|
|
5
|
+
A handrolled documentation engine — no `remark`, no `gray-matter`, no `fuse.js` — plus (in later phases) the docs shell, search, and a tag system including a d3 tag graph. Sits above the other four UI packages and consumes them.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
**Phase 1 — the pure engine (this release).** React-free, zero-dependency:
|
|
10
|
+
|
|
11
|
+
| Import | What |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `parseDocsMarkdown(md)` | block + inline markdown → `{ sections, toc, introBlocks, inlineTags }` |
|
|
14
|
+
| `parseFrontmatter(raw)` | YAML-subset frontmatter → object (inline arrays + block lists) |
|
|
15
|
+
| `buildInventory(modules)` | a raw-markdown module map → a doc inventory (see the seam below) |
|
|
16
|
+
| `buildInventoryCounts(inv)` | status / category / content-type tallies |
|
|
17
|
+
| `getTagColor`, `extractDocNumber`, `cleanTitle`, `groupDocsByMajor`, … | doc/tag helpers |
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { parseDocsMarkdown, buildInventory } from '@kolkrabbi/kol-workshop/engine'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Content-injection seam
|
|
24
|
+
|
|
25
|
+
The package is **Vite-agnostic** — it never globs your docs. The consumer supplies the raw-markdown module map:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const modules = import.meta.glob('/docs/**/*.md', { eager: true, query: '?raw', import: 'default' })
|
|
29
|
+
const inventory = buildInventory(modules)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Roadmap
|
|
33
|
+
|
|
34
|
+
- **Phase 2** — the shell + docs viewer + tag-mode components, KOL-conformed (Button / Icon-v1 / no text-transform), chrome CSS moved into `@kolkrabbi/kol-theme`.
|
|
35
|
+
- **Phase 3** — content-injection seam extended to route + search config.
|
|
36
|
+
- **Phase 4** — flip `private` off, publish, repoint the monorepo `apps/web` onto it, delete the local copies.
|
|
37
|
+
|
|
38
|
+
Self-check: `node src/engine/__check.mjs`.
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kolkrabbi/kol-workshop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "KOL workshop/docs system — handrolled markdown engine, search, tag system, and the docs shell. Sits above @kolkrabbi/kol-{theme,icons,component,framework}. Private until the conformance sweep (Phase 4) ships it.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./src/index.js",
|
|
9
|
+
"module": "./src/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js",
|
|
12
|
+
"./engine": "./src/engine/index.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"d3": "^7.9.0",
|
|
16
|
+
"@kolkrabbi/kol-component": "0.7.0",
|
|
17
|
+
"@kolkrabbi/kol-framework": "0.3.2",
|
|
18
|
+
"@kolkrabbi/kol-icons": "0.5.0",
|
|
19
|
+
"@kolkrabbi/kol-theme": "0.7.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "^18.3.0 || ^19.0.0",
|
|
23
|
+
"react-dom": "^18.3.0 || ^19.0.0",
|
|
24
|
+
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"keywords": [
|
|
31
|
+
"kol",
|
|
32
|
+
"kolkrabbi",
|
|
33
|
+
"design-system",
|
|
34
|
+
"docs",
|
|
35
|
+
"markdown",
|
|
36
|
+
"workshop"
|
|
37
|
+
],
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/Tor-Grimsson/kol-ds.git",
|
|
41
|
+
"directory": "packages/workshop"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useLocation, Link } from 'react-router-dom'
|
|
2
|
+
import { Icon } from '@kolkrabbi/kol-component'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WorkshopDefaultSidebar — EXAMPLE composition. The contextual right rail:
|
|
6
|
+
* given the current route it shows sibling pages, the section's repo links, and
|
|
7
|
+
* quick actions. Decoupled from app singletons via props:
|
|
8
|
+
*
|
|
9
|
+
* routes — nav tree (was `WORKSHOP_ROUTES`): [{ id, label, path, children }],
|
|
10
|
+
* where a child may carry `links: { live, repo }`.
|
|
11
|
+
* basePath — mount point of the workshop (drives route matching + hrefs).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const WorkshopDefaultSidebar = ({ routes = [], basePath = '/workshop' }) => {
|
|
15
|
+
const location = useLocation()
|
|
16
|
+
const stripped = location.pathname.startsWith(basePath)
|
|
17
|
+
? location.pathname.slice(basePath.length).replace(/^\//, '')
|
|
18
|
+
: location.pathname
|
|
19
|
+
const firstSegment = stripped.split('/')[0]
|
|
20
|
+
|
|
21
|
+
const parentRoute = routes.find(r => r.path === firstSegment)
|
|
22
|
+
const siblings = parentRoute?.children || []
|
|
23
|
+
const currentChild = siblings.find(c => `${basePath}/${c.path}` === location.pathname)
|
|
24
|
+
const links = currentChild?.links
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="space-y-4">
|
|
28
|
+
{siblings.length > 1 && (
|
|
29
|
+
<div>
|
|
30
|
+
<div className="shell-sidebar-label">{parentRoute.label}</div>
|
|
31
|
+
<nav className="space-y-0">
|
|
32
|
+
{siblings.map(child => (
|
|
33
|
+
<Link
|
|
34
|
+
key={child.id}
|
|
35
|
+
to={`${basePath}/${child.path}`}
|
|
36
|
+
className={`shell-sidebar-link block ${
|
|
37
|
+
location.pathname === `${basePath}/${child.path}` ? 'active' : ''
|
|
38
|
+
}`}
|
|
39
|
+
>
|
|
40
|
+
{child.label}
|
|
41
|
+
</Link>
|
|
42
|
+
))}
|
|
43
|
+
</nav>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{links && (
|
|
48
|
+
<div>
|
|
49
|
+
<div className="shell-sidebar-label">Repository</div>
|
|
50
|
+
<div className="space-y-1">
|
|
51
|
+
{links.live && (
|
|
52
|
+
<a href={links.live} target="_blank" rel="noopener noreferrer" className="shell-sidebar-action">
|
|
53
|
+
<Icon name="external-link" size={14} />
|
|
54
|
+
Live site
|
|
55
|
+
</a>
|
|
56
|
+
)}
|
|
57
|
+
{links.repo && (
|
|
58
|
+
<a href={links.repo} target="_blank" rel="noopener noreferrer" className="shell-sidebar-action">
|
|
59
|
+
<Icon name="external-link" size={14} />
|
|
60
|
+
GitHub
|
|
61
|
+
</a>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
<div>
|
|
68
|
+
<div className="shell-sidebar-label">Quick actions</div>
|
|
69
|
+
<div className="space-y-1">
|
|
70
|
+
<Link to={basePath} className="shell-sidebar-action">
|
|
71
|
+
<Icon name="layout" size={14} />
|
|
72
|
+
Workshop home
|
|
73
|
+
</Link>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className="shell-sidebar-action"
|
|
77
|
+
onClick={() => navigator.clipboard.writeText(window.location.pathname)}
|
|
78
|
+
title="Copy page path to clipboard"
|
|
79
|
+
>
|
|
80
|
+
<Icon name="copy" size={14} />
|
|
81
|
+
Copy path
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default WorkshopDefaultSidebar
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import { Link, useLocation } from 'react-router-dom'
|
|
3
|
+
import { ShellSidebar } from '../shell'
|
|
4
|
+
import { Icon } from '@kolkrabbi/kol-component'
|
|
5
|
+
import {
|
|
6
|
+
extractDocNumber,
|
|
7
|
+
cleanTitle,
|
|
8
|
+
categoryLabels,
|
|
9
|
+
groupDocsByMajor,
|
|
10
|
+
} from '../engine'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* WorkshopSidebar — EXAMPLE composition. This is the app-specific glue that
|
|
14
|
+
* wires the KOL shell + docs engine into a product's primary navigation.
|
|
15
|
+
* Consumers copy this file and adapt it; it is intentionally decoupled from
|
|
16
|
+
* any app singleton via props:
|
|
17
|
+
*
|
|
18
|
+
* routes — nav tree (was `WORKSHOP_ROUTES`): [{ id, label, path, children }]
|
|
19
|
+
* inventory — parsed docs (was `documentationInventory`): [{ id, title, ... }]
|
|
20
|
+
* basePath — mount point of the workshop (drives active-route detection)
|
|
21
|
+
* docHref — builds a doc URL: docHref(id) → doc page; docHref() → docs index.
|
|
22
|
+
* Assumes docs are mounted under `${basePath}/docs/`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DocsSidebar = ({ inventory = [], docHref, basePath, onNavigate, collapsed, onToggle }) => {
|
|
26
|
+
const location = useLocation()
|
|
27
|
+
const activeDocId = useMemo(() => {
|
|
28
|
+
const prefix = `${basePath}/docs/`
|
|
29
|
+
if (!location.pathname.startsWith(prefix)) return null
|
|
30
|
+
const rest = location.pathname.slice(prefix.length)
|
|
31
|
+
return rest.length > 0 ? rest : null
|
|
32
|
+
}, [location.pathname, basePath])
|
|
33
|
+
|
|
34
|
+
const groupedDocs = useMemo(() => groupDocsByMajor(inventory), [inventory])
|
|
35
|
+
|
|
36
|
+
const [collapsedGroups, setCollapsedGroups] = useState(() => {
|
|
37
|
+
const initialState = {}
|
|
38
|
+
Object.keys(groupedDocs).forEach((major) => {
|
|
39
|
+
initialState[major] = true
|
|
40
|
+
})
|
|
41
|
+
return initialState
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Auto-expand the group containing the active doc
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!activeDocId) return
|
|
47
|
+
for (const [major, docs] of Object.entries(groupedDocs)) {
|
|
48
|
+
if (docs.some(d => d.id === activeDocId)) {
|
|
49
|
+
setCollapsedGroups(prev => ({ ...prev, [major]: false }))
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}, [activeDocId, groupedDocs])
|
|
54
|
+
|
|
55
|
+
const toggleGroup = (major) => {
|
|
56
|
+
setCollapsedGroups(prev => ({
|
|
57
|
+
...prev,
|
|
58
|
+
[major]: !prev[major]
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-4">
|
|
64
|
+
<div className="shell-sidebar-toggle shell-sidebar-label" style={{ justifyContent: 'space-between', paddingRight: '4px' }}>
|
|
65
|
+
<Link to={docHref()} className="shell-sidebar-label" onClick={(e) => {
|
|
66
|
+
if (collapsed && onToggle) onToggle()
|
|
67
|
+
if (onNavigate) onNavigate(e)
|
|
68
|
+
}}>
|
|
69
|
+
Documentation
|
|
70
|
+
</Link>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={onToggle}
|
|
74
|
+
aria-label={collapsed ? 'Expand Documentation' : 'Collapse Documentation'}
|
|
75
|
+
className="flex items-center justify-center"
|
|
76
|
+
style={{ height: '16.5px', marginBottom: '8px' }}
|
|
77
|
+
>
|
|
78
|
+
<Icon
|
|
79
|
+
name="chevron-down"
|
|
80
|
+
size={10}
|
|
81
|
+
className={`stroke-[2.5] transition-transform ${collapsed ? '' : 'rotate-180'}`}
|
|
82
|
+
/>
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{!collapsed && <div className="space-y-4">
|
|
87
|
+
{Object.entries(groupedDocs)
|
|
88
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
89
|
+
.map(([major, docs]) => {
|
|
90
|
+
const isCollapsed = collapsedGroups[major]
|
|
91
|
+
return (
|
|
92
|
+
<div key={major} className="shell-nav-group">
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
className="shell-nav-group-header w-full text-left"
|
|
96
|
+
onClick={() => toggleGroup(major)}
|
|
97
|
+
>
|
|
98
|
+
<span className="flex items-center gap-2">
|
|
99
|
+
<Icon
|
|
100
|
+
name="chevron-right"
|
|
101
|
+
size={12}
|
|
102
|
+
className={`h-3 w-3 transition-transform ${isCollapsed ? '' : 'rotate-90'}`}
|
|
103
|
+
/>
|
|
104
|
+
{categoryLabels[major] || 'Other'}
|
|
105
|
+
</span>
|
|
106
|
+
<span className="shell-nav-group-count">({docs.length})</span>
|
|
107
|
+
</button>
|
|
108
|
+
{!isCollapsed && (
|
|
109
|
+
<div className="shell-nav-items">
|
|
110
|
+
{docs.map((d) => {
|
|
111
|
+
const isActive = d.id === activeDocId
|
|
112
|
+
return (
|
|
113
|
+
<Link
|
|
114
|
+
key={d.id}
|
|
115
|
+
to={docHref(d.id)}
|
|
116
|
+
className={`shell-nav-item shell-nav-item-doc ${isActive ? 'active' : ''}`}
|
|
117
|
+
onClick={onNavigate}
|
|
118
|
+
>
|
|
119
|
+
<span className="shell-nav-item-title">{cleanTitle(d.title, d.id)}</span>
|
|
120
|
+
<span className="shell-nav-item-id">{extractDocNumber(d.id)}</span>
|
|
121
|
+
</Link>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
})}
|
|
129
|
+
</div>}
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const WorkshopSidebar = ({
|
|
135
|
+
routes = [],
|
|
136
|
+
inventory = [],
|
|
137
|
+
basePath = '/workshop',
|
|
138
|
+
docHref = (id) => (id ? `${basePath}/docs/${id}` : `${basePath}/docs`),
|
|
139
|
+
onNavigate,
|
|
140
|
+
}) => {
|
|
141
|
+
const location = useLocation()
|
|
142
|
+
const [workshopCollapsed, setWorkshopCollapsed] = useState(false)
|
|
143
|
+
const [docsCollapsed, setDocsCollapsed] = useState(true)
|
|
144
|
+
|
|
145
|
+
// Auto-expand Documentation section when on a docs route
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (location.pathname.startsWith(`${basePath}/docs`)) {
|
|
148
|
+
setDocsCollapsed(false)
|
|
149
|
+
}
|
|
150
|
+
}, [location.pathname, basePath])
|
|
151
|
+
|
|
152
|
+
const workshopRoutes = useMemo(
|
|
153
|
+
() => routes.filter(r => r.id !== 'docs'),
|
|
154
|
+
[routes]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="space-y-6">
|
|
159
|
+
<ShellSidebar
|
|
160
|
+
routes={workshopRoutes}
|
|
161
|
+
basePath={basePath}
|
|
162
|
+
onNavigate={onNavigate}
|
|
163
|
+
label="Workshop"
|
|
164
|
+
labelTo={basePath}
|
|
165
|
+
collapsed={workshopCollapsed}
|
|
166
|
+
onToggle={() => setWorkshopCollapsed(p => !p)}
|
|
167
|
+
/>
|
|
168
|
+
<DocsSidebar
|
|
169
|
+
inventory={inventory}
|
|
170
|
+
docHref={docHref}
|
|
171
|
+
basePath={basePath}
|
|
172
|
+
onNavigate={onNavigate}
|
|
173
|
+
collapsed={docsCollapsed}
|
|
174
|
+
onToggle={() => setDocsCollapsed(p => !p)}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default WorkshopSidebar
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example compositions — the app-specific glue that wires the KOL shell + docs
|
|
3
|
+
* engine into a product's navigation. These are reference implementations meant
|
|
4
|
+
* to be copied and adapted; they take app data (routes, inventory) and href
|
|
5
|
+
* helpers (basePath, docHref) as props rather than importing any singleton.
|
|
6
|
+
*/
|
|
7
|
+
export { default as WorkshopSidebar } from './WorkshopSidebar.jsx'
|
|
8
|
+
export { default as WorkshopDefaultSidebar } from './WorkshopDefaultSidebar.jsx'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Icon, Tag } from '@kolkrabbi/kol-component'
|
|
2
|
+
import { getTagColor } from '../engine'
|
|
3
|
+
import { useTagMode } from '../tags'
|
|
4
|
+
|
|
5
|
+
const FIELD_ICONS = {
|
|
6
|
+
file: 'file',
|
|
7
|
+
title: null,
|
|
8
|
+
version: 'hash-01',
|
|
9
|
+
date: null,
|
|
10
|
+
status: 'check',
|
|
11
|
+
'content-type': null,
|
|
12
|
+
category: 'folder',
|
|
13
|
+
tags: null,
|
|
14
|
+
modified: null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FIELD_ORDER = ['title', 'category', 'date', 'tags', 'modified']
|
|
18
|
+
|
|
19
|
+
const formatDate = (dateStr) => {
|
|
20
|
+
if (!dateStr) return null
|
|
21
|
+
const d = new Date(dateStr)
|
|
22
|
+
if (isNaN(d)) return dateStr
|
|
23
|
+
return d.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }).replace(/\//g, '.')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DocsFrontmatter = ({ metadata, docId }) => {
|
|
27
|
+
const { openTagMode } = useTagMode()
|
|
28
|
+
if (!metadata || Object.keys(metadata).length === 0) return null
|
|
29
|
+
|
|
30
|
+
const fields = FIELD_ORDER.filter(key => metadata[key] != null && metadata[key] !== '')
|
|
31
|
+
|
|
32
|
+
if (fields.length === 0) return null
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="docs-frontmatter">
|
|
36
|
+
<p className="shell-sidebar-label">Frontmatter</p>
|
|
37
|
+
{fields.map((key) => {
|
|
38
|
+
const value = metadata[key]
|
|
39
|
+
const icon = FIELD_ICONS[key]
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div key={key} className="docs-frontmatter-row">
|
|
43
|
+
<span className="docs-frontmatter-key">
|
|
44
|
+
{icon && <Icon name={icon} size={14} />}
|
|
45
|
+
{key}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="docs-frontmatter-value">
|
|
48
|
+
{key === 'tags' && Array.isArray(value) ? (
|
|
49
|
+
<span className="flex flex-wrap gap-1.5">
|
|
50
|
+
{value.map((tag) => (
|
|
51
|
+
<Tag
|
|
52
|
+
key={tag}
|
|
53
|
+
size="sm"
|
|
54
|
+
color={getTagColor(tag)}
|
|
55
|
+
onClick={() => openTagMode(tag)}
|
|
56
|
+
>
|
|
57
|
+
{tag}
|
|
58
|
+
</Tag>
|
|
59
|
+
))}
|
|
60
|
+
</span>
|
|
61
|
+
) : key === 'date' || key === 'modified' ? (
|
|
62
|
+
formatDate(String(value))
|
|
63
|
+
) : (
|
|
64
|
+
String(value)
|
|
65
|
+
)}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
})}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default DocsFrontmatter
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const join = (...classes) => classes.filter(Boolean).join(' ')
|
|
2
|
+
|
|
3
|
+
const DocsHeader = ({ title, subtitle, meta = [], className }) => (
|
|
4
|
+
<header className={join('docs-header', className)}>
|
|
5
|
+
{title ? <h1 className="docs-title">{title}</h1> : null}
|
|
6
|
+
{subtitle ? <p className="docs-subtitle">{subtitle}</p> : null}
|
|
7
|
+
{Array.isArray(meta) && meta.length > 0 ? (
|
|
8
|
+
<div className="docs-meta">
|
|
9
|
+
{meta.map(({ label, value }) => (
|
|
10
|
+
<span key={label}>
|
|
11
|
+
<strong>{label}</strong> {value}
|
|
12
|
+
</span>
|
|
13
|
+
))}
|
|
14
|
+
</div>
|
|
15
|
+
) : null}
|
|
16
|
+
</header>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export default DocsHeader
|