@docpensieve/components 0.3.0-beta.1 → 0.3.0-beta.2
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 +2 -2
- package/src/cards.js +161 -0
- package/src/index.js +1 -0
- package/src/registry.js +2 -0
- package/src/site.js +7 -0
- package/styles/components.css +25 -0
- package/types/cards.d.ts +54 -0
- package/types/index.d.ts +1 -0
- package/types/site.d.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docpensieve/components",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.2",
|
|
4
4
|
"description": "DocPensieve global MDX components, usable without import: Card, Columns, Tooltip, Tree, Skill, TimeTimer, LogoIcon, ScrollToTop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"types"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@docpensieve/shared": "0.3.0-beta.
|
|
23
|
+
"@docpensieve/shared": "0.3.0-beta.2"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"react": "^19"
|
package/src/cards.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grids of clickable cards, built from the pages of the version.
|
|
3
|
+
*
|
|
4
|
+
* A folder is a series: its `index` page introduces it, the pages beside that
|
|
5
|
+
* index are its instalments. `<Cards />` turns that structure into cards
|
|
6
|
+
* rather than a list written by hand — a hand-written index goes stale at the
|
|
7
|
+
* first page renamed, and nothing says so.
|
|
8
|
+
*
|
|
9
|
+
* The component reads the page list from the site context, which the
|
|
10
|
+
* generator sets before every page (ADR-006): the compiler plugins work on the
|
|
11
|
+
* Markdown tree, and know nothing of what a component needs.
|
|
12
|
+
*
|
|
13
|
+
* @module @docpensieve/components/cards
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createElement as h } from 'react';
|
|
17
|
+
|
|
18
|
+
import { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
|
|
19
|
+
import { classNames, cls } from './classes.js';
|
|
20
|
+
import { getSiteContext } from './site.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} PageEntry
|
|
24
|
+
* @property {string} url Final address of the page.
|
|
25
|
+
* @property {string} slug Path of the page within the version.
|
|
26
|
+
* @property {string} title
|
|
27
|
+
* @property {string} [description]
|
|
28
|
+
* @property {string} [preview] Image, already resolved by the generator.
|
|
29
|
+
* @property {{ iso: string, label: string }} [modified]
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Slug of the folder holding a page, `''` at the root.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} slug
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
const folderOf = (slug) => (slug.includes('/') ? slug.slice(0, slug.lastIndexOf('/')) : '');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Direct children of a folder, told apart: a child that is itself a folder is
|
|
42
|
+
* a series, the others are pages.
|
|
43
|
+
*
|
|
44
|
+
* @param {PageEntry[]} pages
|
|
45
|
+
* @param {string} base Folder to look into, `''` for the root.
|
|
46
|
+
* @returns {{ series: PageEntry[], pages: PageEntry[] }}
|
|
47
|
+
*/
|
|
48
|
+
function childrenOf(pages, base) {
|
|
49
|
+
const prefix = base === '' ? '' : `${base}/`;
|
|
50
|
+
|
|
51
|
+
// A folder exists as soon as a page lives under it. Its own index page
|
|
52
|
+
// carries the folder's slug, which is what a series card links to.
|
|
53
|
+
const folders = new Set(pages.map((page) => folderOf(page.slug)).filter(Boolean));
|
|
54
|
+
|
|
55
|
+
/** @type {PageEntry[]} */
|
|
56
|
+
const series = [];
|
|
57
|
+
/** @type {PageEntry[]} */
|
|
58
|
+
const leaves = [];
|
|
59
|
+
|
|
60
|
+
for (const page of pages) {
|
|
61
|
+
if (page.slug === base || !page.slug.startsWith(prefix)) continue;
|
|
62
|
+
const rest = page.slug.slice(prefix.length);
|
|
63
|
+
// Only one level down: a grid shows a folder, not its whole depth.
|
|
64
|
+
if (rest === '' || rest.includes('/')) continue;
|
|
65
|
+
|
|
66
|
+
if (folders.has(page.slug)) series.push(page);
|
|
67
|
+
else leaves.push(page);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { series, pages: leaves };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Number of pages a series holds, its own index page excluded.
|
|
75
|
+
*
|
|
76
|
+
* @param {PageEntry[]} pages
|
|
77
|
+
* @param {string} slug Slug of the series.
|
|
78
|
+
* @returns {number}
|
|
79
|
+
*/
|
|
80
|
+
const countOf = (pages, slug) =>
|
|
81
|
+
pages.filter((page) => page.slug.startsWith(`${slug}/`) && page.slug !== slug).length;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One card: the whole surface is the link, never the title alone.
|
|
85
|
+
*
|
|
86
|
+
* @param {{ page: PageEntry, count?: number }} props
|
|
87
|
+
*/
|
|
88
|
+
function PageCard({ page, count }) {
|
|
89
|
+
const footer = [
|
|
90
|
+
count !== undefined && count > 0 ? `${count} ${count === 1 ? 'page' : 'pages'}` : '',
|
|
91
|
+
page.modified ? `Updated ${page.modified.label}` : '',
|
|
92
|
+
].filter(Boolean);
|
|
93
|
+
|
|
94
|
+
return h(
|
|
95
|
+
Card,
|
|
96
|
+
{ className: cls('cardsItem'), href: page.url, elevated: true },
|
|
97
|
+
page.preview ? h(CardImage, { src: page.preview, alt: '' }) : null,
|
|
98
|
+
h(CardHeader, null, page.title),
|
|
99
|
+
page.description ? h(CardBody, null, page.description) : null,
|
|
100
|
+
footer.length > 0
|
|
101
|
+
? h(
|
|
102
|
+
CardFooter,
|
|
103
|
+
null,
|
|
104
|
+
footer.map((text, index) =>
|
|
105
|
+
h('span', { key: text, className: index === 0 ? undefined : cls('cardsMeta') }, text),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
: null,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Grid of cards for the children of a folder.
|
|
114
|
+
*
|
|
115
|
+
* @param {{
|
|
116
|
+
* className?: string, style?: object,
|
|
117
|
+
* of?: 'all' | 'series' | 'pages', from?: string,
|
|
118
|
+
* }} props `of` narrows the grid to the sub-folders (`'series'`) or to the
|
|
119
|
+
* pages beside the index (`'pages'`); `from` reads another folder, named by
|
|
120
|
+
* its slug, instead of the one holding the page.
|
|
121
|
+
* @throws {Error} When the page list is missing, or `from` names nothing.
|
|
122
|
+
*/
|
|
123
|
+
export function Cards({ className, style, of = 'all', from }) {
|
|
124
|
+
const context = getSiteContext();
|
|
125
|
+
const pages = /** @type {PageEntry[] | undefined} */ (context.pages);
|
|
126
|
+
|
|
127
|
+
if (!pages) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'Cards needs the list of pages, which the generator sets before rendering a page.',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const current = context.slug ?? '';
|
|
134
|
+
// The index page of a folder lists that folder; a page beside it lists the
|
|
135
|
+
// folder holding them both. Without this, a series page would show its
|
|
136
|
+
// neighbours instead of its own instalments.
|
|
137
|
+
const opensFolder = current === '' || pages.some((page) => page.slug.startsWith(`${current}/`));
|
|
138
|
+
const base = from ?? (opensFolder ? current : folderOf(current));
|
|
139
|
+
|
|
140
|
+
if (from !== undefined && from !== '' && !pages.some((page) => page.slug.startsWith(from))) {
|
|
141
|
+
throw new Error(`Cards found no page under "${from}": check the folder name.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const children = childrenOf(pages, base);
|
|
145
|
+
const shown = [
|
|
146
|
+
...(of === 'pages' ? [] : children.series),
|
|
147
|
+
...(of === 'series' ? [] : children.pages),
|
|
148
|
+
].filter((page) => page.url !== context.url);
|
|
149
|
+
|
|
150
|
+
return h(
|
|
151
|
+
'div',
|
|
152
|
+
{ className: classNames(cls('cards'), className), style },
|
|
153
|
+
shown.map((page) =>
|
|
154
|
+
h(PageCard, {
|
|
155
|
+
key: page.url,
|
|
156
|
+
page,
|
|
157
|
+
count: children.series.includes(page) ? countOf(pages, page.slug) : undefined,
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export {
|
|
|
19
19
|
setThemeFramework,
|
|
20
20
|
} from './classes.js';
|
|
21
21
|
export { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
|
|
22
|
+
export { Cards } from './cards.js';
|
|
22
23
|
export { Column, Columns } from './columns.js';
|
|
23
24
|
export { FallbackAfter, FallbackBefore, TimeTimer } from './time-timer.js';
|
|
24
25
|
export { TOOLTIP_PLACEMENTS, Tooltip } from './tooltip.js';
|
package/src/registry.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
|
|
11
|
+
import { Cards } from './cards.js';
|
|
11
12
|
import { Column, Columns } from './columns.js';
|
|
12
13
|
import { ForTheme } from './for-theme.js';
|
|
13
14
|
import { LogoIcon } from './logo-icon.js';
|
|
@@ -27,6 +28,7 @@ export const builtinComponents = {
|
|
|
27
28
|
CardBody,
|
|
28
29
|
CardFooter,
|
|
29
30
|
CardImage,
|
|
31
|
+
Cards,
|
|
30
32
|
Columns,
|
|
31
33
|
Column,
|
|
32
34
|
TimeTimer,
|
package/src/site.js
CHANGED
|
@@ -28,6 +28,11 @@ const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
|
|
|
28
28
|
* @property {string} basePath Version root, deployment prefix included.
|
|
29
29
|
* @property {string} [filepath] Source file of the page, on disk.
|
|
30
30
|
* @property {string} [sourceDir] Source folder of the version.
|
|
31
|
+
* @property {string} [slug] Path of the page within the version, `''` at the
|
|
32
|
+
* root. It is what tells a component which folder it stands in.
|
|
33
|
+
* @property {{ url: string, slug: string, title: string, description?: string, preview?: string, modified?: { iso: string, label: string } }[]} [pages]
|
|
34
|
+
* Every page of the version, targets already resolved. A component that
|
|
35
|
+
* lists pages cannot compute this: the compiler works one page at a time.
|
|
31
36
|
*/
|
|
32
37
|
|
|
33
38
|
/** @type {SiteContext} */
|
|
@@ -47,6 +52,8 @@ export function setSiteContext(page = {}) {
|
|
|
47
52
|
basePath: page.basePath ?? '/',
|
|
48
53
|
filepath: page.filepath,
|
|
49
54
|
sourceDir: page.sourceDir,
|
|
55
|
+
slug: page.slug,
|
|
56
|
+
pages: page.pages,
|
|
50
57
|
};
|
|
51
58
|
}
|
|
52
59
|
|
package/styles/components.css
CHANGED
|
@@ -14,6 +14,31 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
@layer components {
|
|
17
|
+
/* --- Cards ------------------------------------------------------------- */
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Grid of cards built from the pages of a folder. `auto-fill` rather than a
|
|
21
|
+
* fixed column count: the same grid serves three series and eleven pages,
|
|
22
|
+
* and follows the width it is given without a media query.
|
|
23
|
+
*/
|
|
24
|
+
.dp-cards {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));
|
|
27
|
+
gap: 1rem;
|
|
28
|
+
margin: 1.5rem 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* The card fills its cell, whatever its neighbours hold. */
|
|
32
|
+
.dp-cards-item {
|
|
33
|
+
height: 100%;
|
|
34
|
+
margin: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.dp-cards-meta::before {
|
|
38
|
+
content: '·';
|
|
39
|
+
margin: 0 0.4rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
/* --- Card -------------------------------------------------------------- */
|
|
18
43
|
|
|
19
44
|
/*
|
package/types/cards.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grids of clickable cards, built from the pages of the version.
|
|
3
|
+
*
|
|
4
|
+
* A folder is a series: its `index` page introduces it, the pages beside that
|
|
5
|
+
* index are its instalments. `<Cards />` turns that structure into cards
|
|
6
|
+
* rather than a list written by hand — a hand-written index goes stale at the
|
|
7
|
+
* first page renamed, and nothing says so.
|
|
8
|
+
*
|
|
9
|
+
* The component reads the page list from the site context, which the
|
|
10
|
+
* generator sets before every page (ADR-006): the compiler plugins work on the
|
|
11
|
+
* Markdown tree, and know nothing of what a component needs.
|
|
12
|
+
*
|
|
13
|
+
* @module @docpensieve/components/cards
|
|
14
|
+
*/
|
|
15
|
+
export type PageEntry = {
|
|
16
|
+
/**
|
|
17
|
+
* Final address of the page.
|
|
18
|
+
*/
|
|
19
|
+
url: string;
|
|
20
|
+
/**
|
|
21
|
+
* Path of the page within the version.
|
|
22
|
+
*/
|
|
23
|
+
slug: string;
|
|
24
|
+
title: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Image, already resolved by the generator.
|
|
28
|
+
*/
|
|
29
|
+
preview?: string;
|
|
30
|
+
modified?: {
|
|
31
|
+
iso: string;
|
|
32
|
+
label: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Grid of cards for the children of a folder.
|
|
37
|
+
*
|
|
38
|
+
* @param {{
|
|
39
|
+
* className?: string, style?: object,
|
|
40
|
+
* of?: 'all' | 'series' | 'pages', from?: string,
|
|
41
|
+
* }} props `of` narrows the grid to the sub-folders (`'series'`) or to the
|
|
42
|
+
* pages beside the index (`'pages'`); `from` reads another folder, named by
|
|
43
|
+
* its slug, instead of the one holding the page.
|
|
44
|
+
* @throws {Error} When the page list is missing, or `from` names nothing.
|
|
45
|
+
*/
|
|
46
|
+
export declare function Cards({ className, style, of, from }: {
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: object;
|
|
49
|
+
of?: 'all' | 'series' | 'pages';
|
|
50
|
+
from?: string;
|
|
51
|
+
}): import("react").DetailedReactHTMLElement<{
|
|
52
|
+
className: string | undefined;
|
|
53
|
+
style: object | undefined;
|
|
54
|
+
}, HTMLElement>;
|
package/types/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { classNames, cls, fallbackClass, getThemeClasses, getThemeFramework, setThemeClasses, setThemeFramework, } from './classes.js';
|
|
12
12
|
export { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
|
|
13
|
+
export { Cards } from './cards.js';
|
|
13
14
|
export { Column, Columns } from './columns.js';
|
|
14
15
|
export { FallbackAfter, FallbackBefore, TimeTimer } from './time-timer.js';
|
|
15
16
|
export { TOOLTIP_PLACEMENTS, Tooltip } from './tooltip.js';
|
package/types/site.d.ts
CHANGED
|
@@ -33,6 +33,26 @@ export type SiteContext = {
|
|
|
33
33
|
* Source folder of the version.
|
|
34
34
|
*/
|
|
35
35
|
sourceDir?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Path of the page within the version, `''` at the
|
|
38
|
+
* root. It is what tells a component which folder it stands in.
|
|
39
|
+
*/
|
|
40
|
+
slug?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Every page of the version, targets already resolved. A component that
|
|
43
|
+
* lists pages cannot compute this: the compiler works one page at a time.
|
|
44
|
+
*/
|
|
45
|
+
pages?: {
|
|
46
|
+
url: string;
|
|
47
|
+
slug: string;
|
|
48
|
+
title: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
preview?: string;
|
|
51
|
+
modified?: {
|
|
52
|
+
iso: string;
|
|
53
|
+
label: string;
|
|
54
|
+
};
|
|
55
|
+
}[];
|
|
36
56
|
};
|
|
37
57
|
/**
|
|
38
58
|
* Declares the page being rendered.
|