@docpensieve/components 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/LICENSE +21 -0
- package/README.md +34 -0
- package/package.json +52 -0
- package/src/card.js +94 -0
- package/src/classes.js +90 -0
- package/src/columns.js +124 -0
- package/src/index.js +23 -0
- package/src/logo-icon.js +125 -0
- package/src/registry.js +62 -0
- package/src/scroll-to-top.js +65 -0
- package/src/site.js +110 -0
- package/src/skill.js +200 -0
- package/src/styles.js +35 -0
- package/src/time-timer.js +251 -0
- package/src/tooltip.js +89 -0
- package/src/tree.js +88 -0
- package/styles/components.css +590 -0
- package/types/card.d.ts +72 -0
- package/types/classes.d.ts +54 -0
- package/types/columns.d.ts +56 -0
- package/types/index.d.ts +22 -0
- package/types/logo-icon.d.ts +39 -0
- package/types/registry.d.ts +29 -0
- package/types/scroll-to-top.d.ts +32 -0
- package/types/site.d.ts +68 -0
- package/types/skill.d.ts +46 -0
- package/types/styles.d.ts +16 -0
- package/types/time-timer.d.ts +61 -0
- package/types/tooltip.d.ts +34 -0
- package/types/tree.d.ts +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valentin Chevoleau
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @docpensieve/components
|
|
2
|
+
|
|
3
|
+
> Global MDX components
|
|
4
|
+
|
|
5
|
+
Part of [DocPensieve](https://github.com/Juniors017/docpensieve), a static documentation site generator:
|
|
6
|
+
Markdown and MDX in, static HTML out, one version per orphan branch, JSON-LD
|
|
7
|
+
structured data from the frontmatter.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @docpensieve/components
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
The components registered here are injected into the MDX compilation: they
|
|
18
|
+
can be used in any page **without an import**.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { createRegistry } from '@docpensieve/components';
|
|
22
|
+
|
|
23
|
+
const components = createRegistry({ MyBlock });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
React is a build-only dependency.
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
See the [repository](https://github.com/Juniors017/docpensieve#readme).
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@docpensieve/components",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DocPensieve global MDX components, usable without import: Card, Columns, Tooltip, Tree, Skill, TimeTimer, LogoIcon, ScrollToTop",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.0.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./types/index.d.ts",
|
|
14
|
+
"default": "./src/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"styles",
|
|
20
|
+
"types"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@docpensieve/shared": "^0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^19"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"docpensieve",
|
|
30
|
+
"mdx",
|
|
31
|
+
"components",
|
|
32
|
+
"react",
|
|
33
|
+
"documentation"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Juniors017/docpensieve.git",
|
|
38
|
+
"directory": "packages/components"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Juniors017/docpensieve#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/Juniors017/docpensieve/issues"
|
|
43
|
+
},
|
|
44
|
+
"author": "Valentin Chevoleau (Juniors017)",
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"types": "./types/index.d.ts",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"prepack": "tsc -b tsconfig.build.json"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/card.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card and its parts.
|
|
3
|
+
*
|
|
4
|
+
* The component provides the **structure** — the wrapper, the separators, the
|
|
5
|
+
* spacing. The look is set through `className`, with the utilities of the
|
|
6
|
+
* active theme. One prop per typographic setting would mean reimplementing,
|
|
7
|
+
* less well, what CSS already does.
|
|
8
|
+
*
|
|
9
|
+
* Classes come from the theme (ADR-007), never from a hard-coded framework.
|
|
10
|
+
*
|
|
11
|
+
* The files of this package are plain JavaScript, with no build step: no JSX,
|
|
12
|
+
* hence `createElement` — shortened to `h` for readability.
|
|
13
|
+
*
|
|
14
|
+
* @module @docpensieve/components/card
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createElement as h } from 'react';
|
|
18
|
+
|
|
19
|
+
import { classNames, cls } from './classes.js';
|
|
20
|
+
import { resolveUrl } from './site.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} PartProps
|
|
24
|
+
* @property {string} [className] Classes added to the component's own.
|
|
25
|
+
* @property {object} [style]
|
|
26
|
+
* @property {any} [children]
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Makes a card part: header, body or footer.
|
|
31
|
+
*
|
|
32
|
+
* The three only differ by their theme slot.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} slot Theme slot, `'cardHeader'` for instance.
|
|
35
|
+
* @param {string} name Name shown in errors and rendering tools.
|
|
36
|
+
* @returns {(props: PartProps) => any}
|
|
37
|
+
*/
|
|
38
|
+
function cardPart(slot, name) {
|
|
39
|
+
/** @param {PartProps} props */
|
|
40
|
+
function Part({ className, style, children }) {
|
|
41
|
+
return h('div', { className: classNames(cls(slot), className), style }, children);
|
|
42
|
+
}
|
|
43
|
+
Object.defineProperty(Part, 'name', { value: name });
|
|
44
|
+
return Part;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Card container.
|
|
49
|
+
*
|
|
50
|
+
* @param {PartProps & { elevated?: boolean, href?: string }} props
|
|
51
|
+
* `elevated` adds a shadow. `href` makes the whole card clickable, rather
|
|
52
|
+
* than a link on the title alone that would leave the rest inert.
|
|
53
|
+
*/
|
|
54
|
+
export function Card({ className, style, children, elevated = false, href }) {
|
|
55
|
+
return h(
|
|
56
|
+
href ? 'a' : 'div',
|
|
57
|
+
{
|
|
58
|
+
className: classNames(cls('card', elevated && 'elevated'), className),
|
|
59
|
+
style,
|
|
60
|
+
// A link produced by a component escapes the compiler plugins: it
|
|
61
|
+
// resolves itself, following the same rules (ADR-006).
|
|
62
|
+
...(href ? { href: resolveUrl(href) } : {}),
|
|
63
|
+
},
|
|
64
|
+
children,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const CardHeader = cardPart('cardHeader', 'CardHeader');
|
|
69
|
+
export const CardBody = cardPart('cardBody', 'CardBody');
|
|
70
|
+
export const CardFooter = cardPart('cardFooter', 'CardFooter');
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Image at the top of a card.
|
|
74
|
+
*
|
|
75
|
+
* `src` resolves as in Markdown — relative to the page, absolute from the
|
|
76
|
+
* version root. The compiler plugins cannot handle it: they work on the
|
|
77
|
+
* Markdown tree, before React renders anything. So the component does it
|
|
78
|
+
* itself (ADR-006).
|
|
79
|
+
*
|
|
80
|
+
* @param {{
|
|
81
|
+
* className?: string, style?: object, src?: string,
|
|
82
|
+
* alt?: string, title?: string, srcSet?: string, sizes?: string,
|
|
83
|
+
* }} props `alt` defaults to the empty string: without that attribute, a
|
|
84
|
+
* screen reader would announce the file URL.
|
|
85
|
+
*/
|
|
86
|
+
export function CardImage({ className, style, src, alt = '', ...rest }) {
|
|
87
|
+
return h('img', {
|
|
88
|
+
className: classNames(cls('cardImage'), className),
|
|
89
|
+
style,
|
|
90
|
+
src: resolveUrl(src),
|
|
91
|
+
alt,
|
|
92
|
+
...rest,
|
|
93
|
+
});
|
|
94
|
+
}
|
package/src/classes.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution of component classes.
|
|
3
|
+
*
|
|
4
|
+
* A component never writes a framework class. It asks for the class of its
|
|
5
|
+
* slot, and the theme answers — or not. Without an answer, the fallback is a
|
|
6
|
+
* `dp-*` class that the package stylesheet styles on the `--dp-*` tokens: the
|
|
7
|
+
* component therefore follows the active palette without knowing anything
|
|
8
|
+
* about it (ADR-007).
|
|
9
|
+
*
|
|
10
|
+
* @module @docpensieve/components/classes
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Table supplied by the theme, set once by the generator.
|
|
15
|
+
*
|
|
16
|
+
* Module state rather than a React context: MDX compilation happens in a
|
|
17
|
+
* single process, with a single configuration, and a context would force
|
|
18
|
+
* `core` to know this package — which ADR-002 forbids.
|
|
19
|
+
*
|
|
20
|
+
* @type {Record<string, string>}
|
|
21
|
+
*/
|
|
22
|
+
let themeClasses = {};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Declares the theme table for the whole compilation.
|
|
26
|
+
*
|
|
27
|
+
* @param {Record<string, string>} [classes]
|
|
28
|
+
*/
|
|
29
|
+
export function setThemeClasses(classes = {}) {
|
|
30
|
+
themeClasses = classes;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @returns {Record<string, string>} The current table, for inspection. */
|
|
34
|
+
export function getThemeClasses() {
|
|
35
|
+
return themeClasses;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Converts a slot name into a fallback class.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* fallbackClass('cardHeader') // 'dp-card-header'
|
|
43
|
+
*
|
|
44
|
+
* @param {string} slot
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function fallbackClass(slot) {
|
|
48
|
+
return `dp-${slot.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Class of a slot, variants included.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* cls('card') // 'dp-card'
|
|
56
|
+
* cls('card', 'shadow') // 'dp-card dp-card--shadow'
|
|
57
|
+
* cls('alert', false && 'x') // 'dp-alert' — falsy values are ignored
|
|
58
|
+
*
|
|
59
|
+
* @param {string} slot Slot name.
|
|
60
|
+
* @param {...unknown} modifiers Variants, each one suffixed as `--variant`.
|
|
61
|
+
* Falsy values are ignored, which allows writing `cls('card', shadow && shadow)`.
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
export function cls(slot, ...modifiers) {
|
|
65
|
+
const base = themeClasses[slot] ?? fallbackClass(slot);
|
|
66
|
+
const root = fallbackClass(slot);
|
|
67
|
+
|
|
68
|
+
// `filter(Boolean)` lets 0 through: a `span && ...` on a zero number would
|
|
69
|
+
// produce “--0”. Only non-empty strings are kept.
|
|
70
|
+
const variants = modifiers
|
|
71
|
+
.filter((modifier) => typeof modifier === 'string' && modifier !== '')
|
|
72
|
+
.map((modifier) => themeClasses[`${slot}.${modifier}`] ?? `${root}--${modifier}`);
|
|
73
|
+
|
|
74
|
+
return [base, ...variants].join(' ');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Joins classes while ignoring falsy values.
|
|
79
|
+
*
|
|
80
|
+
* A minimal equivalent of `clsx`: one more dependency is not worth it for six
|
|
81
|
+
* lines.
|
|
82
|
+
*
|
|
83
|
+
* @param {...unknown} parts
|
|
84
|
+
* @returns {string | undefined} `undefined` when nothing is left, to avoid a
|
|
85
|
+
* `class=""` in the produced HTML.
|
|
86
|
+
*/
|
|
87
|
+
export function classNames(...parts) {
|
|
88
|
+
const kept = parts.filter(Boolean).join(' ').trim();
|
|
89
|
+
return kept === '' ? undefined : kept;
|
|
90
|
+
}
|
package/src/columns.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column grid.
|
|
3
|
+
*
|
|
4
|
+
* Not to be confused with CSS multi-column (`column-count`), where content
|
|
5
|
+
* *flows* from one column to the next as in a newspaper. Here every block is
|
|
6
|
+
* placed explicitly: it is a grid, not a flow. For a flow, the theme's
|
|
7
|
+
* utilities are enough, without a component.
|
|
8
|
+
*
|
|
9
|
+
* Two layouts, depending on what the row holds:
|
|
10
|
+
*
|
|
11
|
+
* - **no declared width** — the columns share the space equally, whatever
|
|
12
|
+
* their number;
|
|
13
|
+
* - **declared widths** — twelve tracks, and each column takes the number it
|
|
14
|
+
* asks for.
|
|
15
|
+
*
|
|
16
|
+
* The grid works out the gap between columns by itself: a width therefore has
|
|
17
|
+
* no calculation to make, and changing the gap breaks nothing.
|
|
18
|
+
*
|
|
19
|
+
* Classes come from the theme (ADR-007), never from a hard-coded framework.
|
|
20
|
+
*
|
|
21
|
+
* @module @docpensieve/components/columns
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Children, createContext, createElement as h, isValidElement, useContext } from 'react';
|
|
25
|
+
|
|
26
|
+
import { DocPensieveError } from '@docpensieve/shared';
|
|
27
|
+
|
|
28
|
+
import { classNames, cls } from './classes.js';
|
|
29
|
+
|
|
30
|
+
/** Number of tracks when widths are declared. */
|
|
31
|
+
const TRACKS = 12;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What a column learns from its row.
|
|
35
|
+
*
|
|
36
|
+
* `null` outside a row: without this landmark, a lone `Column` rendered a
|
|
37
|
+
* grid cell without a grid — full width, with no error or warning. A tag that
|
|
38
|
+
* silently does nothing is what this project sets out to avoid.
|
|
39
|
+
*
|
|
40
|
+
* @type {import('react').Context<{ sized: boolean } | null>}
|
|
41
|
+
*/
|
|
42
|
+
const Row = createContext(/** @type {{ sized: boolean } | null} */ (null));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tells whether at least one column of the row declares a width.
|
|
46
|
+
*
|
|
47
|
+
* The walk goes down into the children: depending on how the author spaces
|
|
48
|
+
* out their MDX, the columns are not always direct children of the row.
|
|
49
|
+
*
|
|
50
|
+
* @param {any} children
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
function hasSizedColumn(children) {
|
|
54
|
+
return Children.toArray(children).some((child) => {
|
|
55
|
+
if (!isValidElement(child)) return false;
|
|
56
|
+
// `props` is not typed on an arbitrary element: open it here rather than
|
|
57
|
+
// impose a shape on everything an author can write.
|
|
58
|
+
const props = /** @type {{ span?: number, children?: any }} */ (child.props);
|
|
59
|
+
if (child.type === Column) return props.span !== undefined;
|
|
60
|
+
return props?.children !== undefined && hasSizedColumn(props.children);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Row of columns.
|
|
66
|
+
*
|
|
67
|
+
* The gap is set through `className` or `style`, with the theme's utilities:
|
|
68
|
+
* the grid recomputes the widths by itself.
|
|
69
|
+
*
|
|
70
|
+
* @param {{ className?: string, style?: object, children?: any }} props
|
|
71
|
+
*/
|
|
72
|
+
export function Columns({ className, style, children }) {
|
|
73
|
+
const sized = hasSizedColumn(children);
|
|
74
|
+
|
|
75
|
+
return h(
|
|
76
|
+
Row.Provider,
|
|
77
|
+
{ value: { sized } },
|
|
78
|
+
h(
|
|
79
|
+
'div',
|
|
80
|
+
{ className: classNames(cls('columns', sized && 'twelfths'), className), style },
|
|
81
|
+
children,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Column of a row.
|
|
88
|
+
*
|
|
89
|
+
* @param {{ className?: string, style?: object, children?: any, span?: number }} props
|
|
90
|
+
* `span` is the number of tracks taken out of twelve — `span={6}` for a
|
|
91
|
+
* half, `span={8}` for two thirds. Twelve because twelve divides by two,
|
|
92
|
+
* three, four and six. Without `span`, the columns share the space equally.
|
|
93
|
+
* @throws {DocPensieveError} Outside a `Columns`, or when the row mixes
|
|
94
|
+
* columns with and without a width.
|
|
95
|
+
*/
|
|
96
|
+
export function Column({ className, style, children, span }) {
|
|
97
|
+
const row = useContext(Row);
|
|
98
|
+
|
|
99
|
+
if (row === null) {
|
|
100
|
+
throw new DocPensieveError('A <Column> was written outside a <Columns>.', {
|
|
101
|
+
hint: 'Wrap the columns: <Columns><Column>…</Column></Columns>.',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (span !== undefined && (!Number.isInteger(span) || span < 1 || span > TRACKS)) {
|
|
106
|
+
throw new DocPensieveError(`Invalid column width: "${span}".`, {
|
|
107
|
+
hint: `span expects an integer from 1 to ${TRACKS}, the number of tracks taken out of ${TRACKS}.`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A column without a width in a row that declares some would take a single
|
|
112
|
+
// track out of twelve: a sliver, where the author expected a column.
|
|
113
|
+
if (row.sized && span === undefined) {
|
|
114
|
+
throw new DocPensieveError('A <Column> without a width in a row that declares some.', {
|
|
115
|
+
hint: `Give a span to every column of the row, or to none. The total is ${TRACKS}.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return h(
|
|
120
|
+
'div',
|
|
121
|
+
{ className: classNames(cls('column', span && `span-${span}`), className), style },
|
|
122
|
+
children,
|
|
123
|
+
);
|
|
124
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docpensieve/components — global components available in MDX.
|
|
3
|
+
*
|
|
4
|
+
* React is a build-only dependency: the generated HTML loads no React
|
|
5
|
+
* runtime. The components are therefore **static** — no state, no event
|
|
6
|
+
* listener. Whatever needs interaction goes through CSS or through native
|
|
7
|
+
* HTML elements such as `<details>`.
|
|
8
|
+
*
|
|
9
|
+
* @module @docpensieve/components
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { classNames, cls, fallbackClass, getThemeClasses, setThemeClasses } from './classes.js';
|
|
13
|
+
export { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
|
|
14
|
+
export { Column, Columns } from './columns.js';
|
|
15
|
+
export { FallbackAfter, FallbackBefore, TimeTimer } from './time-timer.js';
|
|
16
|
+
export { TOOLTIP_PLACEMENTS, Tooltip } from './tooltip.js';
|
|
17
|
+
export { Tree, TreeItem } from './tree.js';
|
|
18
|
+
export { ScrollToTop } from './scroll-to-top.js';
|
|
19
|
+
export { SKILL_SHAPES, Skill } from './skill.js';
|
|
20
|
+
export { LogoIcon } from './logo-icon.js';
|
|
21
|
+
export { componentsCss } from './styles.js';
|
|
22
|
+
export { getSiteContext, resolveFile, resolveUrl, setSiteContext } from './site.js';
|
|
23
|
+
export { builtinComponents, createRegistry, listComponentNames } from './registry.js';
|
package/src/logo-icon.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG icon inlined at build time.
|
|
3
|
+
*
|
|
4
|
+
* The file is read once and its content placed in the page. That is what
|
|
5
|
+
* lets it take its colour from `currentColor` and be styled like everything
|
|
6
|
+
* else — impossible through an `img` tag, which isolates the document.
|
|
7
|
+
*
|
|
8
|
+
* @module @docpensieve/components/logo-icon
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { createElement as h } from 'react';
|
|
13
|
+
|
|
14
|
+
import { DocPensieveError } from '@docpensieve/shared';
|
|
15
|
+
|
|
16
|
+
import { classNames, cls } from './classes.js';
|
|
17
|
+
import { resolveFile } from './site.js';
|
|
18
|
+
|
|
19
|
+
/** Isolates the `svg` root: XML header, doctype and comments stay out. */
|
|
20
|
+
const SVG_ROOT = /<svg\b[\s\S]*<\/svg>/i;
|
|
21
|
+
|
|
22
|
+
/** A script embedded in an SVG would run in the page. */
|
|
23
|
+
const SCRIPT = /<script\b[\s\S]*?<\/script\s*>/gi;
|
|
24
|
+
|
|
25
|
+
/** Event handlers written as attributes, for the same reason. */
|
|
26
|
+
const HANDLER = /\son[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Files already read, by absolute path.
|
|
30
|
+
*
|
|
31
|
+
* The same icon often comes back within a page and from one page to the
|
|
32
|
+
* next: without this cache, every occurrence would read the disk again.
|
|
33
|
+
*
|
|
34
|
+
* @type {Map<string, string>}
|
|
35
|
+
*/
|
|
36
|
+
const cache = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reads an SVG and strips what would run.
|
|
40
|
+
*
|
|
41
|
+
* The file comes from the project, so from a trusted source; the stripping
|
|
42
|
+
* mostly matters for an icon fetched elsewhere and dropped in without review.
|
|
43
|
+
* The produced site loads no JavaScript, and a component is not going to
|
|
44
|
+
* introduce any.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} src Path written by the author.
|
|
47
|
+
* @returns {string} The content of the `svg` tag.
|
|
48
|
+
* @throws {DocPensieveError} File not found, or without an `svg` tag.
|
|
49
|
+
*/
|
|
50
|
+
function readSvg(src) {
|
|
51
|
+
/** @type {string} */
|
|
52
|
+
let file;
|
|
53
|
+
try {
|
|
54
|
+
file = resolveFile(src);
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
throw new DocPensieveError(
|
|
57
|
+
`Icon "${src}": ${(cause instanceof Error && cause.message) || cause}`,
|
|
58
|
+
{
|
|
59
|
+
cause,
|
|
60
|
+
hint: 'A relative path starts from the page, an absolute path from the version folder.',
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cached = cache.get(file);
|
|
66
|
+
if (cached !== undefined) return cached;
|
|
67
|
+
|
|
68
|
+
/** @type {string} */
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = readFileSync(file, 'utf8');
|
|
72
|
+
} catch (cause) {
|
|
73
|
+
throw new DocPensieveError(`Icon not found: "${src}".`, {
|
|
74
|
+
cause,
|
|
75
|
+
hint: `Expected file here: ${file}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const match = raw.match(SVG_ROOT);
|
|
80
|
+
if (!match) {
|
|
81
|
+
throw new DocPensieveError(`The file "${src}" contains no <svg> tag.`, {
|
|
82
|
+
hint: 'LogoIcon expects an SVG file.',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const clean = match[0].replace(SCRIPT, '').replace(HANDLER, '');
|
|
87
|
+
cache.set(file, clean);
|
|
88
|
+
return clean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Project SVG icon, inlined in the page.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* <LogoIcon src="./icons/book.svg" label="Documentation" />
|
|
96
|
+
*
|
|
97
|
+
* @param {{
|
|
98
|
+
* className?: string, style?: object,
|
|
99
|
+
* src?: string, label?: string, size?: string,
|
|
100
|
+
* }} props `label` describes the icon; without it the icon is treated as
|
|
101
|
+
* decorative and hidden from screen readers — which is right when nearby
|
|
102
|
+
* text already says the same thing. `size` accepts any CSS length.
|
|
103
|
+
* @throws {DocPensieveError} Without `src`, or when the file cannot be read.
|
|
104
|
+
*/
|
|
105
|
+
export function LogoIcon({ className, style, src, label, size }) {
|
|
106
|
+
if (typeof src !== 'string' || src === '') {
|
|
107
|
+
throw new DocPensieveError('A <LogoIcon> without src.', {
|
|
108
|
+
hint: 'Give the path of the file: <LogoIcon src="./logo.svg" />.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const decorative = label === undefined || label === null || label === '';
|
|
113
|
+
|
|
114
|
+
return h('span', {
|
|
115
|
+
className: classNames(cls('logoIcon'), className),
|
|
116
|
+
style: size ? { '--dp-logo-icon-size': size, ...style } : style,
|
|
117
|
+
role: decorative ? undefined : 'img',
|
|
118
|
+
'aria-label': decorative ? undefined : label,
|
|
119
|
+
'aria-hidden': decorative ? 'true' : undefined,
|
|
120
|
+
// The only way to put an SVG read from disk into the rendered tree. Its
|
|
121
|
+
// content comes from the project sources and has been stripped of any
|
|
122
|
+
// script.
|
|
123
|
+
dangerouslySetInnerHTML: { __html: readSvg(src) },
|
|
124
|
+
});
|
|
125
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of global components.
|
|
3
|
+
*
|
|
4
|
+
* The components registered here are passed to @mdx-js/mdx as the
|
|
5
|
+
* `components` table: they become usable in any `.mdx` without an import.
|
|
6
|
+
*
|
|
7
|
+
* @module @docpensieve/components/registry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
|
|
11
|
+
import { Column, Columns } from './columns.js';
|
|
12
|
+
import { LogoIcon } from './logo-icon.js';
|
|
13
|
+
import { ScrollToTop } from './scroll-to-top.js';
|
|
14
|
+
import { Skill } from './skill.js';
|
|
15
|
+
import { FallbackAfter, FallbackBefore, TimeTimer } from './time-timer.js';
|
|
16
|
+
import { Tooltip } from './tooltip.js';
|
|
17
|
+
import { Tree, TreeItem } from './tree.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Components shipped with DocPensieve.
|
|
21
|
+
* @type {Record<string, Function>}
|
|
22
|
+
*/
|
|
23
|
+
export const builtinComponents = {
|
|
24
|
+
Card,
|
|
25
|
+
CardHeader,
|
|
26
|
+
CardBody,
|
|
27
|
+
CardFooter,
|
|
28
|
+
CardImage,
|
|
29
|
+
Columns,
|
|
30
|
+
Column,
|
|
31
|
+
TimeTimer,
|
|
32
|
+
FallbackBefore,
|
|
33
|
+
FallbackAfter,
|
|
34
|
+
Tooltip,
|
|
35
|
+
Tree,
|
|
36
|
+
TreeItem,
|
|
37
|
+
ScrollToTop,
|
|
38
|
+
Skill,
|
|
39
|
+
LogoIcon,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds the component table passed to the MDX compiler.
|
|
44
|
+
*
|
|
45
|
+
* @param {Record<string, Function>} [userComponents] Project components, which
|
|
46
|
+
* override the built-in components of the same name.
|
|
47
|
+
* @returns {Record<string, Function>} Table ready for @mdx-js/mdx.
|
|
48
|
+
*/
|
|
49
|
+
export function createRegistry(userComponents = {}) {
|
|
50
|
+
return { ...builtinComponents, ...userComponents };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Lists the available component names — useful for a readable error message
|
|
55
|
+
* when an `.mdx` references an unknown component.
|
|
56
|
+
*
|
|
57
|
+
* @param {Record<string, Function>} registry
|
|
58
|
+
* @returns {string[]} Names sorted alphabetically.
|
|
59
|
+
*/
|
|
60
|
+
export function listComponentNames(registry) {
|
|
61
|
+
return Object.keys(registry).sort();
|
|
62
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Back to the top of the page.
|
|
3
|
+
*
|
|
4
|
+
* No JavaScript: a link to the `top` fragment, which the HTML specification
|
|
5
|
+
* reserves for the top of the document when no element carries that
|
|
6
|
+
* identifier. The button therefore works without adding anything to the page.
|
|
7
|
+
*
|
|
8
|
+
* It only shows once the page has scrolled, through `animation-timeline`.
|
|
9
|
+
* Where the browser ignores it, it simply stays visible: a button always there
|
|
10
|
+
* is better than a button never there.
|
|
11
|
+
*
|
|
12
|
+
* @module @docpensieve/components/scroll-to-top
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createElement as h } from 'react';
|
|
16
|
+
|
|
17
|
+
import { classNames, cls } from './classes.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Arrow of the button.
|
|
21
|
+
*
|
|
22
|
+
* Drawn here rather than written as a character: a typographic chevron
|
|
23
|
+
* changes shape and alignment from one font to another.
|
|
24
|
+
*/
|
|
25
|
+
function Chevron() {
|
|
26
|
+
return h(
|
|
27
|
+
'svg',
|
|
28
|
+
{
|
|
29
|
+
className: cls('scrollTopIcon'),
|
|
30
|
+
viewBox: '0 0 24 24',
|
|
31
|
+
fill: 'none',
|
|
32
|
+
stroke: 'currentColor',
|
|
33
|
+
strokeWidth: 2,
|
|
34
|
+
strokeLinecap: 'round',
|
|
35
|
+
strokeLinejoin: 'round',
|
|
36
|
+
// The arrow duplicates the accessible name of the link: announcing it a
|
|
37
|
+
// second time would tell nothing.
|
|
38
|
+
'aria-hidden': 'true',
|
|
39
|
+
focusable: 'false',
|
|
40
|
+
},
|
|
41
|
+
h('path', { d: 'M12 19V5' }),
|
|
42
|
+
h('path', { d: 'm5 12 7-7 7 7' }),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Back-to-top button.
|
|
48
|
+
*
|
|
49
|
+
* @param {{
|
|
50
|
+
* className?: string, style?: object, children?: any, label?: string,
|
|
51
|
+
* }} props `label` is read by screen readers. The children replace the arrow
|
|
52
|
+
* with whatever you want.
|
|
53
|
+
*/
|
|
54
|
+
export function ScrollToTop({ className, style, children, label = 'Back to top' }) {
|
|
55
|
+
return h(
|
|
56
|
+
'a',
|
|
57
|
+
{
|
|
58
|
+
className: classNames(cls('scrollTop'), className),
|
|
59
|
+
style,
|
|
60
|
+
href: '#top',
|
|
61
|
+
'aria-label': label,
|
|
62
|
+
},
|
|
63
|
+
children ?? h(Chevron),
|
|
64
|
+
);
|
|
65
|
+
}
|