@docpensieve/components 0.4.0-beta.1 → 0.4.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.4.0-beta.1",
3
+ "version": "0.4.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.4.0-beta.1"
23
+ "@docpensieve/shared": "0.4.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "react": "^19"
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Admonition: a block set apart from the text, which says how to read it — a
3
+ * tip, a caution, a danger.
4
+ *
5
+ * Six kinds ship with the tool, and a project declares its own in its
6
+ * configuration rather than waiting for the list to grow. A kind is a label
7
+ * and a **tone**: the tone carries the colour, taken from the theme's tokens,
8
+ * so a project adds a kind without touching a stylesheet, and the block
9
+ * follows the palette of whichever theme is active (ADR-007).
10
+ *
11
+ * @module @docpensieve/components/admonition
12
+ */
13
+
14
+ import { createElement as h } from 'react';
15
+
16
+ import { ADMONITION_TONES, DocPensieveError } from '@docpensieve/shared';
17
+
18
+ import { classNames, cls } from './classes.js';
19
+ import { LogoIcon } from './logo-icon.js';
20
+
21
+ /**
22
+ * The kinds the tool ships with, each a label and a tone.
23
+ *
24
+ * `alert` and `danger` share a tone and differ in their label and their icon:
25
+ * one calls for attention now, the other warns of what a wrong move costs.
26
+ */
27
+ export const ADMONITION_KINDS = Object.freeze({
28
+ note: { label: 'Note', tone: 'note' },
29
+ info: { label: 'Info', tone: 'info' },
30
+ tip: { label: 'Tip', tone: 'tip' },
31
+ attention: { label: 'Attention', tone: 'attention' },
32
+ alert: { label: 'Alert', tone: 'danger' },
33
+ danger: { label: 'Danger', tone: 'danger' },
34
+ });
35
+
36
+ /**
37
+ * Icons, drawn rather than written as characters: an emoji changes shape from
38
+ * one system to the next, and carries a meaning screen readers announce.
39
+ *
40
+ * Each is a path on a 24 × 24 grid, stroked in the current colour — never
41
+ * filled from CSS, which would turn an outline into a solid shape.
42
+ *
43
+ * @type {Record<string, string>}
44
+ */
45
+ const PATHS = Object.freeze({
46
+ note: 'M4 5h16M4 12h16M4 19h10',
47
+ info: 'M12 16v-5M12 8h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z',
48
+ tip: 'M9 18h6M10 21h4M12 3a6 6 0 0 0-3.5 10.9c.5.4.8 1 .9 1.6h5.2c.1-.6.4-1.2.9-1.6A6 6 0 0 0 12 3z',
49
+ attention:
50
+ 'M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z',
51
+ alert: 'M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 0 1-3.4 0',
52
+ danger: 'M12 8v5M12 16h.01M8.6 2.6h6.8L21.4 8.6v6.8L15.4 21.4H8.6L2.6 15.4V8.6z',
53
+ });
54
+
55
+ /**
56
+ * Kinds the project declares, set once for the whole compilation.
57
+ *
58
+ * Module state rather than a prop: an author writes `type="…"` and nothing
59
+ * else, and `core` cannot hand this down without knowing this package
60
+ * (ADR-002). The same reasoning as the theme's class table.
61
+ *
62
+ * @type {Record<string, { label: string, tone: string, icon?: string }>}
63
+ */
64
+ let projectKinds = {};
65
+
66
+ /**
67
+ * Declares the kinds of the project, beside the ones shipped.
68
+ *
69
+ * @param {Record<string, { label: string, tone: string, icon?: string }>} [kinds]
70
+ */
71
+ export function setAdmonitionKinds(kinds = {}) {
72
+ projectKinds = kinds;
73
+ }
74
+
75
+ /** @returns {Record<string, { label: string, tone: string, icon?: string }>} Every kind available. */
76
+ export function getAdmonitionKinds() {
77
+ return { ...ADMONITION_KINDS, ...projectKinds };
78
+ }
79
+
80
+ /**
81
+ * Block set apart from the text.
82
+ *
83
+ * @param {{
84
+ * className?: string, style?: object, children?: any,
85
+ * type?: string, title?: string,
86
+ * }} props `type` names the kind; `title` replaces its label for this block
87
+ * alone.
88
+ * @throws {DocPensieveError} When the kind is unknown.
89
+ */
90
+ export function Admonition({ className, style, children, type = 'note', title, ...rest }) {
91
+ const table = getAdmonitionKinds();
92
+ const kind = table[type];
93
+
94
+ // A kind nobody declared would render a block with no colour and no label —
95
+ // exactly the silent nothing this project refuses.
96
+ if (!kind) {
97
+ throw new DocPensieveError(`Unknown admonition type: "${type}".`, {
98
+ hint: `Known types: ${Object.keys(table).sort().join(', ')}. Declare your own in the admonitions field of the configuration.`,
99
+ });
100
+ }
101
+
102
+ const tone = ADMONITION_TONES.includes(kind.tone) ? kind.tone : 'note';
103
+
104
+ return h(
105
+ 'aside',
106
+ {
107
+ className: classNames(cls('admonition', tone), className),
108
+ style,
109
+ // Set apart from the flow of the text, and announced as such.
110
+ role: 'note',
111
+ ...rest,
112
+ },
113
+ h(
114
+ 'p',
115
+ { className: cls('admonitionTitle') },
116
+ // A kind may bring a mark of its own — a logo, inlined from the version
117
+ // folder. Without one, the drawing of its tone stands.
118
+ kind.icon
119
+ ? h(LogoIcon, { className: cls('admonitionIcon'), src: kind.icon })
120
+ : h(
121
+ 'svg',
122
+ {
123
+ className: cls('admonitionIcon'),
124
+ viewBox: '0 0 24 24',
125
+ fill: 'none',
126
+ stroke: 'currentColor',
127
+ strokeWidth: 2,
128
+ strokeLinecap: 'round',
129
+ strokeLinejoin: 'round',
130
+ // The title beside it already names the kind: announcing the
131
+ // icon too would say it twice.
132
+ 'aria-hidden': 'true',
133
+ focusable: 'false',
134
+ },
135
+ h('path', { d: PATHS[type] ?? PATHS[tone] ?? PATHS.note }),
136
+ ),
137
+ title ?? kind.label,
138
+ ),
139
+ h('div', { className: cls('admonitionBody') }, children),
140
+ );
141
+ }
package/src/iconify.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Icons taken from an icon set, at build time.
3
+ *
4
+ * An icon is named `prefix:name` — the way those collections name them. The
5
+ * set is read from the package the project installed, and the drawing is
6
+ * placed in the page like any other icon: the reader downloads nothing, and no
7
+ * request leaves their browser. The online service those collections offer
8
+ * would mean both.
9
+ *
10
+ * The sets are **optional dependencies of the project**: a site installs the
11
+ * ones it uses, and this package keeps depending on nothing but `shared`. A
12
+ * set that is not installed stops the build, naming what to install.
13
+ *
14
+ * @module @docpensieve/components/iconify
15
+ */
16
+
17
+ import { createRequire } from 'node:module';
18
+ import path from 'node:path';
19
+
20
+ import { DocPensieveError } from '@docpensieve/shared';
21
+
22
+ /** `prefix:name` — never a path, which always holds a dot or a slash. */
23
+ const ICON_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
24
+
25
+ /**
26
+ * Whether a target names an icon of a set rather than a file.
27
+ *
28
+ * @param {string} src
29
+ * @returns {boolean}
30
+ */
31
+ export function isIconName(src) {
32
+ return ICON_NAME.test(src) && !src.includes('/');
33
+ }
34
+
35
+ /**
36
+ * Sets already read, by prefix. A page uses the same set many times over.
37
+ *
38
+ * @type {Map<string, any>}
39
+ */
40
+ const sets = new Map();
41
+
42
+ /** Drawings already assembled, by full name. @type {Map<string, string>} */
43
+ const drawings = new Map();
44
+
45
+ /**
46
+ * Loads an icon set from the project.
47
+ *
48
+ * Two resolutions are tried: from this package — where a hoisted install puts
49
+ * the set within reach — and from the project folder, which covers the rest.
50
+ *
51
+ * @param {string} prefix
52
+ * @returns {any} The set, as the collection ships it.
53
+ * @throws {DocPensieveError} When the set is not installed.
54
+ */
55
+ function loadSet(prefix) {
56
+ const known = sets.get(prefix);
57
+ if (known) return known;
58
+
59
+ const specifier = `@iconify-json/${prefix}/icons.json`;
60
+ const froms = [import.meta.url, path.join(process.cwd(), 'noop.js')];
61
+
62
+ for (const from of froms) {
63
+ try {
64
+ const set = createRequire(from)(specifier);
65
+ sets.set(prefix, set);
66
+ return set;
67
+ } catch {
68
+ // Tried elsewhere before giving up.
69
+ }
70
+ }
71
+
72
+ throw new DocPensieveError(`No icon set installed for "${prefix}".`, {
73
+ hint: `Install the set beside your project: npm install --save-dev @iconify-json/${prefix}`,
74
+ });
75
+ }
76
+
77
+ /**
78
+ * The drawing of an icon, as a complete `svg` tag.
79
+ *
80
+ * @param {string} name Full name, `prefix:icon`.
81
+ * @returns {string}
82
+ * @throws {DocPensieveError} Set not installed, or icon absent from it.
83
+ */
84
+ export function iconSvg(name) {
85
+ const known = drawings.get(name);
86
+ if (known !== undefined) return known;
87
+
88
+ const [prefix, icon] = name.split(':');
89
+ const set = loadSet(prefix);
90
+
91
+ // A set names some icons twice: the second name is an alias of the first.
92
+ const alias = set.aliases?.[icon];
93
+ const entry = set.icons?.[icon] ?? (alias ? set.icons?.[alias.parent] : undefined);
94
+
95
+ if (!entry) {
96
+ throw new DocPensieveError(`The icon set "${prefix}" holds no icon named "${icon}".`, {
97
+ hint: `Check the name in that collection: it is written ${prefix}:some-icon.`,
98
+ });
99
+ }
100
+
101
+ const width = entry.width ?? set.width ?? 16;
102
+ const height = entry.height ?? set.height ?? 16;
103
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${entry.body}</svg>`;
104
+
105
+ drawings.set(name, svg);
106
+ return svg;
107
+ }
package/src/index.js CHANGED
@@ -18,9 +18,16 @@ export {
18
18
  setThemeClasses,
19
19
  setThemeFramework,
20
20
  } from './classes.js';
21
+ export {
22
+ ADMONITION_KINDS,
23
+ Admonition,
24
+ getAdmonitionKinds,
25
+ setAdmonitionKinds,
26
+ } from './admonition.js';
21
27
  export { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
22
28
  export { Cards } from './cards.js';
23
29
  export { Column, Columns } from './columns.js';
30
+ export { Menu, MenuGroup, MenuLink } from './menu.js';
24
31
  export { FallbackAfter, FallbackBefore, TimeTimer } from './time-timer.js';
25
32
  export { TOOLTIP_PLACEMENTS, Tooltip } from './tooltip.js';
26
33
  export { Tree, TreeItem } from './tree.js';
package/src/logo-icon.js CHANGED
@@ -14,6 +14,7 @@ import { createElement as h } from 'react';
14
14
  import { DocPensieveError } from '@docpensieve/shared';
15
15
 
16
16
  import { classNames, cls } from './classes.js';
17
+ import { iconSvg, isIconName } from './iconify.js';
17
18
  import { resolveFile } from './site.js';
18
19
 
19
20
  /** Isolates the `svg` root: XML header, doctype and comments stay out. */
@@ -48,6 +49,11 @@ const cache = new Map();
48
49
  * @throws {DocPensieveError} File not found, or without an `svg` tag.
49
50
  */
50
51
  function readSvg(src) {
52
+ // `prefix:name` names an icon of a set rather than a file of the project:
53
+ // the drawing then comes from the set the project installed, and is placed
54
+ // in the page all the same.
55
+ if (isIconName(src)) return iconSvg(src).replace(SCRIPT, '').replace(HANDLER, '');
56
+
51
57
  /** @type {string} */
52
58
  let file;
53
59
  try {
package/src/menu.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * A menu of links, placed anywhere in a page.
3
+ *
4
+ * The header already carries the navigation of the site, and the sidebar that
5
+ * of the documentation. This one belongs to a page: the links a section wants
6
+ * to offer where it stands — a summary at the top of a landing page, the
7
+ * chapters of a guide, the entries of a portal.
8
+ *
9
+ * It holds no state and loads no script. On a narrow screen it folds behind a
10
+ * button, a native `details` element: the menu is therefore written once and
11
+ * rendered twice, a row and a fold, of which the stylesheet shows one. The
12
+ * alternative, a single rendering revealed by a recent CSS pseudo-element,
13
+ * hides the links outright on a browser that does not know it.
14
+ *
15
+ * Classes come from the theme (ADR-007), never from a hard-coded framework.
16
+ *
17
+ * @module @docpensieve/components/menu
18
+ */
19
+
20
+ import { createContext, createElement as h, useContext } from 'react';
21
+
22
+ import { DocPensieveError } from '@docpensieve/shared';
23
+
24
+ import { classNames, cls } from './classes.js';
25
+ import { resolveUrl } from './site.js';
26
+
27
+ /**
28
+ * What an entry learns from the menu holding it.
29
+ *
30
+ * `false` outside a menu: without this landmark, a lone `MenuLink` rendered a
31
+ * link with no menu around it — styled as an entry, standing nowhere, and
32
+ * without a word to say so.
33
+ *
34
+ * @type {import('react').Context<boolean>}
35
+ */
36
+ const InMenu = createContext(false);
37
+
38
+ /**
39
+ * Menu of links.
40
+ *
41
+ * @param {{
42
+ * className?: string, style?: object, children?: any, label?: string,
43
+ * }} props `label` names the menu for screen readers, and labels the button it
44
+ * folds into on a narrow screen.
45
+ */
46
+ export function Menu({ className, style, children, label = 'Menu' }) {
47
+ const entries = h(InMenu.Provider, { value: true }, children);
48
+
49
+ return h(
50
+ 'div',
51
+ { className: classNames(cls('pageMenu'), className), style },
52
+ // The row and the fold hold the same entries; the stylesheet shows one or
53
+ // the other, so a screen reader never meets the menu twice.
54
+ h('nav', { className: cls('pageMenuRow'), 'aria-label': label }, entries),
55
+ h(
56
+ 'details',
57
+ { className: cls('pageMenuFold') },
58
+ h('summary', null, label),
59
+ h('nav', { className: cls('pageMenuFoldList'), 'aria-label': label }, entries),
60
+ ),
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Entry of a menu.
66
+ *
67
+ * @param {{
68
+ * className?: string, style?: object, children?: any, href?: string,
69
+ * }} props
70
+ * @throws {DocPensieveError} Outside a `Menu`, or without a target.
71
+ */
72
+ export function MenuLink({ className, style, children, href }) {
73
+ if (!useContext(InMenu)) {
74
+ throw new DocPensieveError('A <MenuLink> was written outside a <Menu>.', {
75
+ hint: 'Wrap the entries in <Menu>…</Menu>.',
76
+ });
77
+ }
78
+
79
+ // A menu entry that leads nowhere is furniture: better said at build time
80
+ // than left for a reader to click.
81
+ if (typeof href !== 'string' || href.trim() === '') {
82
+ throw new DocPensieveError('A <MenuLink> without href.', {
83
+ hint: 'Give the target: <MenuLink href="/guide/">Guide</MenuLink>.',
84
+ });
85
+ }
86
+
87
+ return h(
88
+ 'a',
89
+ {
90
+ className: classNames(cls('pageMenuLink'), className),
91
+ style,
92
+ // A link produced by a component escapes the compiler plugins: it
93
+ // resolves itself, following the same rules (ADR-006).
94
+ href: resolveUrl(href),
95
+ },
96
+ children,
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Group of entries, folded under a title.
102
+ *
103
+ * In the row it opens as a panel below its title; folded, it unfolds in place
104
+ * rather than over the rest — on a narrow screen a panel would open off
105
+ * screen.
106
+ *
107
+ * @param {{
108
+ * className?: string, style?: object, children?: any, title?: string,
109
+ * }} props
110
+ * @throws {DocPensieveError} Outside a `Menu`, or without a title.
111
+ */
112
+ export function MenuGroup({ className, style, children, title }) {
113
+ if (!useContext(InMenu)) {
114
+ throw new DocPensieveError('A <MenuGroup> was written outside a <Menu>.', {
115
+ hint: 'Wrap the groups in <Menu>…</Menu>.',
116
+ });
117
+ }
118
+
119
+ if (typeof title !== 'string' || title.trim() === '') {
120
+ throw new DocPensieveError('A <MenuGroup> without title.', {
121
+ hint: 'The title is what opens the group: <MenuGroup title="Reference">…</MenuGroup>.',
122
+ });
123
+ }
124
+
125
+ return h(
126
+ 'details',
127
+ { className: classNames(cls('pageMenuGroup'), className), style },
128
+ h('summary', { className: cls('pageMenuTitle') }, title),
129
+ h('div', { className: cls('pageMenuPanel') }, children),
130
+ );
131
+ }
package/src/registry.js CHANGED
@@ -7,11 +7,13 @@
7
7
  * @module @docpensieve/components/registry
8
8
  */
9
9
 
10
+ import { Admonition } from './admonition.js';
10
11
  import { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
11
12
  import { Cards } from './cards.js';
12
13
  import { Column, Columns } from './columns.js';
13
14
  import { ForTheme } from './for-theme.js';
14
15
  import { LogoIcon } from './logo-icon.js';
16
+ import { Menu, MenuGroup, MenuLink } from './menu.js';
15
17
  import { ScrollToTop } from './scroll-to-top.js';
16
18
  import { Skill } from './skill.js';
17
19
  import { FallbackAfter, FallbackBefore, TimeTimer } from './time-timer.js';
@@ -23,6 +25,7 @@ import { Tree, TreeItem } from './tree.js';
23
25
  * @type {Record<string, Function>}
24
26
  */
25
27
  export const builtinComponents = {
28
+ Admonition,
26
29
  Card,
27
30
  CardHeader,
28
31
  CardBody,
@@ -31,6 +34,9 @@ export const builtinComponents = {
31
34
  Cards,
32
35
  Columns,
33
36
  Column,
37
+ Menu,
38
+ MenuLink,
39
+ MenuGroup,
34
40
  TimeTimer,
35
41
  FallbackBefore,
36
42
  FallbackAfter,
@@ -14,6 +14,200 @@
14
14
  */
15
15
 
16
16
  @layer components {
17
+ /* --- Admonition -------------------------------------------------------- */
18
+
19
+ /*
20
+ * A block set apart from the text. Its colour comes from its tone, through
21
+ * two variables the modifier sets: the component therefore knows no colour,
22
+ * and a kind declared by a project needs no rule of its own.
23
+ */
24
+ .dp-admonition {
25
+ --dp-admonition-colour: var(--dp-text-soft);
26
+ --dp-admonition-ground: var(--dp-bg-soft);
27
+ margin: 1.5rem 0;
28
+ padding: 0.9rem 1.1rem;
29
+ border: 1px solid var(--dp-border);
30
+ border-inline-start: 3px solid var(--dp-admonition-colour);
31
+ border-radius: var(--dp-radius);
32
+ background: var(--dp-admonition-ground);
33
+ }
34
+
35
+ .dp-admonition--info {
36
+ --dp-admonition-colour: var(--dp-accent);
37
+ --dp-admonition-ground: var(--dp-accent-soft);
38
+ }
39
+
40
+ .dp-admonition--tip {
41
+ --dp-admonition-colour: var(--dp-tip);
42
+ --dp-admonition-ground: var(--dp-tip-soft);
43
+ }
44
+
45
+ .dp-admonition--attention {
46
+ --dp-admonition-colour: var(--dp-attention);
47
+ --dp-admonition-ground: var(--dp-attention-soft);
48
+ }
49
+
50
+ .dp-admonition--danger {
51
+ --dp-admonition-colour: var(--dp-danger);
52
+ --dp-admonition-ground: var(--dp-danger-soft);
53
+ }
54
+
55
+ .dp-admonition-title {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 0.45rem;
59
+ margin: 0 0 0.4rem;
60
+ color: var(--dp-admonition-colour);
61
+ font-weight: 600;
62
+ }
63
+
64
+ /* Size only: the icon inherits the colour of the title it stands beside. */
65
+ .dp-admonition-icon {
66
+ flex: none;
67
+ inline-size: 1.05rem;
68
+ block-size: 1.05rem;
69
+ }
70
+
71
+ .dp-admonition-body > :first-child {
72
+ margin-block-start: 0;
73
+ }
74
+
75
+ .dp-admonition-body > :last-child {
76
+ margin-block-end: 0;
77
+ }
78
+
79
+ /* --- Menu -------------------------------------------------------------- */
80
+
81
+ /*
82
+ * A menu of links inside a page. Written once and rendered twice — a row and
83
+ * a fold — of which one shows: a single rendering revealed by CSS alone
84
+ * hides the links outright where that CSS is unknown.
85
+ */
86
+ .dp-page-menu {
87
+ margin: 1.5rem 0;
88
+ }
89
+
90
+ .dp-page-menu-row {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ align-items: center;
94
+ gap: 0.35rem 1.25rem;
95
+ }
96
+
97
+ .dp-page-menu-fold {
98
+ display: none;
99
+ }
100
+
101
+ .dp-page-menu-fold > summary {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ gap: 0.4rem;
105
+ padding: 0.45rem 0.9rem;
106
+ border: 1px solid var(--dp-border);
107
+ border-radius: var(--dp-radius);
108
+ color: var(--dp-text-soft);
109
+ cursor: pointer;
110
+ list-style: none;
111
+ }
112
+
113
+ .dp-page-menu-fold > summary::-webkit-details-marker {
114
+ display: none;
115
+ }
116
+
117
+ .dp-page-menu-fold[open] > summary,
118
+ .dp-page-menu-fold > summary:hover {
119
+ border-color: var(--dp-text-soft);
120
+ color: var(--dp-text);
121
+ }
122
+
123
+ .dp-page-menu-fold-list {
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 0.6rem;
127
+ margin-block-start: 0.75rem;
128
+ padding-inline-start: 0.75rem;
129
+ border-inline-start: 1px solid var(--dp-border);
130
+ }
131
+
132
+ .dp-page-menu-link {
133
+ color: var(--dp-text);
134
+ font-weight: 500;
135
+ text-decoration: none;
136
+ }
137
+
138
+ .dp-page-menu-link:hover {
139
+ color: var(--dp-accent);
140
+ }
141
+
142
+ /* A group: a panel in the row, unfolded in place once the menu is folded. */
143
+ .dp-page-menu-group {
144
+ position: relative;
145
+ }
146
+
147
+ .dp-page-menu-title {
148
+ color: var(--dp-text-soft);
149
+ font-weight: 500;
150
+ cursor: pointer;
151
+ list-style: none;
152
+ }
153
+
154
+ .dp-page-menu-title::-webkit-details-marker {
155
+ display: none;
156
+ }
157
+
158
+ .dp-page-menu-title::after {
159
+ content: '';
160
+ display: inline-block;
161
+ inline-size: 0.35rem;
162
+ block-size: 0.35rem;
163
+ margin-inline-start: 0.4rem;
164
+ border-inline-end: 1px solid currentColor;
165
+ border-block-end: 1px solid currentColor;
166
+ rotate: 45deg;
167
+ translate: 0 -0.15rem;
168
+ }
169
+
170
+ .dp-page-menu-group[open] > .dp-page-menu-title {
171
+ color: var(--dp-text);
172
+ }
173
+
174
+ .dp-page-menu-panel {
175
+ position: absolute;
176
+ top: calc(100% + 0.4rem);
177
+ left: 0;
178
+ z-index: 10;
179
+ display: flex;
180
+ flex-direction: column;
181
+ gap: 0.5rem;
182
+ min-inline-size: 10rem;
183
+ padding: 0.8rem 1rem;
184
+ border: 1px solid var(--dp-border);
185
+ border-radius: var(--dp-radius);
186
+ background: var(--dp-bg);
187
+ box-shadow: 0 8px 24px var(--dp-shadow);
188
+ }
189
+
190
+ .dp-page-menu-fold-list .dp-page-menu-panel {
191
+ position: static;
192
+ min-inline-size: 0;
193
+ padding: 0.5rem 0 0 0.75rem;
194
+ border: 0;
195
+ border-inline-start: 1px solid var(--dp-border);
196
+ border-radius: 0;
197
+ background: transparent;
198
+ box-shadow: none;
199
+ }
200
+
201
+ @media (max-width: 40rem) {
202
+ .dp-page-menu-row {
203
+ display: none;
204
+ }
205
+
206
+ .dp-page-menu-fold {
207
+ display: block;
208
+ }
209
+ }
210
+
17
211
  /* --- Cards ------------------------------------------------------------- */
18
212
 
19
213
  /*
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Admonition: a block set apart from the text, which says how to read it — a
3
+ * tip, a caution, a danger.
4
+ *
5
+ * Six kinds ship with the tool, and a project declares its own in its
6
+ * configuration rather than waiting for the list to grow. A kind is a label
7
+ * and a **tone**: the tone carries the colour, taken from the theme's tokens,
8
+ * so a project adds a kind without touching a stylesheet, and the block
9
+ * follows the palette of whichever theme is active (ADR-007).
10
+ *
11
+ * @module @docpensieve/components/admonition
12
+ */
13
+ /**
14
+ * The kinds the tool ships with, each a label and a tone.
15
+ *
16
+ * `alert` and `danger` share a tone and differ in their label and their icon:
17
+ * one calls for attention now, the other warns of what a wrong move costs.
18
+ */
19
+ export declare const ADMONITION_KINDS: Readonly<{
20
+ note: {
21
+ label: string;
22
+ tone: string;
23
+ };
24
+ info: {
25
+ label: string;
26
+ tone: string;
27
+ };
28
+ tip: {
29
+ label: string;
30
+ tone: string;
31
+ };
32
+ attention: {
33
+ label: string;
34
+ tone: string;
35
+ };
36
+ alert: {
37
+ label: string;
38
+ tone: string;
39
+ };
40
+ danger: {
41
+ label: string;
42
+ tone: string;
43
+ };
44
+ }>;
45
+ /**
46
+ * Declares the kinds of the project, beside the ones shipped.
47
+ *
48
+ * @param {Record<string, { label: string, tone: string, icon?: string }>} [kinds]
49
+ */
50
+ export declare function setAdmonitionKinds(kinds?: Record<string, {
51
+ label: string;
52
+ tone: string;
53
+ icon?: string;
54
+ }>): void;
55
+ /** @returns {Record<string, { label: string, tone: string, icon?: string }>} Every kind available. */
56
+ export declare function getAdmonitionKinds(): Record<string, {
57
+ label: string;
58
+ tone: string;
59
+ icon?: string;
60
+ }>;
61
+ /**
62
+ * Block set apart from the text.
63
+ *
64
+ * @param {{
65
+ * className?: string, style?: object, children?: any,
66
+ * type?: string, title?: string,
67
+ * }} props `type` names the kind; `title` replaces its label for this block
68
+ * alone.
69
+ * @throws {DocPensieveError} When the kind is unknown.
70
+ */
71
+ export declare function Admonition({ className, style, children, type, title, ...rest }: {
72
+ className?: string;
73
+ style?: object;
74
+ children?: any;
75
+ type?: string;
76
+ title?: string;
77
+ }): import("react").DetailedReactHTMLElement<{
78
+ className: string | undefined;
79
+ style: object | undefined;
80
+ role: "note";
81
+ }, HTMLElement>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Icons taken from an icon set, at build time.
3
+ *
4
+ * An icon is named `prefix:name` — the way those collections name them. The
5
+ * set is read from the package the project installed, and the drawing is
6
+ * placed in the page like any other icon: the reader downloads nothing, and no
7
+ * request leaves their browser. The online service those collections offer
8
+ * would mean both.
9
+ *
10
+ * The sets are **optional dependencies of the project**: a site installs the
11
+ * ones it uses, and this package keeps depending on nothing but `shared`. A
12
+ * set that is not installed stops the build, naming what to install.
13
+ *
14
+ * @module @docpensieve/components/iconify
15
+ */
16
+ /**
17
+ * Whether a target names an icon of a set rather than a file.
18
+ *
19
+ * @param {string} src
20
+ * @returns {boolean}
21
+ */
22
+ export declare function isIconName(src: string): boolean;
23
+ /**
24
+ * The drawing of an icon, as a complete `svg` tag.
25
+ *
26
+ * @param {string} name Full name, `prefix:icon`.
27
+ * @returns {string}
28
+ * @throws {DocPensieveError} Set not installed, or icon absent from it.
29
+ */
30
+ export declare function iconSvg(name: string): string;
package/types/index.d.ts CHANGED
@@ -9,9 +9,11 @@
9
9
  * @module @docpensieve/components
10
10
  */
11
11
  export { classNames, cls, fallbackClass, getThemeClasses, getThemeFramework, setThemeClasses, setThemeFramework, } from './classes.js';
12
+ export { ADMONITION_KINDS, Admonition, getAdmonitionKinds, setAdmonitionKinds, } from './admonition.js';
12
13
  export { Card, CardBody, CardFooter, CardHeader, CardImage } from './card.js';
13
14
  export { Cards } from './cards.js';
14
15
  export { Column, Columns } from './columns.js';
16
+ export { Menu, MenuGroup, MenuLink } from './menu.js';
15
17
  export { FallbackAfter, FallbackBefore, TimeTimer } from './time-timer.js';
16
18
  export { TOOLTIP_PLACEMENTS, Tooltip } from './tooltip.js';
17
19
  export { Tree, TreeItem } from './tree.js';
@@ -0,0 +1,74 @@
1
+ /**
2
+ * A menu of links, placed anywhere in a page.
3
+ *
4
+ * The header already carries the navigation of the site, and the sidebar that
5
+ * of the documentation. This one belongs to a page: the links a section wants
6
+ * to offer where it stands — a summary at the top of a landing page, the
7
+ * chapters of a guide, the entries of a portal.
8
+ *
9
+ * It holds no state and loads no script. On a narrow screen it folds behind a
10
+ * button, a native `details` element: the menu is therefore written once and
11
+ * rendered twice, a row and a fold, of which the stylesheet shows one. The
12
+ * alternative, a single rendering revealed by a recent CSS pseudo-element,
13
+ * hides the links outright on a browser that does not know it.
14
+ *
15
+ * Classes come from the theme (ADR-007), never from a hard-coded framework.
16
+ *
17
+ * @module @docpensieve/components/menu
18
+ */
19
+ /**
20
+ * Menu of links.
21
+ *
22
+ * @param {{
23
+ * className?: string, style?: object, children?: any, label?: string,
24
+ * }} props `label` names the menu for screen readers, and labels the button it
25
+ * folds into on a narrow screen.
26
+ */
27
+ export declare function Menu({ className, style, children, label }: {
28
+ className?: string;
29
+ style?: object;
30
+ children?: any;
31
+ label?: string;
32
+ }): import("react").DetailedReactHTMLElement<{
33
+ className: string | undefined;
34
+ style: object | undefined;
35
+ }, HTMLElement>;
36
+ /**
37
+ * Entry of a menu.
38
+ *
39
+ * @param {{
40
+ * className?: string, style?: object, children?: any, href?: string,
41
+ * }} props
42
+ * @throws {DocPensieveError} Outside a `Menu`, or without a target.
43
+ */
44
+ export declare function MenuLink({ className, style, children, href }: {
45
+ className?: string;
46
+ style?: object;
47
+ children?: any;
48
+ href?: string;
49
+ }): import("react").DetailedReactHTMLElement<{
50
+ className: string | undefined;
51
+ style: object | undefined;
52
+ href: string | undefined;
53
+ }, HTMLElement>;
54
+ /**
55
+ * Group of entries, folded under a title.
56
+ *
57
+ * In the row it opens as a panel below its title; folded, it unfolds in place
58
+ * rather than over the rest — on a narrow screen a panel would open off
59
+ * screen.
60
+ *
61
+ * @param {{
62
+ * className?: string, style?: object, children?: any, title?: string,
63
+ * }} props
64
+ * @throws {DocPensieveError} Outside a `Menu`, or without a title.
65
+ */
66
+ export declare function MenuGroup({ className, style, children, title }: {
67
+ className?: string;
68
+ style?: object;
69
+ children?: any;
70
+ title?: string;
71
+ }): import("react").DetailedReactHTMLElement<{
72
+ className: string | undefined;
73
+ style: object | undefined;
74
+ }, HTMLElement>;