@docpensieve/components 0.3.0-beta.1 → 0.3.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": "@docpensieve/components",
3
- "version": "0.3.0-beta.1",
3
+ "version": "0.3.0",
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.1"
23
+ "@docpensieve/shared": "0.3.0"
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
 
@@ -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
  /*
@@ -464,13 +489,16 @@
464
489
  }
465
490
 
466
491
  /* The fill plays as it enters the viewport, where that is possible.
467
- Elsewhere the bar is full from the start: the value is still read. */
492
+ Elsewhere the bar is full from the start: the value is still read.
493
+ It ends once the gauge is wholly in view, not further up the screen: a
494
+ gauge low on a short page can never scroll higher, and a full gauge
495
+ stopped short of the end reads as a wrong value. */
468
496
  @supports (animation-timeline: view()) {
469
497
  @media (prefers-reduced-motion: no-preference) {
470
498
  .dp-skill-fill {
471
499
  animation: dp-skill-grow linear both;
472
500
  animation-timeline: view();
473
- animation-range: entry 10% cover 60%;
501
+ animation-range: entry 0% entry 100%;
474
502
  }
475
503
  }
476
504
  }
@@ -583,7 +611,7 @@
583
611
  .dp-skill-dial-fill {
584
612
  animation: dp-skill-draw linear both;
585
613
  animation-timeline: view();
586
- animation-range: entry 10% cover 60%;
614
+ animation-range: entry 0% entry 100%;
587
615
  }
588
616
  }
589
617
  }
@@ -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.