@markii/html 0.6.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/dist/components/badge.d.ts +9 -0
- package/dist/components/badge.js +24 -0
- package/dist/components/callout.d.ts +10 -0
- package/dist/components/callout.js +29 -0
- package/dist/components/card.d.ts +9 -0
- package/dist/components/card.js +15 -0
- package/dist/components/cell.d.ts +9 -0
- package/dist/components/cell.js +10 -0
- package/dist/components/chart.d.ts +15 -0
- package/dist/components/chart.js +158 -0
- package/dist/components/details.d.ts +10 -0
- package/dist/components/details.js +16 -0
- package/dist/components/figure.d.ts +18 -0
- package/dist/components/figure.js +28 -0
- package/dist/components/index.d.ts +27 -0
- package/dist/components/index.js +91 -0
- package/dist/components/kbd.d.ts +7 -0
- package/dist/components/kbd.js +8 -0
- package/dist/components/layout-wrapper.d.ts +23 -0
- package/dist/components/layout-wrapper.js +45 -0
- package/dist/components/progress.d.ts +13 -0
- package/dist/components/progress.js +79 -0
- package/dist/components/rating.d.ts +9 -0
- package/dist/components/rating.js +33 -0
- package/dist/components/row.d.ts +9 -0
- package/dist/components/row.js +19 -0
- package/dist/components/stat.d.ts +18 -0
- package/dist/components/stat.js +81 -0
- package/dist/components/tab.d.ts +18 -0
- package/dist/components/tab.js +19 -0
- package/dist/components/tabs.d.ts +30 -0
- package/dist/components/tabs.js +33 -0
- package/dist/doc-css.generated.d.ts +2 -0
- package/dist/doc-css.generated.js +5 -0
- package/dist/document.d.ts +42 -0
- package/dist/document.js +44 -0
- package/dist/escape.d.ts +7 -0
- package/dist/escape.js +23 -0
- package/dist/failure-presentation.d.ts +14 -0
- package/dist/failure-presentation.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/layout.d.ts +31 -0
- package/dist/layout.js +75 -0
- package/dist/registry.d.ts +139 -0
- package/dist/registry.js +118 -0
- package/dist/render.d.ts +34 -0
- package/dist/render.js +417 -0
- package/dist/resolve.d.ts +59 -0
- package/dist/resolve.js +154 -0
- package/dist/test/html-context.d.ts +9 -0
- package/dist/test/html-context.js +21 -0
- package/dist/value-format.d.ts +12 -0
- package/dist/value-format.js +41 -0
- package/package.json +64 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { safeRead } from '../resolve.js';
|
|
2
|
+
import { dataStateClassName, failureTitle } from '../failure-presentation.js';
|
|
3
|
+
const DEFAULT_MAX = 1;
|
|
4
|
+
/** Parses a numeric string defensively: non-numeric, `NaN`, and `±Infinity` all fall back to `fallback`. */
|
|
5
|
+
function parseFiniteNumber(raw, fallback) {
|
|
6
|
+
if (raw === null || raw === undefined)
|
|
7
|
+
return fallback;
|
|
8
|
+
const parsed = Number(raw);
|
|
9
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
10
|
+
}
|
|
11
|
+
function clamp(value, min, max) {
|
|
12
|
+
return Math.min(Math.max(value, min), max);
|
|
13
|
+
}
|
|
14
|
+
function coerceNumber(value) {
|
|
15
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
16
|
+
return value;
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
if (Number.isFinite(parsed))
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reads `value`/`max` off a bound `data` value: a bare finite number
|
|
26
|
+
* supplies `value` alone; a plain object may supply either/both fields. MAY
|
|
27
|
+
* THROW for a hostile bound value — guarded at the call site via `safeRead`.
|
|
28
|
+
*/
|
|
29
|
+
function readProgressFields(data) {
|
|
30
|
+
if (typeof data === 'number') {
|
|
31
|
+
return { value: Number.isFinite(data) ? data : undefined };
|
|
32
|
+
}
|
|
33
|
+
if (data !== null && typeof data === 'object' && !Array.isArray(data)) {
|
|
34
|
+
const record = data;
|
|
35
|
+
return {
|
|
36
|
+
value: coerceNumber(record.value),
|
|
37
|
+
max: coerceNumber(record.max),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `::progress{value=3 max=5 label="tasks"}` — a meter bar. Data binding
|
|
44
|
+
* (§8): a bound numeric `data` supplies `value`; a bound object may supply
|
|
45
|
+
* `value`/`max` — explicit directive attributes always win. Parses
|
|
46
|
+
* defensively: non-numeric/`NaN`/`Infinity` input (from either source) falls
|
|
47
|
+
* back to `0` (value) or the default `max` of `1`; `value` is then clamped
|
|
48
|
+
* to `[0, max]` and `max` is guarded to be positive. Missing/error binding
|
|
49
|
+
* renders a `0%` bar rather than crashing.
|
|
50
|
+
*
|
|
51
|
+
* Markup and class names match `@markii/react`'s `Progress` byte-for-byte.
|
|
52
|
+
*/
|
|
53
|
+
export const Progress = (attributes, _childrenHtml, ctx) => {
|
|
54
|
+
const { data, dataStatus, dataError, dataFailureKind } = ctx;
|
|
55
|
+
const bound = safeRead(() => dataStatus === 'missing' || dataStatus === 'error'
|
|
56
|
+
? {}
|
|
57
|
+
: readProgressFields(data), () => ({}));
|
|
58
|
+
const fromData = bound.fields;
|
|
59
|
+
const rawMax = parseFiniteNumber(attributes.max, fromData.max ?? DEFAULT_MAX);
|
|
60
|
+
const max = rawMax > 0 ? rawMax : DEFAULT_MAX;
|
|
61
|
+
const rawValue = parseFiniteNumber(attributes.value, fromData.value ?? 0);
|
|
62
|
+
const value = clamp(rawValue, 0, max);
|
|
63
|
+
const percent = clamp((value / max) * 100, 0, 100);
|
|
64
|
+
const label = attributes.label ?? null;
|
|
65
|
+
const className = dataStateClassName('mk-progress', dataStatus, dataFailureKind);
|
|
66
|
+
const title = failureTitle(dataError ?? bound.fault, dataFailureKind);
|
|
67
|
+
const titleAttr = title ? ` title="${ctx.esc(title)}"` : '';
|
|
68
|
+
const labelHtml = label
|
|
69
|
+
? `<span class="mk-progress__label">${ctx.esc(label)}</span>`
|
|
70
|
+
: '';
|
|
71
|
+
return (`<div class="${className}"${titleAttr} role="progressbar" ` +
|
|
72
|
+
`aria-valuenow="${String(value)}" aria-valuemin="0" aria-valuemax="${String(max)}">` +
|
|
73
|
+
`${labelHtml}` +
|
|
74
|
+
`<div class="mk-progress__track">` +
|
|
75
|
+
`<div class="mk-progress__bar" style="width: ${String(percent)}%"></div>` +
|
|
76
|
+
`</div>` +
|
|
77
|
+
`<span class="mk-progress__percent">${String(Math.round(percent))}%</span>` +
|
|
78
|
+
`</div>`);
|
|
79
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::rating{value=3 max=5}` — a leaf directive rendering a row of stars.
|
|
4
|
+
* Both attributes are optional and clamped to sane bounds; malformed input
|
|
5
|
+
* degrades gracefully instead of throwing or rendering something
|
|
6
|
+
* nonsensical. Matches `@markii/react`'s `Rating` markup byte-for-byte so
|
|
7
|
+
* one stylesheet covers both renderers.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Rating: HtmlComponent;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const DEFAULT_MAX = 5;
|
|
2
|
+
const MIN_MAX = 1;
|
|
3
|
+
const MAX_MAX = 20;
|
|
4
|
+
function parseCount(raw, fallback) {
|
|
5
|
+
if (raw === null || raw === undefined)
|
|
6
|
+
return fallback;
|
|
7
|
+
const parsed = Number.parseInt(raw, 10);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
9
|
+
}
|
|
10
|
+
function clamp(value, min, max) {
|
|
11
|
+
return Math.min(Math.max(value, min), max);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `::rating{value=3 max=5}` — a leaf directive rendering a row of stars.
|
|
15
|
+
* Both attributes are optional and clamped to sane bounds; malformed input
|
|
16
|
+
* degrades gracefully instead of throwing or rendering something
|
|
17
|
+
* nonsensical. Matches `@markii/react`'s `Rating` markup byte-for-byte so
|
|
18
|
+
* one stylesheet covers both renderers.
|
|
19
|
+
*/
|
|
20
|
+
export const Rating = (attributes) => {
|
|
21
|
+
const max = clamp(parseCount(attributes.max, DEFAULT_MAX), MIN_MAX, MAX_MAX);
|
|
22
|
+
const value = clamp(parseCount(attributes.value, 0), 0, max);
|
|
23
|
+
let stars = '';
|
|
24
|
+
for (let index = 0; index < max; index += 1) {
|
|
25
|
+
const filled = index < value;
|
|
26
|
+
const className = filled
|
|
27
|
+
? 'mk-rating__star mk-rating__star--filled'
|
|
28
|
+
: 'mk-rating__star';
|
|
29
|
+
stars += `<span class="${className}" aria-hidden="true">${filled ? '★' : '☆'}</span>`;
|
|
30
|
+
}
|
|
31
|
+
return (`<div class="mk-rating" role="img" aria-label="rating: ${String(value)} out of ${String(max)}">` +
|
|
32
|
+
`${stars}</div>`);
|
|
33
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `:::row{cols=3} ... :::` — docs/format.md's one layout *container*. An
|
|
4
|
+
* absent or invalid `cols` value degrades to plain `mk-row` (auto-fit)
|
|
5
|
+
* rather than an error. Matches `@markii/react`'s `Row` markup byte-for-byte
|
|
6
|
+
* so one stylesheet covers both renderers. No outer margin: the document
|
|
7
|
+
* stylesheet owns spacing between this and its siblings.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Row: HtmlComponent;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The exact `cols` values that select a fixed-column-count class; anything else degrades to auto-fit. */
|
|
2
|
+
const COLS_VALUES = ['2', '3', '4'];
|
|
3
|
+
function isColsValue(value) {
|
|
4
|
+
return COLS_VALUES.includes(value);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `:::row{cols=3} ... :::` — docs/format.md's one layout *container*. An
|
|
8
|
+
* absent or invalid `cols` value degrades to plain `mk-row` (auto-fit)
|
|
9
|
+
* rather than an error. Matches `@markii/react`'s `Row` markup byte-for-byte
|
|
10
|
+
* so one stylesheet covers both renderers. No outer margin: the document
|
|
11
|
+
* stylesheet owns spacing between this and its siblings.
|
|
12
|
+
*/
|
|
13
|
+
export const Row = (attributes, childrenHtml) => {
|
|
14
|
+
const rawCols = attributes.cols ?? '';
|
|
15
|
+
const className = isColsValue(rawCols)
|
|
16
|
+
? `mk-row mk-row--cols-${rawCols}`
|
|
17
|
+
: 'mk-row';
|
|
18
|
+
return `<div class="${className}">${childrenHtml}</div>`;
|
|
19
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::stat{value=42 label="stars" trend=up}` — a big value + label, with an
|
|
4
|
+
* optional delta/trend annotation. Data binding (§8): if the bound `data`
|
|
5
|
+
* value is a number/string it supplies `value`; if it is an object, its
|
|
6
|
+
* `value`/`label`/`delta`/`trend` fields are read — an explicit directive
|
|
7
|
+
* attribute always wins over the bound object's field. Missing value (from
|
|
8
|
+
* either source) renders `—` rather than a blank box; a missing or errored
|
|
9
|
+
* binding degrades the same way. Never throws.
|
|
10
|
+
*
|
|
11
|
+
* Failure presentation mirrors `@markii/react`'s `Stat` exactly
|
|
12
|
+
* (docs/scripting.md, AGENTS.md's cleanliness principle): the BODY stays
|
|
13
|
+
* quiet — `—`, or whatever static attributes supplied — and a failed/stale
|
|
14
|
+
* binding surfaces only as a `title` tooltip plus a modifier class on the
|
|
15
|
+
* root element (`mk-stat--stale`, `mk-stat--tier-blocked`, ...). Markup and
|
|
16
|
+
* class names match `@markii/react`'s `Stat` byte-for-byte.
|
|
17
|
+
*/
|
|
18
|
+
export declare const Stat: HtmlComponent;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { safeRead } from '../resolve.js';
|
|
2
|
+
import { dataStateClassName, failureTitle } from '../failure-presentation.js';
|
|
3
|
+
const EMPTY_VALUE = '—';
|
|
4
|
+
const TRENDS = ['up', 'down', 'flat'];
|
|
5
|
+
function isTrend(value) {
|
|
6
|
+
return TRENDS.includes(value);
|
|
7
|
+
}
|
|
8
|
+
/** Coerces an unknown field of a bound `data` object to a display string, or `undefined` if it isn't string/number/boolean. */
|
|
9
|
+
function coerceField(value) {
|
|
10
|
+
if (typeof value === 'string')
|
|
11
|
+
return value;
|
|
12
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads `value`/`label`/`delta`/`trend` off a bound `data` value. Only a
|
|
19
|
+
* plain object contributes named fields; a bare number/string contributes
|
|
20
|
+
* `value` alone. MAY THROW for a hostile bound value — guarded at the call
|
|
21
|
+
* site via `safeRead`.
|
|
22
|
+
*/
|
|
23
|
+
function readStatFields(data) {
|
|
24
|
+
if (typeof data === 'number' || typeof data === 'string') {
|
|
25
|
+
return { value: String(data) };
|
|
26
|
+
}
|
|
27
|
+
if (data !== null && typeof data === 'object' && !Array.isArray(data)) {
|
|
28
|
+
const record = data;
|
|
29
|
+
return {
|
|
30
|
+
value: coerceField(record.value),
|
|
31
|
+
label: coerceField(record.label),
|
|
32
|
+
delta: coerceField(record.delta),
|
|
33
|
+
trend: coerceField(record.trend),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
/** Explicit directive attributes win over the bound `data` object's own fields. */
|
|
39
|
+
function pick(attribute, fromData) {
|
|
40
|
+
return attribute ?? fromData ?? undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `::stat{value=42 label="stars" trend=up}` — a big value + label, with an
|
|
44
|
+
* optional delta/trend annotation. Data binding (§8): if the bound `data`
|
|
45
|
+
* value is a number/string it supplies `value`; if it is an object, its
|
|
46
|
+
* `value`/`label`/`delta`/`trend` fields are read — an explicit directive
|
|
47
|
+
* attribute always wins over the bound object's field. Missing value (from
|
|
48
|
+
* either source) renders `—` rather than a blank box; a missing or errored
|
|
49
|
+
* binding degrades the same way. Never throws.
|
|
50
|
+
*
|
|
51
|
+
* Failure presentation mirrors `@markii/react`'s `Stat` exactly
|
|
52
|
+
* (docs/scripting.md, AGENTS.md's cleanliness principle): the BODY stays
|
|
53
|
+
* quiet — `—`, or whatever static attributes supplied — and a failed/stale
|
|
54
|
+
* binding surfaces only as a `title` tooltip plus a modifier class on the
|
|
55
|
+
* root element (`mk-stat--stale`, `mk-stat--tier-blocked`, ...). Markup and
|
|
56
|
+
* class names match `@markii/react`'s `Stat` byte-for-byte.
|
|
57
|
+
*/
|
|
58
|
+
export const Stat = (attributes, _childrenHtml, ctx) => {
|
|
59
|
+
const { data, dataStatus, dataError, dataFailureKind } = ctx;
|
|
60
|
+
const bound = safeRead(() => dataStatus === 'missing' || dataStatus === 'error'
|
|
61
|
+
? {}
|
|
62
|
+
: readStatFields(data), () => ({}));
|
|
63
|
+
const fromData = bound.fields;
|
|
64
|
+
const value = pick(attributes.value, fromData.value);
|
|
65
|
+
const label = pick(attributes.label, fromData.label);
|
|
66
|
+
const delta = pick(attributes.delta, fromData.delta);
|
|
67
|
+
const rawTrend = pick(attributes.trend, fromData.trend);
|
|
68
|
+
const trend = rawTrend && isTrend(rawTrend) ? rawTrend : undefined;
|
|
69
|
+
const deltaHtml = delta
|
|
70
|
+
? `<span class="${trend ? `mk-stat__delta mk-stat__delta--${trend}` : 'mk-stat__delta'}">${ctx.esc(delta)}</span>`
|
|
71
|
+
: '';
|
|
72
|
+
const className = dataStateClassName('mk-stat', dataStatus, dataFailureKind);
|
|
73
|
+
const title = failureTitle(dataError ?? bound.fault, dataFailureKind);
|
|
74
|
+
const titleAttr = title ? ` title="${ctx.esc(title)}"` : '';
|
|
75
|
+
const labelHtml = label
|
|
76
|
+
? `<div class="mk-stat__label">${ctx.esc(label)}</div>`
|
|
77
|
+
: '';
|
|
78
|
+
return (`<div class="${className}"${titleAttr}>` +
|
|
79
|
+
`<div class="mk-stat__value">${ctx.esc(value || EMPTY_VALUE)}</div>` +
|
|
80
|
+
`${labelHtml}${deltaHtml}</div>`);
|
|
81
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/** Default label used when a `tab` directive has no `label` attribute (kept for parity with `@markii/react`; the HTML engine has no way to surface it, see `tabs.ts`). */
|
|
3
|
+
export declare const DEFAULT_TAB_LABEL = "Tab";
|
|
4
|
+
/**
|
|
5
|
+
* The panel markup a tab shows. Matches `@markii/react`'s `TabPanel`
|
|
6
|
+
* markup byte-for-byte.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tabPanel(childrenHtml: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* `:::tab{label="..."} ... :::` — one panel of a `tabs` component
|
|
11
|
+
* (`tabs.ts`). Rendered standalone (outside a `tabs` parent), it shows its
|
|
12
|
+
* own panel. `label` has no effect here: it is only meaningful to an
|
|
13
|
+
* enclosing `tabs`, and (unlike `@markii/react`, which can inspect its
|
|
14
|
+
* parent's structured React children) this string-based engine has no way
|
|
15
|
+
* for `tabs` to read a child directive's attributes — see `tabs.ts`'s doc
|
|
16
|
+
* comment for the resulting limitation.
|
|
17
|
+
*/
|
|
18
|
+
export declare const Tab: HtmlComponent;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Default label used when a `tab` directive has no `label` attribute (kept for parity with `@markii/react`; the HTML engine has no way to surface it, see `tabs.ts`). */
|
|
2
|
+
export const DEFAULT_TAB_LABEL = 'Tab';
|
|
3
|
+
/**
|
|
4
|
+
* The panel markup a tab shows. Matches `@markii/react`'s `TabPanel`
|
|
5
|
+
* markup byte-for-byte.
|
|
6
|
+
*/
|
|
7
|
+
export function tabPanel(childrenHtml) {
|
|
8
|
+
return `<div class="mk-tab" role="tabpanel">${childrenHtml}</div>`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `:::tab{label="..."} ... :::` — one panel of a `tabs` component
|
|
12
|
+
* (`tabs.ts`). Rendered standalone (outside a `tabs` parent), it shows its
|
|
13
|
+
* own panel. `label` has no effect here: it is only meaningful to an
|
|
14
|
+
* enclosing `tabs`, and (unlike `@markii/react`, which can inspect its
|
|
15
|
+
* parent's structured React children) this string-based engine has no way
|
|
16
|
+
* for `tabs` to read a child directive's attributes — see `tabs.ts`'s doc
|
|
17
|
+
* comment for the resulting limitation.
|
|
18
|
+
*/
|
|
19
|
+
export const Tab = (_attributes, childrenHtml) => tabPanel(childrenHtml);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HtmlComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* `::::tabs :::tab{label="..."} ... ::: :::tab{label="..."} ... ::: ::::` —
|
|
4
|
+
* a tabbed panel switcher.
|
|
5
|
+
*
|
|
6
|
+
* FAITHFULNESS LIMITATION: `@markii/react`'s `Tabs` inspects its own
|
|
7
|
+
* (structured) React children to find `tab` directives, reads each one's
|
|
8
|
+
* `label` attribute, and renders a `role="tablist"` button bar plus only
|
|
9
|
+
* the active panel, switching on click via `useState`. This HTML engine
|
|
10
|
+
* hands every component its children pre-rendered to a single opaque HTML
|
|
11
|
+
* string (`childrenHtml`) — by the time `Tabs` runs, each `tab` child has
|
|
12
|
+
* already been rendered by the `Tab` component above into its own
|
|
13
|
+
* `.mk-tab` panel, and the `label` attribute that lived on each `tab`
|
|
14
|
+
* directive is gone; there is no supported way for this component to
|
|
15
|
+
* recover it, and the engine's render pipeline (`render.ts`) is explicitly
|
|
16
|
+
* out of scope to change for this.
|
|
17
|
+
*
|
|
18
|
+
* So this is the simplest correct rendering available from a string: every
|
|
19
|
+
* tab panel is shown, in document order, with no tablist button bar and no
|
|
20
|
+
* JS-driven active-tab switching (this package is zero-JS by design; see
|
|
21
|
+
* `@markii/html`'s package description). A reader sees all tabs' content at
|
|
22
|
+
* once, wrapped in `.mk-tabs` so the surrounding CSS still applies to
|
|
23
|
+
* whatever it can (panel spacing via `.mk-tab`). Static-HTML consumers
|
|
24
|
+
* (publishing, CI, archive) generally want the content anyway, and no
|
|
25
|
+
* content is silently dropped. A future slice could restore full
|
|
26
|
+
* faithfulness (e.g. CSS-only radio-button tabs) if `render.ts` grows a way
|
|
27
|
+
* to hand a container component its children pre-parsed rather than
|
|
28
|
+
* pre-rendered.
|
|
29
|
+
*/
|
|
30
|
+
export declare const Tabs: HtmlComponent;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `::::tabs :::tab{label="..."} ... ::: :::tab{label="..."} ... ::: ::::` —
|
|
3
|
+
* a tabbed panel switcher.
|
|
4
|
+
*
|
|
5
|
+
* FAITHFULNESS LIMITATION: `@markii/react`'s `Tabs` inspects its own
|
|
6
|
+
* (structured) React children to find `tab` directives, reads each one's
|
|
7
|
+
* `label` attribute, and renders a `role="tablist"` button bar plus only
|
|
8
|
+
* the active panel, switching on click via `useState`. This HTML engine
|
|
9
|
+
* hands every component its children pre-rendered to a single opaque HTML
|
|
10
|
+
* string (`childrenHtml`) — by the time `Tabs` runs, each `tab` child has
|
|
11
|
+
* already been rendered by the `Tab` component above into its own
|
|
12
|
+
* `.mk-tab` panel, and the `label` attribute that lived on each `tab`
|
|
13
|
+
* directive is gone; there is no supported way for this component to
|
|
14
|
+
* recover it, and the engine's render pipeline (`render.ts`) is explicitly
|
|
15
|
+
* out of scope to change for this.
|
|
16
|
+
*
|
|
17
|
+
* So this is the simplest correct rendering available from a string: every
|
|
18
|
+
* tab panel is shown, in document order, with no tablist button bar and no
|
|
19
|
+
* JS-driven active-tab switching (this package is zero-JS by design; see
|
|
20
|
+
* `@markii/html`'s package description). A reader sees all tabs' content at
|
|
21
|
+
* once, wrapped in `.mk-tabs` so the surrounding CSS still applies to
|
|
22
|
+
* whatever it can (panel spacing via `.mk-tab`). Static-HTML consumers
|
|
23
|
+
* (publishing, CI, archive) generally want the content anyway, and no
|
|
24
|
+
* content is silently dropped. A future slice could restore full
|
|
25
|
+
* faithfulness (e.g. CSS-only radio-button tabs) if `render.ts` grows a way
|
|
26
|
+
* to hand a container component its children pre-parsed rather than
|
|
27
|
+
* pre-rendered.
|
|
28
|
+
*/
|
|
29
|
+
export const Tabs = (_attributes, childrenHtml) => {
|
|
30
|
+
if (!childrenHtml.trim())
|
|
31
|
+
return '';
|
|
32
|
+
return `<div class="mk-tabs">${childrenHtml}</div>`;
|
|
33
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Regenerate with: node scripts/generate-doc-css.ts
|
|
3
|
+
// Source of truth: packages/platforms/markii-react/src/doc.css
|
|
4
|
+
/** The shared document stylesheet (@markii/react's doc.css), embedded as a string for exportHtmlDocument's <style> block. */
|
|
5
|
+
export const DOC_CSS = "/*\n * Document rhythm: components own their insides only, never outer margins.\n * This single rule spaces every block-level child of `.doc` identically —\n * paragraphs, headings, and components alike — so new components always\n * sit correctly in the flow with zero per-component tuning.\n */\n.doc > * + * {\n margin-block-start: 1rem;\n}\n\n.doc {\n color: #1a1a1a;\n font-family:\n system-ui,\n -apple-system,\n 'Segoe UI',\n sans-serif;\n line-height: 1.6;\n}\n\n.doc pre {\n overflow-x: auto;\n background: #f4f4f5;\n padding: 0.75rem 1rem;\n border-radius: 6px;\n}\n\n.doc code {\n background: #f0f0f2;\n border-radius: 3px;\n padding: 0.1em 0.35em;\n font-size: 0.9em;\n}\n\n.doc pre code {\n background: none;\n padding: 0;\n}\n\n/* ---------- GFM table ---------- */\n\n/*\n * `display: block` on the table itself (rather than wrapping it in an\n * extra element the renderer doesn't otherwise inject) is what makes a wide\n * table scroll horizontally instead of overflowing the page or the doc\n * column — the table box becomes independently scrollable content, the\n * same trick used by GitHub's own Markdown rendering.\n */\n.doc table {\n display: block;\n overflow-x: auto;\n border-collapse: collapse;\n font-size: 0.95em;\n}\n\n.doc th,\n.doc td {\n border: 1px solid #e4e4e7;\n padding: 0.4rem 0.75rem;\n text-align: left;\n}\n\n.doc th {\n font-weight: 600;\n background: #f8fafc;\n}\n\n.doc tr:nth-child(even) {\n background: #fafafa;\n}\n\n/* ---------- GFM task list ---------- */\n\n/*\n * `li:has(> input[type=\"checkbox\"])` scopes bullet removal + checkbox\n * alignment to task-list items only — an ordinary `<ul>`/`<ol>` item keeps\n * its normal bullet/number, since GFM only adds a leading `<input>` to\n * items that used `- [ ]`/`- [x]` syntax.\n */\n.doc li:has(> input[type='checkbox']) {\n list-style: none;\n margin-inline-start: -1.5em;\n}\n\n.doc li > input[type='checkbox'] {\n margin-inline-end: 0.5em;\n vertical-align: middle;\n}\n\n/* ---------- callout ---------- */\n\n.mk-callout {\n border: 1px solid var(--mk-callout-border, #94a3b8);\n border-left-width: 4px;\n border-radius: 6px;\n padding: 0.75rem 1rem;\n background: var(--mk-callout-bg, #f8fafc);\n}\n\n.mk-callout--info {\n --mk-callout-border: #3b82f6;\n --mk-callout-bg: #eff6ff;\n}\n\n.mk-callout--warning {\n --mk-callout-border: #d97706;\n --mk-callout-bg: #fffbeb;\n}\n\n.mk-callout--danger {\n --mk-callout-border: #dc2626;\n --mk-callout-bg: #fef2f2;\n}\n\n.mk-callout__header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-weight: 600;\n}\n\n.mk-callout__icon {\n line-height: 1;\n}\n\n/*\n * `display: flex; flex-direction: column` is load-bearing here, not just\n * `> * + *` margin: several block-holding components (`stat`, `badge`)\n * declare their OWN outer display as `inline-flex`/`inline-block` (correct\n * for sitting inline in a sentence), so without a flex/grid parent they\n * flow side-by-side on the same line instead of stacking — a\n * `margin-block-start` on a same-line inline-level sibling creates no\n * visible gap. Making the body a column flex container forces every\n * child's *used* display to blockify (CSS Flexbox: a flex item's outer\n * display is always block-level), so they stack regardless of the\n * component's own declared display, and the margin rule below then has\n * something to actually separate.\n */\n.mk-callout__body {\n display: flex;\n flex-direction: column;\n}\n\n.mk-callout__body > :first-child {\n margin-block-start: 0.5rem;\n}\n\n.mk-callout__body > * + * {\n margin-block-start: 0.5rem;\n}\n\n/* ---------- kbd ---------- */\n\n.mk-kbd {\n display: inline-block;\n vertical-align: baseline;\n line-height: 1.4;\n height: 1.4em;\n padding: 0 0.4em;\n font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace;\n font-size: 0.85em;\n border: 1px solid #c9ccd1;\n border-bottom-width: 2px;\n border-radius: 4px;\n background: #f6f7f9;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.05);\n}\n\n/* ---------- rating ---------- */\n\n.mk-rating {\n display: inline-flex;\n gap: 0.15em;\n font-size: 1.1em;\n color: #cbd5e1;\n}\n\n.mk-rating__star--filled {\n color: #f59e0b;\n}\n\n/* ---------- value interpolation ---------- */\n\n.mk-value {\n display: inline;\n vertical-align: baseline;\n}\n\n.mk-value--stale {\n color: #92400e;\n border-bottom: 1px dashed #d97706;\n}\n\n.mk-value--missing {\n font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace;\n font-size: 0.9em;\n color: #94a3b8;\n font-style: italic;\n}\n\n/*\n * Failure-kind modifiers (`@markii/runtime`'s `FailureKind`, docs/scripting.md) —\n * layered on top of `.mk-value--missing`, only ever added when the\n * resolution's root entry carried a `failureKind` on a genuine error (see\n * `ValueDirective`). Each just tints the dashed-underline treatment a\n * distinct hue so a reader can tell \"script bug\" apart from \"needs\n * permission\" / \"needs a manual run\" / \"hit a resource limit\" at a glance,\n * with the full message still available via the `title` tooltip.\n */\n.mk-value--script-error {\n border-bottom: 1px dashed #ef4444;\n}\n\n.mk-value--capability-denied {\n border-bottom: 1px dashed #d97706;\n}\n\n.mk-value--tier-blocked {\n border-bottom: 1px dashed #2563eb;\n}\n\n.mk-value--limit {\n border-bottom: 1px dashed #7c3aed;\n}\n\n/* ---------- script marker ---------- */\n\n.mk-script {\n border: 1px solid #d4d4d8;\n border-radius: 6px;\n background: #fafafa;\n}\n\n.mk-script__summary {\n cursor: pointer;\n padding: 0.5rem 0.75rem;\n font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace;\n font-size: 0.85em;\n color: #52525b;\n}\n\n.mk-script[open] > .mk-script__summary {\n border-bottom: 1px solid #e4e4e7;\n}\n\n.mk-script__code {\n margin: 0.75rem;\n margin-block-start: 0;\n}\n\n.mk-script__empty {\n margin: 0.75rem;\n margin-block-start: 0;\n font-size: 0.85em;\n font-style: italic;\n color: #94a3b8;\n}\n\n/* ---------- details ---------- */\n\n.mk-details {\n border: 1px solid #d4d4d8;\n border-radius: 6px;\n padding: 0.75rem 1rem;\n background: #fafafa;\n}\n\n.mk-details__summary {\n cursor: pointer;\n font-weight: 600;\n}\n\n.mk-details[open] > .mk-details__summary {\n margin-block-end: 0.5rem;\n}\n\n/* Blockifies inline-flex/inline-block children (`stat`, `badge`, ...) so they stack — see `.mk-callout__body`'s comment above for why this is load-bearing. */\n.mk-details__body {\n display: flex;\n flex-direction: column;\n}\n\n.mk-details__body > :first-child {\n margin-block-start: 0.5rem;\n}\n\n.mk-details__body > * + * {\n margin-block-start: 0.5rem;\n}\n\n/* ---------- card ---------- */\n\n.mk-card {\n border: 1px solid #e4e4e7;\n border-radius: 8px;\n padding: 1rem;\n background: #fff;\n}\n\n.mk-card__title {\n font-weight: 600;\n margin-block-end: 0.5rem;\n}\n\n/* Blockifies inline-flex/inline-block children (`stat`, `badge`, ...) so they stack — see `.mk-callout__body`'s comment above for why this is load-bearing. */\n.mk-card__body {\n display: flex;\n flex-direction: column;\n}\n\n.mk-card__body > :first-child {\n margin-block-start: 0;\n}\n\n.mk-card__body > * + * {\n margin-block-start: 0.5rem;\n}\n\n/* ---------- badge ---------- */\n\n.mk-badge {\n display: inline-block;\n vertical-align: baseline;\n line-height: 1.4;\n height: 1.4em;\n padding: 0 0.6em;\n font-size: 0.8em;\n font-weight: 600;\n border-radius: 999px;\n color: var(--mk-badge-fg, #334155);\n background: var(--mk-badge-bg, #e2e8f0);\n}\n\n.mk-badge--info {\n --mk-badge-fg: #1d4ed8;\n --mk-badge-bg: #dbeafe;\n}\n\n.mk-badge--success {\n --mk-badge-fg: #15803d;\n --mk-badge-bg: #dcfce7;\n}\n\n.mk-badge--warning {\n --mk-badge-fg: #b45309;\n --mk-badge-bg: #fef3c7;\n}\n\n.mk-badge--danger {\n --mk-badge-fg: #b91c1c;\n --mk-badge-bg: #fee2e2;\n}\n\n/* ---------- figure ---------- */\n\n.mk-figure {\n margin: 0;\n}\n\n.mk-figure__img {\n display: block;\n max-width: 100%;\n height: auto;\n border-radius: 6px;\n}\n\n.mk-figure__caption {\n margin-block-start: 0.5rem;\n font-size: 0.9em;\n color: #52525b;\n}\n\n.mk-figure__caption > :first-child {\n margin-block-start: 0;\n}\n\n.mk-figure__caption > * + * {\n margin-block-start: 0.5rem;\n}\n\n/* ---------- tabs ---------- */\n\n.mk-tabs__list {\n display: flex;\n gap: 0.25rem;\n border-bottom: 1px solid #e4e4e7;\n}\n\n.mk-tabs__button {\n cursor: pointer;\n border: none;\n background: none;\n padding: 0.5rem 0.9rem;\n font: inherit;\n font-weight: 600;\n color: #71717a;\n border-bottom: 2px solid transparent;\n margin-block-end: -1px;\n}\n\n.mk-tabs__button--active {\n color: #1d4ed8;\n border-bottom-color: #1d4ed8;\n}\n\n/* Blockifies inline-flex/inline-block children (`stat`, `badge`, ...) so they stack — see `.mk-callout__body`'s comment above for why this is load-bearing. */\n.mk-tab {\n display: flex;\n flex-direction: column;\n padding-block-start: 0.75rem;\n}\n\n.mk-tab > :first-child {\n margin-block-start: 0;\n}\n\n.mk-tab > * + * {\n margin-block-start: 0.5rem;\n}\n\n/* ---------- stat ---------- */\n\n.mk-stat {\n display: inline-flex;\n flex-direction: column;\n gap: 0.15rem;\n}\n\n.mk-stat__value {\n font-size: 1.8em;\n font-weight: 700;\n line-height: 1.2;\n}\n\n.mk-stat__label {\n font-size: 0.85em;\n color: #71717a;\n}\n\n.mk-stat__delta {\n font-size: 0.85em;\n font-weight: 600;\n color: #71717a;\n}\n\n.mk-stat__delta--up {\n color: #15803d;\n}\n\n.mk-stat__delta--down {\n color: #b91c1c;\n}\n\n.mk-stat__delta--flat {\n color: #71717a;\n}\n\n/* ---------- progress ---------- */\n\n.mk-progress {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n}\n\n.mk-progress__label {\n font-size: 0.85em;\n color: #52525b;\n flex: 0 0 auto;\n}\n\n.mk-progress__track {\n flex: 1 1 auto;\n height: 0.6rem;\n border-radius: 999px;\n background: #e4e4e7;\n overflow: hidden;\n}\n\n.mk-progress__bar {\n height: 100%;\n background: #3b82f6;\n border-radius: inherit;\n}\n\n.mk-progress__percent {\n flex: 0 0 auto;\n font-size: 0.85em;\n font-variant-numeric: tabular-nums;\n color: #52525b;\n}\n\n/* ---------- chart ---------- */\n\n.mk-chart {\n display: block;\n max-width: 100%;\n}\n\n.mk-chart__line {\n fill: none;\n stroke: #3b82f6;\n stroke-width: 2;\n stroke-linejoin: round;\n stroke-linecap: round;\n}\n\n.mk-chart__bar {\n fill: #3b82f6;\n}\n\n.mk-chart--empty {\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.8em;\n font-style: italic;\n color: #94a3b8;\n border: 1px dashed #d4d4d8;\n border-radius: 6px;\n}\n\n/* ---------- data-binding state (docs/scripting.md) ---------- */\n\n/*\n * The block twins of the `.mk-value--*` markers above: a data-bound\n * component (`stat`, `progress`, `chart`) whose `data=` binding is stale or\n * failed keeps its ordinary QUIET body — `—`, a `0%` bar, `no data` — and\n * says so only through these hooks plus its `title` tooltip. No rule here\n * may add body text (`content:` is deliberately absent) or an outer margin;\n * the failure taxonomy's wording lives in one place only, `components/\n * failure-presentation.ts`.\n *\n * The hues match `.mk-value--*` exactly, so the same failing name reads the\n * same whether it surfaced inline via `:value[...]` or as a component.\n */\n.mk-stat--stale,\n.mk-progress--stale,\n.mk-chart--stale {\n opacity: 0.8;\n}\n\n.mk-stat--script-error,\n.mk-progress--script-error,\n.mk-chart--script-error {\n border-bottom: 2px solid #ef4444;\n}\n\n.mk-stat--capability-denied,\n.mk-progress--capability-denied,\n.mk-chart--capability-denied {\n border-bottom: 2px solid #d97706;\n}\n\n.mk-stat--tier-blocked,\n.mk-progress--tier-blocked,\n.mk-chart--tier-blocked {\n border-bottom: 2px solid #2563eb;\n}\n\n.mk-stat--limit,\n.mk-progress--limit,\n.mk-chart--limit {\n border-bottom: 2px solid #7c3aed;\n}\n\n/* ---------- layout presets (docs/format.md) ---------- */\n\n/*\n * `render.tsx` only wraps a directive in a `mk-width-*`/`mk-align-*` `<div>`\n * when at least one of these classes actually applies, so the wrapper below\n * IS the element sitting directly in `.doc`'s rhythm flow — never\n * `margin-block` here, that's `.doc > * + *`'s job alone; setting it on the\n * wrapper too would double up spacing. `max-width: min(<size>, 100%)` keeps\n * every preset from ever overflowing the document column, even on a narrow\n * viewport where the size below is wider than the column itself.\n */\n.mk-width-narrow {\n max-width: min(30rem, 100%);\n}\n\n.mk-width-wide {\n max-width: min(64rem, 100%);\n}\n\n/* \"full\" is the full available column width — not a viewport-bleed hack with negative margins. */\n.mk-width-full {\n max-width: 100%;\n}\n\n.mk-align-left {\n margin-inline-end: auto;\n}\n\n.mk-align-center {\n margin-inline: auto;\n}\n\n.mk-align-right {\n margin-inline-start: auto;\n}\n\n/*\n * The five `:::center`/`:::right`/`:::wide`/`:::narrow`/`:::full` layout\n * wrappers (docs/format.md, `layout-wrapper.tsx`) reuse the `mk-width-*`/\n * `mk-align-*` classes above and add `mk-layout` on top for the rules below,\n * which only make sense on a container that has its OWN plain-markdown\n * children (a table, an image, a paragraph) rather than on the bare\n * attribute-interception wrapper `render.tsx` emits for `width=`/`align=`.\n *\n * No outer margin on `.mk-layout` itself, same rule as every component\n * (Architecture rule 4) — `.doc > * + *` spaces the wrapper against its\n * siblings. This rule instead restores RHYTHM *inside* the wrapper's own\n * scope, mirroring `.mk-card__body > * + *`: without it, the wrapper's\n * children would have no spacing between them at all, since `.doc > * + *`\n * only ever sees the wrapper `<div>` as a whole, never reaches inside it.\n */\n.mk-layout > * + * {\n margin-block-start: 1rem;\n}\n\n/* `center`/`right` additionally set text alignment for everything in scope — not just the shrink-to-fit block alignment below. */\n.mk-layout.mk-align-center {\n text-align: center;\n}\n\n.mk-layout.mk-align-right {\n text-align: right;\n}\n\n/*\n * `.mk-align-center`/`.mk-align-right` above (shared with the `align=`\n * attribute wrapper) only center/right-align the wrapper `<div>` ITSELF\n * within ITS container — they say nothing about the wrapper's own children.\n * These two rules do that: they shrink-to-fit and align every direct child\n * of the wrapper's scope, which is what actually centers/right-aligns a\n * narrower-than-column table or image sitting inside `:::center`/`:::right`.\n */\n.mk-layout.mk-align-center > * {\n margin-inline: auto;\n}\n\n.mk-layout.mk-align-right > * {\n margin-inline-start: auto;\n margin-inline-end: 0;\n}\n\n/*\n * `.doc table` (above) sets `display: block` for horizontal-scroll\n * overflow, which also makes the table fill its column — defeating\n * shrink-to-fit alignment before it can even apply. These two rules size a\n * table down to its content instead, so the `margin-inline` rules above\n * have a narrower box to actually move. `.doc th`/`.doc td` set\n * `text-align: left` directly on the cells, so this scope's `text-align`\n * never flips table cell text — only the table's own position in the\n * scope, and any non-table text alongside it.\n */\n.mk-layout.mk-align-center > table,\n.mk-layout.mk-align-right > table {\n width: fit-content;\n max-width: 100%;\n}\n\n/* ---------- row ---------- */\n\n.mk-row {\n display: grid;\n gap: 1rem;\n grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));\n}\n\n/*\n * `minmax(0, 1fr)` — a bare `1fr` track can still grow past an equal share\n * to fit a wide intrinsic-content cell (e.g. a `chart` SVG, a long code\n * span); pinning the minimum to `0` is what keeps such a cell, and\n * therefore the whole row, from blowing out past its column.\n */\n.mk-row--cols-2 {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.mk-row--cols-3 {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n.mk-row--cols-4 {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n}\n\n/*\n * No separate blockification rule is needed here, unlike `.mk-card__body`'s:\n * a grid container (which `.mk-row` is) already promotes every direct\n * child's outer display to block-level as a grid item, same as a flex\n * container does — an inline-flex/inline-block component (`stat`, `badge`,\n * ...) used directly as a row cell already stacks/sizes as a proper grid\n * cell with no extra rule required.\n */\n\n@media (max-width: 40rem) {\n .mk-row,\n .mk-row--cols-2,\n .mk-row--cols-3,\n .mk-row--cols-4 {\n grid-template-columns: 1fr;\n }\n}\n\n/*\n * `:::row{align=...}` (docs/format.md): `align` is intercepted like on any\n * other block directive (`render.tsx`/`layout.ts`), wrapping `.mk-row` in\n * the same generic `mk-align-*` `<div>` every other directive gets — but\n * `margin-inline: auto` (the rule above, under \"layout presets\") has\n * nothing to move: a `.mk-row` grid already fills the document column, so\n * shrink-to-fit placement of the row itself is meaningless. These three\n * rules give `align` on a row its own, more useful meaning instead: they\n * set `text-align` on the row, which every cell's content INHERITS (plain\n * CSS inheritance — `.mk-cell` sets no `text-align` of its own, so nothing\n * blocks it). Scoped to `> .mk-row` specifically, so a non-row directive's\n * `align` wrapper is completely unaffected.\n *\n * Locality wins for free: a more local `:::center`/`:::left`/`:::right`\n * wrapper written inside one cell (`.mk-layout.mk-align-*` above) sets\n * `text-align` directly on itself, and an element's own declared value\n * always wins over one it only inherited — no specificity fight needed.\n */\n.mk-align-left > .mk-row {\n text-align: left;\n}\n\n.mk-align-center > .mk-row {\n text-align: center;\n}\n\n.mk-align-right > .mk-row {\n text-align: right;\n}\n\n/* ---------- cell ---------- */\n\n/*\n * `:::cell` (`cell.tsx`) is a TRANSPARENT grouping container: it has no\n * border, background, padding, or outer margin of its own — its whole job is\n * making several blocks count as ONE `.mk-row` grid cell, so anything visual\n * here would betray that. `.mk-cell` therefore has no rule at all; only the\n * rhythm-restoring rule below exists, mirroring `.mk-layout > * + *`:\n * `.doc > * + *` sees the cell as a single box and never reaches inside it,\n * so without this its children would sit flush against each other.\n */\n.mk-cell > * + * {\n margin-block-start: 1rem;\n}\n\n/* ---------- unknown directive fallback ---------- */\n\n.mk-unknown {\n border: 1px dashed #94a3b8;\n border-radius: 6px;\n color: #475569;\n}\n\n.mk-unknown--block {\n padding: 0.75rem 1rem;\n}\n\n.mk-unknown--inline {\n display: inline-flex;\n align-items: baseline;\n gap: 0.4em;\n padding: 0 0.4em;\n vertical-align: baseline;\n}\n\n.mk-unknown__label {\n font-size: 0.85em;\n font-style: italic;\n margin: 0;\n}\n\n.mk-unknown__content > :first-child {\n margin-block-start: 0.5rem;\n}\n\n.mk-unknown__content > * + * {\n margin-block-start: 0.5rem;\n}\n";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `exportHtmlDocument`'s options. Everything is optional: with no options at
|
|
3
|
+
* all the result is a complete, valid, self-contained HTML document — the
|
|
4
|
+
* "publish a note" default this function exists for (AGENTS.md: `@markii/html`
|
|
5
|
+
* is "for stopped-changing documents (publish/CI/email/archive)").
|
|
6
|
+
*/
|
|
7
|
+
export interface ExportHtmlDocumentOptions {
|
|
8
|
+
/** The document `<title>`. Defaults to `'Markii document'`. HTML-escaped. */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** The document `<html lang="...">` attribute. Defaults to `'en'`. */
|
|
11
|
+
lang?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Extra CSS appended after the shared `doc.css` (e.g. a host's own theme
|
|
14
|
+
* overrides). Inserted verbatim inside the `<style>` block — this is
|
|
15
|
+
* trusted host-authored CSS, not user content, so it is never escaped.
|
|
16
|
+
*/
|
|
17
|
+
extraCss?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wraps an already-rendered document body (typically `renderMarkToHtml`'s
|
|
21
|
+
* output) in a complete, self-contained HTML document: doctype, `<html>`/
|
|
22
|
+
* `<head>`/`<body>`, a `<meta charset>`, a `<title>`, and a `<style>` block
|
|
23
|
+
* carrying the shared `doc.css` — the same stylesheet `@markii/react` ships
|
|
24
|
+
* (`packages/platforms/markii-react/src/doc.css`), embedded as a generated
|
|
25
|
+
* string constant (`./doc-css.generated.ts`, produced by
|
|
26
|
+
* `scripts/generate-doc-css.ts`) rather than duplicated by hand, so the two
|
|
27
|
+
* renderers can never drift on document rhythm or component internals.
|
|
28
|
+
*
|
|
29
|
+
* `body` is inserted verbatim: it is expected to already be safe HTML (the
|
|
30
|
+
* output of `renderMarkToHtml`/`renderMarkNodeToHtml`, which sanitizes and
|
|
31
|
+
* escapes as it renders) — this function does not re-escape it, exactly as
|
|
32
|
+
* a `<body>` wrapper around already-rendered markup should not. `title` and
|
|
33
|
+
* `lang`, by contrast, ARE escaped/attribute-safe here, since they are
|
|
34
|
+
* ordinary strings a caller may have sourced from frontmatter or user input.
|
|
35
|
+
*
|
|
36
|
+
* The whole class of components this renders (`.doc`'s wrapper) expects a
|
|
37
|
+
* `<div class="doc">` root — callers that pass `renderMarkToHtml`'s raw
|
|
38
|
+
* output (which does not add that wrapper itself) get it added here, so the
|
|
39
|
+
* exported document's rhythm rules apply without every caller having to
|
|
40
|
+
* remember the wrapper class.
|
|
41
|
+
*/
|
|
42
|
+
export declare function exportHtmlDocument(body: string, options?: ExportHtmlDocumentOptions): string;
|
package/dist/document.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DOC_CSS } from './doc-css.generated.js';
|
|
2
|
+
import { escapeHtml } from './escape.js';
|
|
3
|
+
const DEFAULT_TITLE = 'Markii document';
|
|
4
|
+
const DEFAULT_LANG = 'en';
|
|
5
|
+
/**
|
|
6
|
+
* Wraps an already-rendered document body (typically `renderMarkToHtml`'s
|
|
7
|
+
* output) in a complete, self-contained HTML document: doctype, `<html>`/
|
|
8
|
+
* `<head>`/`<body>`, a `<meta charset>`, a `<title>`, and a `<style>` block
|
|
9
|
+
* carrying the shared `doc.css` — the same stylesheet `@markii/react` ships
|
|
10
|
+
* (`packages/platforms/markii-react/src/doc.css`), embedded as a generated
|
|
11
|
+
* string constant (`./doc-css.generated.ts`, produced by
|
|
12
|
+
* `scripts/generate-doc-css.ts`) rather than duplicated by hand, so the two
|
|
13
|
+
* renderers can never drift on document rhythm or component internals.
|
|
14
|
+
*
|
|
15
|
+
* `body` is inserted verbatim: it is expected to already be safe HTML (the
|
|
16
|
+
* output of `renderMarkToHtml`/`renderMarkNodeToHtml`, which sanitizes and
|
|
17
|
+
* escapes as it renders) — this function does not re-escape it, exactly as
|
|
18
|
+
* a `<body>` wrapper around already-rendered markup should not. `title` and
|
|
19
|
+
* `lang`, by contrast, ARE escaped/attribute-safe here, since they are
|
|
20
|
+
* ordinary strings a caller may have sourced from frontmatter or user input.
|
|
21
|
+
*
|
|
22
|
+
* The whole class of components this renders (`.doc`'s wrapper) expects a
|
|
23
|
+
* `<div class="doc">` root — callers that pass `renderMarkToHtml`'s raw
|
|
24
|
+
* output (which does not add that wrapper itself) get it added here, so the
|
|
25
|
+
* exported document's rhythm rules apply without every caller having to
|
|
26
|
+
* remember the wrapper class.
|
|
27
|
+
*/
|
|
28
|
+
export function exportHtmlDocument(body, options = {}) {
|
|
29
|
+
const title = options.title ?? DEFAULT_TITLE;
|
|
30
|
+
const lang = options.lang ?? DEFAULT_LANG;
|
|
31
|
+
const css = options.extraCss ? `${DOC_CSS}\n${options.extraCss}` : DOC_CSS;
|
|
32
|
+
return (`<!doctype html>\n` +
|
|
33
|
+
`<html lang="${escapeHtml(lang)}">\n` +
|
|
34
|
+
`<head>\n` +
|
|
35
|
+
`<meta charset="utf-8">\n` +
|
|
36
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1">\n` +
|
|
37
|
+
`<title>${escapeHtml(title)}</title>\n` +
|
|
38
|
+
`<style>\n${css}\n</style>\n` +
|
|
39
|
+
`</head>\n` +
|
|
40
|
+
`<body>\n` +
|
|
41
|
+
`<div class="doc">\n${body}\n</div>\n` +
|
|
42
|
+
`</body>\n` +
|
|
43
|
+
`</html>\n`);
|
|
44
|
+
}
|
package/dist/escape.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML-escapes a string for safe insertion into either element text or a
|
|
3
|
+
* quoted attribute value. This is the engine's single escaping primitive:
|
|
4
|
+
* the plain-hast serialization is handled by `hast-util-to-html`, and every
|
|
5
|
+
* string a component or the fallback builds by hand goes through here.
|
|
6
|
+
*/
|
|
7
|
+
export declare function escapeHtml(value: string): string;
|
package/dist/escape.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five characters that must never reach rendered HTML unescaped. `&`,
|
|
3
|
+
* `<`, and `>` matter in text; `"` and `'` matter inside an attribute value.
|
|
4
|
+
* Escaping all five unconditionally means one function is correct in both
|
|
5
|
+
* places, so a component author calling `ctx.esc` never has to know whether
|
|
6
|
+
* a string is about to land in text or in an attribute.
|
|
7
|
+
*/
|
|
8
|
+
const HTML_ESCAPES = {
|
|
9
|
+
'&': '&',
|
|
10
|
+
'<': '<',
|
|
11
|
+
'>': '>',
|
|
12
|
+
'"': '"',
|
|
13
|
+
"'": ''',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* HTML-escapes a string for safe insertion into either element text or a
|
|
17
|
+
* quoted attribute value. This is the engine's single escaping primitive:
|
|
18
|
+
* the plain-hast serialization is handled by `hast-util-to-html`, and every
|
|
19
|
+
* string a component or the fallback builds by hand goes through here.
|
|
20
|
+
*/
|
|
21
|
+
export function escapeHtml(value) {
|
|
22
|
+
return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FailureKind, ValueStatus } from '@markii/runtime';
|
|
2
|
+
/** The short phrase for `kind`, or `undefined` if absent or not one of the four taxonomy members. */
|
|
3
|
+
export declare function failurePhrase(kind: FailureKind | undefined): string | undefined;
|
|
4
|
+
/** The `title` (tooltip) text for a failed/missing binding: the short phrase for `kind`, with `error` appended when there is one. */
|
|
5
|
+
export declare function failureTitle(error: string | undefined, kind: FailureKind | undefined): string | undefined;
|
|
6
|
+
/** The BEM-ish modifier class for `kind` under `base`, or `undefined` when `kind` is absent or out of taxonomy. */
|
|
7
|
+
export declare function failureKindClass(base: string, kind: FailureKind | undefined): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The full class list for a data-bound component's root element: `base`,
|
|
10
|
+
* plus `<base>--stale` for a stale binding, plus `<base>--<failureKind>` when
|
|
11
|
+
* the binding failed with a recognized kind. `extra` (a component's own
|
|
12
|
+
* state modifiers, e.g. `mk-chart--empty`) is kept adjacent to the base.
|
|
13
|
+
*/
|
|
14
|
+
export declare function dataStateClassName(base: string, status: ValueStatus | undefined, kind: FailureKind | undefined, extra?: readonly string[]): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@markii/html` port of `@markii/react`'s `components/failure-
|
|
3
|
+
* presentation.ts` — the ONE place UI wording and CSS-class hooks for
|
|
4
|
+
* `@markii/runtime`'s failure taxonomy live in this engine. Ported (not
|
|
5
|
+
* imported) for the same reason `./resolve.ts` is ported: the module is not
|
|
6
|
+
* part of `@markii/react`'s public export surface, and the two renderers are
|
|
7
|
+
* independent implementations of the same presentation contract
|
|
8
|
+
* (docs/scripting.md), kept identical in wording and class vocabulary so a
|
|
9
|
+
* failing name reads the same in both.
|
|
10
|
+
*
|
|
11
|
+
* The presentation contract (AGENTS.md's cleanliness principle): a failure
|
|
12
|
+
* NEVER becomes body text. It surfaces as exactly two things — a `title`
|
|
13
|
+
* tooltip and a modifier class — layered on the component's quiet empty/
|
|
14
|
+
* stale state.
|
|
15
|
+
*/
|
|
16
|
+
/** Human-facing phrase per `FailureKind`. Null-prototype so an out-of-taxonomy `kind` can never resolve through the prototype chain. */
|
|
17
|
+
const FAILURE_PHRASE = Object.assign(Object.create(null), {
|
|
18
|
+
'script-error': 'script error',
|
|
19
|
+
'capability-denied': 'needs permission',
|
|
20
|
+
'tier-blocked': 'requires manual run',
|
|
21
|
+
limit: 'limit exceeded',
|
|
22
|
+
});
|
|
23
|
+
/** The short phrase for `kind`, or `undefined` if absent or not one of the four taxonomy members. */
|
|
24
|
+
export function failurePhrase(kind) {
|
|
25
|
+
if (kind === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
return Object.hasOwn(FAILURE_PHRASE, kind) ? FAILURE_PHRASE[kind] : undefined;
|
|
28
|
+
}
|
|
29
|
+
/** The `title` (tooltip) text for a failed/missing binding: the short phrase for `kind`, with `error` appended when there is one. */
|
|
30
|
+
export function failureTitle(error, kind) {
|
|
31
|
+
const phrase = failurePhrase(kind);
|
|
32
|
+
if (!phrase)
|
|
33
|
+
return error ? error : undefined;
|
|
34
|
+
return error ? `${phrase}: ${error}` : phrase;
|
|
35
|
+
}
|
|
36
|
+
/** The BEM-ish modifier class for `kind` under `base`, or `undefined` when `kind` is absent or out of taxonomy. */
|
|
37
|
+
export function failureKindClass(base, kind) {
|
|
38
|
+
if (kind === undefined || !failurePhrase(kind))
|
|
39
|
+
return undefined;
|
|
40
|
+
return `${base}--${kind}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The full class list for a data-bound component's root element: `base`,
|
|
44
|
+
* plus `<base>--stale` for a stale binding, plus `<base>--<failureKind>` when
|
|
45
|
+
* the binding failed with a recognized kind. `extra` (a component's own
|
|
46
|
+
* state modifiers, e.g. `mk-chart--empty`) is kept adjacent to the base.
|
|
47
|
+
*/
|
|
48
|
+
export function dataStateClassName(base, status, kind, extra) {
|
|
49
|
+
const classes = [base, ...(extra ?? [])];
|
|
50
|
+
if (status === 'stale')
|
|
51
|
+
classes.push(`${base}--stale`);
|
|
52
|
+
const kindClass = failureKindClass(base, kind);
|
|
53
|
+
if (kindClass)
|
|
54
|
+
classes.push(kindClass);
|
|
55
|
+
return classes.join(' ');
|
|
56
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { renderMarkToHtml, renderMarkNodeToHtml } from './render.js';
|
|
2
|
+
export { escapeHtml } from './escape.js';
|
|
3
|
+
export { exportHtmlDocument, type ExportHtmlDocumentOptions, } from './document.js';
|
|
4
|
+
export { resolveStorePath, resolveScopedPath, VAULT_NAME_PREFIX, type StorePathResolution, type ValueScope, } from './resolve.js';
|
|
5
|
+
export { failurePhrase, failureTitle, failureKindClass, dataStateClassName, } from './failure-presentation.js';
|
|
6
|
+
export { stringifyStoredValue } from './value-format.js';
|
|
7
|
+
export { createHtmlRegistry, mergeHtmlRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, REGISTRY_ALIASES, type DirectiveAttributes, type HtmlRegistry, type HtmlRegistryEntry, type HtmlComponent, type HtmlRenderContext, type RegistryAlias, type RegistryAliases, type ResolvedDirective, type ValueResolution, } from './registry.js';
|
|
8
|
+
export { resolveLayoutAttributes, LAYOUT_ATTRIBUTE_KEYS, type ResolvedLayoutAttributes, } from './layout.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @markii/html: a framework-free static HTML renderer for Markii documents.
|
|
2
|
+
// It consumes @markii/core's sanitized hast and emits an HTML string, so a
|
|
3
|
+
// stopped-changing document can be rendered for publishing, CI, email, or an
|
|
4
|
+
// archive with no React runtime. It is one platform renderer among possible
|
|
5
|
+
// many; the React renderer (@markii/react) is another consumer of the same core.
|
|
6
|
+
export { renderMarkToHtml, renderMarkNodeToHtml } from './render.js';
|
|
7
|
+
export { escapeHtml } from './escape.js';
|
|
8
|
+
export { exportHtmlDocument, } from './document.js';
|
|
9
|
+
export { resolveStorePath, resolveScopedPath, VAULT_NAME_PREFIX, } from './resolve.js';
|
|
10
|
+
export { failurePhrase, failureTitle, failureKindClass, dataStateClassName, } from './failure-presentation.js';
|
|
11
|
+
export { stringifyStoredValue } from './value-format.js';
|
|
12
|
+
export { createHtmlRegistry, mergeHtmlRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, REGISTRY_ALIASES, } from './registry.js';
|
|
13
|
+
export { resolveLayoutAttributes, LAYOUT_ATTRIBUTE_KEYS, } from './layout.js';
|