@nanisoft/prism-ui 0.15.0 → 0.16.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/.tsbuildinfo +1 -1
- package/dist/blocks/data-table-01/data-table.d.ts +10 -0
- package/dist/blocks/data-table-01/data-table.d.ts.map +1 -1
- package/dist/blocks/data-table-01/data-table.js +2 -1
- package/dist/blocks/page-header-01/page-header.d.ts +7 -0
- package/dist/blocks/page-header-01/page-header.d.ts.map +1 -1
- package/dist/blocks/page-header-01/page-header.js +9 -1
- package/dist/blocks/run-console-01/run-console.d.ts.map +1 -1
- package/dist/blocks/run-console-01/run-console.js +2 -1
- package/dist/blocks/site-footer/site-footer.d.ts +16 -0
- package/dist/blocks/site-footer/site-footer.d.ts.map +1 -1
- package/dist/blocks/site-footer/site-footer.js +17 -1
- package/dist/blocks/stack-grid-01/stack-grid.d.ts +13 -0
- package/dist/blocks/stack-grid-01/stack-grid.d.ts.map +1 -1
- package/dist/blocks/stack-grid-01/stack-grid.js +14 -1
- package/dist/components/ui/diagram.d.ts +38 -7
- package/dist/components/ui/diagram.d.ts.map +1 -1
- package/dist/components/ui/diagram.js +134 -52
- package/dist/components/ui/fact-list.d.ts +17 -0
- package/dist/components/ui/fact-list.d.ts.map +1 -1
- package/dist/components/ui/fact-list.js +18 -1
- package/dist/components/ui/prose.d.ts +11 -0
- package/dist/components/ui/prose.d.ts.map +1 -1
- package/dist/components/ui/prose.js +12 -1
- package/dist/components/ui/pulse-graph.d.ts +36 -4
- package/dist/components/ui/pulse-graph.d.ts.map +1 -1
- package/dist/components/ui/pulse-graph.js +174 -60
- package/dist/components/ui/section.d.ts +33 -31
- package/dist/components/ui/section.d.ts.map +1 -1
- package/dist/components/ui/section.js +33 -31
- package/dist/lib/figure.d.ts +149 -0
- package/dist/lib/figure.d.ts.map +1 -0
- package/dist/lib/figure.js +242 -0
- package/dist/live/run-stream-01/run-stream.d.ts.map +1 -1
- package/dist/live/run-stream-01/run-stream.js +2 -1
- package/dist/pages/blog-post-page/index.d.ts +36 -15
- package/dist/pages/blog-post-page/index.d.ts.map +1 -1
- package/dist/pages/blog-post-page/index.js +35 -10
- package/dist/pages/docs-shell/docs-shell.d.ts +120 -18
- package/dist/pages/docs-shell/docs-shell.d.ts.map +1 -1
- package/dist/pages/docs-shell/docs-shell.js +208 -24
- package/dist/pages/error-page/index.d.ts.map +1 -1
- package/dist/pages/error-page/index.js +7 -3
- package/dist/pages/not-found-page/index.d.ts.map +1 -1
- package/dist/pages/not-found-page/index.js +4 -3
- package/dist/styles.css +107 -12
- package/package.json +2 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arithmetic every Component that draws a figure shares.
|
|
3
|
+
*
|
|
4
|
+
* **One copy rather than one per Component.** Two Components in this package draw
|
|
5
|
+
* labelled marks into a fixed canvas and both have to answer the same three
|
|
6
|
+
* questions: where a label's ink box is, where a stroke leaving a mark has to stop
|
|
7
|
+
* before it enters one, and how a caller's own coordinate space is fitted into the
|
|
8
|
+
* canvas. Two copies of that
|
|
9
|
+
* arithmetic are two answers to one question, and the disagreement between them is
|
|
10
|
+
* invisible until a reader puts the two figures side by side and finds that one of
|
|
11
|
+
* them runs a line through a word. What stays with each Component is the half that
|
|
12
|
+
* is genuinely its own: the numbers its own drawing uses and the labels it prints.
|
|
13
|
+
*
|
|
14
|
+
* **How any of this is reachable from a server Component.** A Component that cannot
|
|
15
|
+
* measure text still knows a monospaced label's width, because a monospaced face has
|
|
16
|
+
* one advance width for every character and so a label's width is its character
|
|
17
|
+
* count. That is a property of the face every label in a figure is set in rather
|
|
18
|
+
* than of this module, and it is what makes a label's box arithmetic instead of a
|
|
19
|
+
* measurement. A label set in a proportional face would have no such answer, which
|
|
20
|
+
* is a limit of the rule rather than of the Component.
|
|
21
|
+
*
|
|
22
|
+
* **Two rules these serve, and the second one has a direction clause.** The
|
|
23
|
+
* legibility floor in DESIGN.md, and the ink-avoidance rule beside it: a stroke
|
|
24
|
+
* takes the longest stretch of the line between two marks that no label blocks, so a
|
|
25
|
+
* label a stroke does not run through costs it nothing. Nothing here draws and
|
|
26
|
+
* nothing here knows a colour, a token or a class name. A geometry helper that
|
|
27
|
+
* also held an ink would be holding a token, which is the one thing a figure in
|
|
28
|
+
* this system never does.
|
|
29
|
+
*/
|
|
30
|
+
/** A point in a figure's own coordinate space. */
|
|
31
|
+
export type FigurePoint = {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
};
|
|
35
|
+
/** An axis-aligned rectangle in a figure's own coordinate space. */
|
|
36
|
+
export type FigureBox = {
|
|
37
|
+
l: number;
|
|
38
|
+
r: number;
|
|
39
|
+
t: number;
|
|
40
|
+
b: number;
|
|
41
|
+
};
|
|
42
|
+
/** The two ends of a drawn stroke. */
|
|
43
|
+
export type FigureStroke = {
|
|
44
|
+
from: FigurePoint;
|
|
45
|
+
to: FigurePoint;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* One label a Component prints beside a mark, in the three numbers the arithmetic
|
|
49
|
+
* below reads it by.
|
|
50
|
+
*
|
|
51
|
+
* A label is passed as its own text rather than as a measured box because there is
|
|
52
|
+
* no measurement to pass: the text is the measurement, and the Component that owns
|
|
53
|
+
* the label is the only one that knows its size and its baseline.
|
|
54
|
+
*/
|
|
55
|
+
export type FigureLabel = {
|
|
56
|
+
/** The label's own text, whose character count is its width. */
|
|
57
|
+
text: string;
|
|
58
|
+
/** Its type size, in user units. */
|
|
59
|
+
size: number;
|
|
60
|
+
/** Its baseline, measured down from the mark's centre. */
|
|
61
|
+
offset: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The canvas a caller's points are fitted into.
|
|
65
|
+
*
|
|
66
|
+
* `bottom` is separate from `height` because a figure that draws something along its
|
|
67
|
+
* foot reserves a band there, and the fitted area stops above that band rather than
|
|
68
|
+
* the canvas growing to fit both.
|
|
69
|
+
*/
|
|
70
|
+
export type FigureFrame = {
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
/** The band left empty on the top and on the two sides. */
|
|
74
|
+
padding: number;
|
|
75
|
+
/** The fitted area's lower edge, which defaults to `height - padding`. */
|
|
76
|
+
bottom?: number;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The metrics of the face every label in a figure is set in, as fractions of the
|
|
80
|
+
* type size, and the gap the drawing keeps between one piece of ink and the next.
|
|
81
|
+
*
|
|
82
|
+
* A monospaced face has one advance width for every character, so `MONO_ADVANCE` is
|
|
83
|
+
* what turns a label's character count into a width, and `LABEL_ASCENT` and
|
|
84
|
+
* `LABEL_DESCENT` are what turn a baseline into a box. The three are the face's own
|
|
85
|
+
* rather than a decision about any one drawing, and they are a little generous on
|
|
86
|
+
* purpose: a box wider or deeper than the ink it stands for costs a little length on
|
|
87
|
+
* a stroke, while a box narrower or shallower costs a word a reader has to guess at.
|
|
88
|
+
*
|
|
89
|
+
* `INK_CLEARANCE` is the space the box adds around that ink, so it is both the gap
|
|
90
|
+
* between a stroke and the label it would otherwise run through and the margin the
|
|
91
|
+
* box keeps around the mark's own centre line. One constant rather than two because
|
|
92
|
+
* it is one fact: no two pieces of ink in a figure touch.
|
|
93
|
+
*/
|
|
94
|
+
export declare const MONO_ADVANCE = 0.6;
|
|
95
|
+
export declare const LABEL_ASCENT = 0.8;
|
|
96
|
+
export declare const LABEL_DESCENT = 0.25;
|
|
97
|
+
export declare const INK_CLEARANCE = 7;
|
|
98
|
+
/** Half the width of a monospaced label of `size` user units, in user units. */
|
|
99
|
+
export declare function labelHalfWidth(label: string, size: number): number;
|
|
100
|
+
/** Half the height of a label of `size` user units, in user units. */
|
|
101
|
+
export declare function labelHalfHeight(size: number): number;
|
|
102
|
+
/**
|
|
103
|
+
* The visible part of a stroke between two marks: the longest stretch of the line
|
|
104
|
+
* between them that no label blocks, held off each mark by the mark's own clearance.
|
|
105
|
+
*
|
|
106
|
+
* **Trim a segment against what it would actually enter, and nothing else.** A label
|
|
107
|
+
* is an obstacle only where the stroke runs, so each label is its own box and a
|
|
108
|
+
* stroke that misses it pays nothing for it. A name is printed under its mark and a
|
|
109
|
+
* note under that, so both sit below it, and a stroke leaving sideways runs along
|
|
110
|
+
* the mark's own centre line and enters neither. Backing a stroke off the mark by
|
|
111
|
+
* the half-width of the widest label at the node anyway pushed every connector on a
|
|
112
|
+
* six-stage rail back by up to fifty units from a six-unit mark: a row of circles
|
|
113
|
+
* with short line segments floating between them, which is a drawing of a shape and
|
|
114
|
+
* none of its relations. So the whole rail reaches both of its marks and nothing is
|
|
115
|
+
* trimmed there at all.
|
|
116
|
+
*
|
|
117
|
+
* **The stretch is the one the whole stroke fits in, and not two trims taken one at a
|
|
118
|
+
* time, because a label can block the corridor behind a mark rather than the mark
|
|
119
|
+
* itself.** A node's name and note sit below it at two different depths, so a
|
|
120
|
+
* stroke arriving from below runs through the name, out of it, and then back into the
|
|
121
|
+
* note further along: stopping where the name ends leaves the stroke sitting on top
|
|
122
|
+
* of the note. The drawn stroke is what has to be clear, so the whole line between
|
|
123
|
+
* the two marks is divided once and the longest unblocked stretch of it is drawn.
|
|
124
|
+
*
|
|
125
|
+
* **A mark's own edge plus the clearance is where an end sits when nothing blocks
|
|
126
|
+
* it.** A mark is a circle, so its edge is `mark` away in every direction, and the
|
|
127
|
+
* clearance is the gap that holds a stroke off the ink it belongs to. A line with no
|
|
128
|
+
* label on it therefore starts one clearance from the first mark and ends one
|
|
129
|
+
* clearance from the second, and that pair is the whole of what a stroke leaving a
|
|
130
|
+
* node sideways is allowed to cost.
|
|
131
|
+
*
|
|
132
|
+
* Two blocked stretches can leave nothing between them when a caller puts two marks
|
|
133
|
+
* close together, and a line trimmed away entirely is a line that silently did not
|
|
134
|
+
* draw, which is the one failure a figure in this package exists to make impossible.
|
|
135
|
+
* The mark boundaries are then the shortest stroke that still joins the two marks,
|
|
136
|
+
* and a caller who has put two marks on top of one another has already been told what
|
|
137
|
+
* that draws.
|
|
138
|
+
*/
|
|
139
|
+
export declare function strokeBetween(from: FigurePoint, to: FigurePoint, mark: number, fromLabels: readonly FigureLabel[], toLabels: readonly FigureLabel[]): FigureStroke;
|
|
140
|
+
/**
|
|
141
|
+
* The caller's points, fitted into the frame with their aspect ratio kept and the
|
|
142
|
+
* leftover shared out evenly.
|
|
143
|
+
*
|
|
144
|
+
* A caller whose points span one axis only, or who passes one item, gets the other
|
|
145
|
+
* axis centred rather than a division by zero: the scale is taken over the axes that
|
|
146
|
+
* have an extent, and a figure with no extent at all keeps the caller's own numbers.
|
|
147
|
+
*/
|
|
148
|
+
export declare function fitPoints<T extends FigurePoint>(items: readonly T[], frame: FigureFrame): (item: T) => FigurePoint;
|
|
149
|
+
//# sourceMappingURL=figure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure.d.ts","sourceRoot":"","sources":["../../src/lib/figure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,kDAAkD;AAClD,MAAM,MAAM,WAAW,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAElD,oEAAoE;AACpE,MAAM,MAAM,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtE,sCAAsC;AACtC,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAA;AAKjE;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,MAAM,CAAA;AAC/B,eAAO,MAAM,YAAY,MAAM,CAAA;AAC/B,eAAO,MAAM,aAAa,OAAO,CAAA;AACjC,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAElE;AAED,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAiGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,WAAW,EACjB,EAAE,EAAE,WAAW,EACf,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,WAAW,EAAE,EAClC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAC/B,YAAY,CAgCd;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,WAAW,EAC7C,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,KAAK,EAAE,WAAW,GACjB,CAAC,IAAI,EAAE,CAAC,KAAK,WAAW,CAyB1B"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arithmetic every Component that draws a figure shares.
|
|
3
|
+
*
|
|
4
|
+
* **One copy rather than one per Component.** Two Components in this package draw
|
|
5
|
+
* labelled marks into a fixed canvas and both have to answer the same three
|
|
6
|
+
* questions: where a label's ink box is, where a stroke leaving a mark has to stop
|
|
7
|
+
* before it enters one, and how a caller's own coordinate space is fitted into the
|
|
8
|
+
* canvas. Two copies of that
|
|
9
|
+
* arithmetic are two answers to one question, and the disagreement between them is
|
|
10
|
+
* invisible until a reader puts the two figures side by side and finds that one of
|
|
11
|
+
* them runs a line through a word. What stays with each Component is the half that
|
|
12
|
+
* is genuinely its own: the numbers its own drawing uses and the labels it prints.
|
|
13
|
+
*
|
|
14
|
+
* **How any of this is reachable from a server Component.** A Component that cannot
|
|
15
|
+
* measure text still knows a monospaced label's width, because a monospaced face has
|
|
16
|
+
* one advance width for every character and so a label's width is its character
|
|
17
|
+
* count. That is a property of the face every label in a figure is set in rather
|
|
18
|
+
* than of this module, and it is what makes a label's box arithmetic instead of a
|
|
19
|
+
* measurement. A label set in a proportional face would have no such answer, which
|
|
20
|
+
* is a limit of the rule rather than of the Component.
|
|
21
|
+
*
|
|
22
|
+
* **Two rules these serve, and the second one has a direction clause.** The
|
|
23
|
+
* legibility floor in DESIGN.md, and the ink-avoidance rule beside it: a stroke
|
|
24
|
+
* takes the longest stretch of the line between two marks that no label blocks, so a
|
|
25
|
+
* label a stroke does not run through costs it nothing. Nothing here draws and
|
|
26
|
+
* nothing here knows a colour, a token or a class name. A geometry helper that
|
|
27
|
+
* also held an ink would be holding a token, which is the one thing a figure in
|
|
28
|
+
* this system never does.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The metrics of the face every label in a figure is set in, as fractions of the
|
|
32
|
+
* type size, and the gap the drawing keeps between one piece of ink and the next.
|
|
33
|
+
*
|
|
34
|
+
* A monospaced face has one advance width for every character, so `MONO_ADVANCE` is
|
|
35
|
+
* what turns a label's character count into a width, and `LABEL_ASCENT` and
|
|
36
|
+
* `LABEL_DESCENT` are what turn a baseline into a box. The three are the face's own
|
|
37
|
+
* rather than a decision about any one drawing, and they are a little generous on
|
|
38
|
+
* purpose: a box wider or deeper than the ink it stands for costs a little length on
|
|
39
|
+
* a stroke, while a box narrower or shallower costs a word a reader has to guess at.
|
|
40
|
+
*
|
|
41
|
+
* `INK_CLEARANCE` is the space the box adds around that ink, so it is both the gap
|
|
42
|
+
* between a stroke and the label it would otherwise run through and the margin the
|
|
43
|
+
* box keeps around the mark's own centre line. One constant rather than two because
|
|
44
|
+
* it is one fact: no two pieces of ink in a figure touch.
|
|
45
|
+
*/
|
|
46
|
+
export const MONO_ADVANCE = 0.6;
|
|
47
|
+
export const LABEL_ASCENT = 0.8;
|
|
48
|
+
export const LABEL_DESCENT = 0.25;
|
|
49
|
+
export const INK_CLEARANCE = 7;
|
|
50
|
+
/** Half the width of a monospaced label of `size` user units, in user units. */
|
|
51
|
+
export function labelHalfWidth(label, size) {
|
|
52
|
+
return (label.length * MONO_ADVANCE * size) / 2;
|
|
53
|
+
}
|
|
54
|
+
/** Half the height of a label of `size` user units, in user units. */
|
|
55
|
+
export function labelHalfHeight(size) {
|
|
56
|
+
return ((LABEL_ASCENT + LABEL_DESCENT) * size) / 2;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The box one label's ink occupies, with the clearance around it.
|
|
60
|
+
*
|
|
61
|
+
* One label rather than a node's whole set of them, and that is the whole of the
|
|
62
|
+
* trim rule: a stroke is stopped by the labels it would actually enter, so the
|
|
63
|
+
* clearance around one label has to be findable on its own rather than only as
|
|
64
|
+
* part of a rectangle enclosing all of them.
|
|
65
|
+
*/
|
|
66
|
+
function labelBox(at, label) {
|
|
67
|
+
const half = labelHalfWidth(label.text, label.size);
|
|
68
|
+
return {
|
|
69
|
+
l: at.x - half - INK_CLEARANCE,
|
|
70
|
+
r: at.x + half + INK_CLEARANCE,
|
|
71
|
+
t: at.y + label.offset - LABEL_ASCENT * label.size - INK_CLEARANCE,
|
|
72
|
+
b: at.y + label.offset + LABEL_DESCENT * label.size + INK_CLEARANCE,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The stretch of a ray that is inside one box, measured as a distance along the ray
|
|
77
|
+
* from where it starts, or `null` for a ray that misses it.
|
|
78
|
+
*
|
|
79
|
+
* The two axes are taken separately and intersected, so a ray running exactly along
|
|
80
|
+
* an axis is not a division by zero and a ray that clips a corner enters and leaves
|
|
81
|
+
* in the same place rather than entering at all.
|
|
82
|
+
*/
|
|
83
|
+
function raySpan(from, toward, box) {
|
|
84
|
+
const length = Math.hypot(toward.x - from.x, toward.y - from.y);
|
|
85
|
+
if (length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
const ux = (toward.x - from.x) / length;
|
|
88
|
+
const uy = (toward.y - from.y) / length;
|
|
89
|
+
let enter = 0;
|
|
90
|
+
let leave = Number.POSITIVE_INFINITY;
|
|
91
|
+
for (const [origin, delta, low, high] of [
|
|
92
|
+
[from.x, ux, box.l, box.r],
|
|
93
|
+
[from.y, uy, box.t, box.b],
|
|
94
|
+
]) {
|
|
95
|
+
if (delta === 0) {
|
|
96
|
+
if (origin < low || origin > high)
|
|
97
|
+
return null;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const near = (low - origin) / delta;
|
|
101
|
+
const far = (high - origin) / delta;
|
|
102
|
+
enter = Math.max(enter, Math.min(near, far));
|
|
103
|
+
leave = Math.min(leave, Math.max(near, far));
|
|
104
|
+
if (enter > leave)
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return { enter, leave };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The stretches of the line between two marks that no label's clearance blocks, in
|
|
111
|
+
* order along it and with the two marks themselves as its ends.
|
|
112
|
+
*
|
|
113
|
+
* Two of the boxes a caller hands over can block the same stretch, and one box can
|
|
114
|
+
* block two, so the stretches are merged before they are compared: the answer is a
|
|
115
|
+
* set of gaps rather than a set of obstacles, and a gap that counted twice would be
|
|
116
|
+
* a gap that won twice.
|
|
117
|
+
*/
|
|
118
|
+
function freeStretches(from, to, boxes) {
|
|
119
|
+
const length = Math.hypot(to.x - from.x, to.y - from.y);
|
|
120
|
+
const blocked = boxes
|
|
121
|
+
.map((box) => raySpan(from, to, box))
|
|
122
|
+
.filter((span) => span !== null)
|
|
123
|
+
.map((span) => ({
|
|
124
|
+
enter: Math.max(0, span.enter),
|
|
125
|
+
leave: Math.min(length, span.leave),
|
|
126
|
+
}))
|
|
127
|
+
.filter((span) => span.leave > span.enter)
|
|
128
|
+
.sort((a, b) => a.enter - b.enter);
|
|
129
|
+
const merged = [];
|
|
130
|
+
for (const span of blocked) {
|
|
131
|
+
const open = merged[merged.length - 1];
|
|
132
|
+
if (open && span.enter <= open.leave) {
|
|
133
|
+
open.leave = Math.max(open.leave, span.leave);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
merged.push({ ...span });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const gaps = [];
|
|
140
|
+
let walked = 0;
|
|
141
|
+
for (const span of merged) {
|
|
142
|
+
if (span.enter > walked)
|
|
143
|
+
gaps.push({ enter: walked, leave: span.enter });
|
|
144
|
+
walked = span.leave;
|
|
145
|
+
}
|
|
146
|
+
gaps.push({ enter: walked, leave: length });
|
|
147
|
+
return gaps;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The visible part of a stroke between two marks: the longest stretch of the line
|
|
151
|
+
* between them that no label blocks, held off each mark by the mark's own clearance.
|
|
152
|
+
*
|
|
153
|
+
* **Trim a segment against what it would actually enter, and nothing else.** A label
|
|
154
|
+
* is an obstacle only where the stroke runs, so each label is its own box and a
|
|
155
|
+
* stroke that misses it pays nothing for it. A name is printed under its mark and a
|
|
156
|
+
* note under that, so both sit below it, and a stroke leaving sideways runs along
|
|
157
|
+
* the mark's own centre line and enters neither. Backing a stroke off the mark by
|
|
158
|
+
* the half-width of the widest label at the node anyway pushed every connector on a
|
|
159
|
+
* six-stage rail back by up to fifty units from a six-unit mark: a row of circles
|
|
160
|
+
* with short line segments floating between them, which is a drawing of a shape and
|
|
161
|
+
* none of its relations. So the whole rail reaches both of its marks and nothing is
|
|
162
|
+
* trimmed there at all.
|
|
163
|
+
*
|
|
164
|
+
* **The stretch is the one the whole stroke fits in, and not two trims taken one at a
|
|
165
|
+
* time, because a label can block the corridor behind a mark rather than the mark
|
|
166
|
+
* itself.** A node's name and note sit below it at two different depths, so a
|
|
167
|
+
* stroke arriving from below runs through the name, out of it, and then back into the
|
|
168
|
+
* note further along: stopping where the name ends leaves the stroke sitting on top
|
|
169
|
+
* of the note. The drawn stroke is what has to be clear, so the whole line between
|
|
170
|
+
* the two marks is divided once and the longest unblocked stretch of it is drawn.
|
|
171
|
+
*
|
|
172
|
+
* **A mark's own edge plus the clearance is where an end sits when nothing blocks
|
|
173
|
+
* it.** A mark is a circle, so its edge is `mark` away in every direction, and the
|
|
174
|
+
* clearance is the gap that holds a stroke off the ink it belongs to. A line with no
|
|
175
|
+
* label on it therefore starts one clearance from the first mark and ends one
|
|
176
|
+
* clearance from the second, and that pair is the whole of what a stroke leaving a
|
|
177
|
+
* node sideways is allowed to cost.
|
|
178
|
+
*
|
|
179
|
+
* Two blocked stretches can leave nothing between them when a caller puts two marks
|
|
180
|
+
* close together, and a line trimmed away entirely is a line that silently did not
|
|
181
|
+
* draw, which is the one failure a figure in this package exists to make impossible.
|
|
182
|
+
* The mark boundaries are then the shortest stroke that still joins the two marks,
|
|
183
|
+
* and a caller who has put two marks on top of one another has already been told what
|
|
184
|
+
* that draws.
|
|
185
|
+
*/
|
|
186
|
+
export function strokeBetween(from, to, mark, fromLabels, toLabels) {
|
|
187
|
+
const dx = to.x - from.x;
|
|
188
|
+
const dy = to.y - from.y;
|
|
189
|
+
const length = Math.hypot(dx, dy);
|
|
190
|
+
if (length === 0)
|
|
191
|
+
return { from, to };
|
|
192
|
+
const ux = dx / length;
|
|
193
|
+
const uy = dy / length;
|
|
194
|
+
// Both nodes' labels, in one list, because the corridor between the marks is one
|
|
195
|
+
// corridor and a label of either end can be what blocks it. Each label is anchored
|
|
196
|
+
// to the mark it is printed under, which for one of them is not where this line
|
|
197
|
+
// starts.
|
|
198
|
+
const gaps = freeStretches(from, to, [
|
|
199
|
+
...fromLabels.map((label) => labelBox(from, label)),
|
|
200
|
+
...toLabels.map((label) => labelBox(to, label)),
|
|
201
|
+
]);
|
|
202
|
+
const room = gaps.reduce((best, gap) => gap.leave - gap.enter > best.leave - best.enter ? gap : best);
|
|
203
|
+
const head = Math.max(room.enter, mark + INK_CLEARANCE);
|
|
204
|
+
const tail = Math.min(room.leave, length - mark - INK_CLEARANCE);
|
|
205
|
+
if (tail > head) {
|
|
206
|
+
return {
|
|
207
|
+
from: { x: from.x + ux * head, y: from.y + uy * head },
|
|
208
|
+
to: { x: from.x + ux * tail, y: from.y + uy * tail },
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
from: { x: from.x + ux * mark, y: from.y + uy * mark },
|
|
213
|
+
to: { x: to.x - ux * mark, y: to.y - uy * mark },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The caller's points, fitted into the frame with their aspect ratio kept and the
|
|
218
|
+
* leftover shared out evenly.
|
|
219
|
+
*
|
|
220
|
+
* A caller whose points span one axis only, or who passes one item, gets the other
|
|
221
|
+
* axis centred rather than a division by zero: the scale is taken over the axes that
|
|
222
|
+
* have an extent, and a figure with no extent at all keeps the caller's own numbers.
|
|
223
|
+
*/
|
|
224
|
+
export function fitPoints(items, frame) {
|
|
225
|
+
const bottom = frame.bottom ?? frame.height - frame.padding;
|
|
226
|
+
if (items.length === 0) {
|
|
227
|
+
return () => ({ x: frame.width / 2, y: (frame.padding + bottom) / 2 });
|
|
228
|
+
}
|
|
229
|
+
const xs = items.map((item) => item.x);
|
|
230
|
+
const ys = items.map((item) => item.y);
|
|
231
|
+
const minX = Math.min(...xs);
|
|
232
|
+
const minY = Math.min(...ys);
|
|
233
|
+
const spanX = Math.max(...xs) - minX;
|
|
234
|
+
const spanY = Math.max(...ys) - minY;
|
|
235
|
+
const roomX = frame.width - frame.padding * 2;
|
|
236
|
+
const roomY = bottom - frame.padding;
|
|
237
|
+
const scale = Math.min(spanX === 0 ? Number.POSITIVE_INFINITY : roomX / spanX, spanY === 0 ? Number.POSITIVE_INFINITY : roomY / spanY);
|
|
238
|
+
const unit = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
239
|
+
const offsetX = frame.padding + (roomX - spanX * unit) / 2 - minX * unit;
|
|
240
|
+
const offsetY = frame.padding + (roomY - spanY * unit) / 2 - minY * unit;
|
|
241
|
+
return (item) => ({ x: item.x * unit + offsetX, y: item.y * unit + offsetY });
|
|
242
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-stream.d.ts","sourceRoot":"","sources":["../../../src/live/run-stream-01/run-stream.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run-stream.d.ts","sourceRoot":"","sources":["../../../src/live/run-stream-01/run-stream.tsx"],"names":[],"mappings":"AASA;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEhE,6CAA6C;AAC7C,MAAM,MAAM,QAAQ,GAAG;IACrB,2FAA2F;IAC3F,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,YAAY,CAAA;IAClB,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAA;IACV,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,6FAA6F;AAC7F,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE5E;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,KAAK,MAAM,IAAI,CAAA;AAE1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,SAAS,EAAE,YAAY,CAAA;IACvB;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAChC;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAA;IAC7B,gCAAgC;IAChC,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;OAOG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,cAAc,EACd,OAAO,EACP,MAAM,EACN,WAAW,EACX,OAAO,EACP,KAAK,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,SAAS,EACT,KAAW,EACX,SAAS,GACV,EAAE,gBAAgB,+BA0IlB;AAED,eAAe,WAAW,CAAA"}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { LiveRegion } from '../../components/ui/live-region.js';
|
|
5
5
|
import { ScrollArea } from '../../components/ui/scroll-area.js';
|
|
6
|
+
import { headingSizeClass } from '../../components/ui/section.js';
|
|
6
7
|
import { cn } from '../../lib/utils.js';
|
|
7
8
|
/** The ink each role is stated in, and the tick beside it. */
|
|
8
9
|
const ROLE_INK = {
|
|
@@ -106,6 +107,6 @@ export function RunStream01({ subscribe, resubscribeKey, initial, status, status
|
|
|
106
107
|
stop();
|
|
107
108
|
};
|
|
108
109
|
}, [subscribe, resubscribeKey, limit]);
|
|
109
|
-
return (_jsxs("section", { "data-slot": "run-stream", className: cn('border-border flex flex-col gap-4 rounded-lg border p-4', className), children: [LiveRegion({ politeness: 'polite', label: liveLabel, children: newest }), title !== undefined || eyebrow !== undefined || description !== undefined ? (_jsxs("header", { className: "flex flex-col gap-1", children: [eyebrow ? (_jsx("span", { className: "text-muted-foreground text-xs font-medium tracking-wide uppercase", children: eyebrow })) : null, title ? _jsx("h2", { className:
|
|
110
|
+
return (_jsxs("section", { "data-slot": "run-stream", className: cn('border-border flex flex-col gap-4 rounded-lg border p-4', className), children: [LiveRegion({ politeness: 'polite', label: liveLabel, children: newest }), title !== undefined || eyebrow !== undefined || description !== undefined ? (_jsxs("header", { className: "flex flex-col gap-1", children: [eyebrow ? (_jsx("span", { className: "text-muted-foreground text-xs font-medium tracking-wide uppercase", children: eyebrow })) : null, title ? _jsx("h2", { className: cn('font-semibold tracking-tight text-balance', headingSizeClass('h2')), children: title }) : null, description ? (_jsx("p", { className: "text-muted-foreground text-pretty text-sm", children: description })) : null] })) : null, status !== undefined ? (_jsx("p", { "data-slot": "run-status", className: cn('font-mono text-xs', STATUS_INK[status]), children: statusLabel })) : null, events.length === 0 ? (_jsx("p", { "data-slot": "run-stream-empty", className: "text-muted-foreground text-sm", children: emptyLabel })) : (_jsx(ScrollArea, { label: eventsLabel, className: "max-h-80", children: _jsx("ol", { "data-slot": "run-events", className: "flex flex-col gap-2", children: events.map((event) => (_jsxs("li", { "data-slot": "run-event", "data-role": event.role, className: "grid grid-cols-[auto_auto_1fr] items-baseline gap-x-3 gap-y-0.5", children: [_jsx("span", { "aria-hidden": "true", className: cn('font-mono text-xs', ROLE_INK[event.role]), children: ROLE_MARK[event.role] }), _jsx("time", { dateTime: new Date(event.at).toISOString(), className: "text-muted-foreground font-mono text-xs tabular-nums", children: new Date(event.at).toISOString().slice(11, 19) }), _jsxs("span", { className: cn('text-pretty text-sm', ROLE_INK[event.role]), children: [event.message, event.detail ? (_jsx("span", { className: "text-muted-foreground block font-mono text-xs", children: event.detail })) : null] })] }, event.id))) }) }))] }));
|
|
110
111
|
}
|
|
111
112
|
export default RunStream01;
|
|
@@ -61,19 +61,26 @@ export type BlogPostPageProps = {
|
|
|
61
61
|
*/
|
|
62
62
|
description?: string;
|
|
63
63
|
/**
|
|
64
|
-
* The publication date
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
64
|
+
* The publication date as a reader reads it: already formatted, in words, in
|
|
65
|
+
* whatever convention the publishing site writes dates in. The Page renders this
|
|
66
|
+
* string verbatim and never touches it, and **it is the caller's to format**.
|
|
67
|
+
*
|
|
68
|
+
* It is a separate prop from `dateTime` precisely so the two can differ, and
|
|
69
|
+
* passing one value to both is refused rather than rendered: see the component
|
|
70
|
+
* JSDoc below for why the Page will not show a reader a machine date.
|
|
68
71
|
*/
|
|
69
72
|
date: string;
|
|
70
73
|
/**
|
|
71
74
|
* The date in a machine-readable form, `YYYY-MM-DD`, carried on the `time`
|
|
72
75
|
* element. Required, and deliberately not derived from `date`: parsing a
|
|
73
|
-
* formatted date back into an ISO one is a guess about a locale, and a guess
|
|
74
|
-
*
|
|
76
|
+
* formatted date back into an ISO one is a guess about a locale, and a guess in
|
|
77
|
+
* a `datetime` attribute is a wrong date in a feed.
|
|
78
|
+
*
|
|
79
|
+
* Typed as the shape it is rather than as `string`, so a formatted string
|
|
80
|
+
* passed here is a compile error instead of a `datetime` attribute no reader can
|
|
81
|
+
* check.
|
|
75
82
|
*/
|
|
76
|
-
dateTime:
|
|
83
|
+
dateTime: `${number}-${number}-${number}`;
|
|
77
84
|
/** Who wrote the post. A name, or a link to an author page. */
|
|
78
85
|
author?: ReactNode;
|
|
79
86
|
/**
|
|
@@ -145,14 +152,28 @@ export type BlogPostPageProps = {
|
|
|
145
152
|
* its frontmatter to this Page. The Page is what makes the frontmatter a
|
|
146
153
|
* contract, because a field that is not a prop is a field nothing checks.
|
|
147
154
|
*
|
|
148
|
-
* **The date is two props
|
|
149
|
-
* the
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* search result, and neither reader can see
|
|
153
|
-
* the
|
|
154
|
-
*
|
|
155
|
-
*
|
|
155
|
+
* **The date is two props, and the Page will not collapse them into one.** `date`
|
|
156
|
+
* is the words a reader sees and `dateTime` is the machine value on the `time`
|
|
157
|
+
* element, and they are kept apart because parsing a formatted date back into an
|
|
158
|
+
* ISO one is a guess about a locale. A wrong date in a `datetime` attribute is a
|
|
159
|
+
* wrong date in a feed reader and in a search result, and neither reader can see
|
|
160
|
+
* the string that was wrong.
|
|
161
|
+
*
|
|
162
|
+
* The other half of that is the one this Page now refuses. A Page that renders a
|
|
163
|
+
* date has no business making a reader parse `2026-09-26`, and the Page is the
|
|
164
|
+
* only place in a site where that sentence can be enforced rather than requested,
|
|
165
|
+
* because a consumer's own pipeline is where the date is a string and Prism is
|
|
166
|
+
* where it becomes a page. So `date` is the caller's to format, and handing this
|
|
167
|
+
* Page the same string in both slots is refused at render rather than shipped:
|
|
168
|
+
* all four NaniSoft sites did exactly that, because a prop called `date` taking a
|
|
169
|
+
* string is not obviously wrong, and the defect is invisible to every reviewer who
|
|
170
|
+
* does not open the page. `StackGrid01` refuses its own missing `ownLabel` for the
|
|
171
|
+
* same reason and by the same mechanism. The refusal is on the pair, not on the
|
|
172
|
+
* shape of either value: a site whose own convention writes dates as `2026-09-26`
|
|
173
|
+
* has the machine value on the element already, in the `datetime` attribute and in
|
|
174
|
+
* every feed, so displaying it a second time is a redundancy rather than a
|
|
175
|
+
* choice, and the `description` and `footer` slots are there for anything else the
|
|
176
|
+
* page wants to say about the post.
|
|
156
177
|
*
|
|
157
178
|
* The body is `children` and it is held by `Prose`, not by a set of per-element
|
|
158
179
|
* props. A post's body is authored somewhere else - a Markdown pipeline, a CMS -
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/pages/blog-post-page/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/pages/blog-post-page/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;;;OAQG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;;;OASG;IACH,QAAQ,EAAE,GAAG,MAAM,IAAI,MAAM,IAAI,MAAM,EAAE,CAAA;IACzC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB;;;;OAIG;IACH,WAAW,EAAE,mBAAmB,CAAA;IAChC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAA;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,WAAW,EACX,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,WAAW,EACX,UAAU,EACV,SAAS,EACT,MAAM,EACN,SAAS,GACV,EAAE,iBAAiB,+BA6FnB;AAED,eAAe,YAAY,CAAA"}
|
|
@@ -3,7 +3,7 @@ import { Badge } from '../../components/ui/badge.js';
|
|
|
3
3
|
import { CtaLink } from '../../components/ui/cta-link.js';
|
|
4
4
|
import { Heading, Text } from '../../components/ui/typography.js';
|
|
5
5
|
import { Prose } from '../../components/ui/prose.js';
|
|
6
|
-
import { Section } from '../../components/ui/section.js';
|
|
6
|
+
import { Section, headingSizeClass } from '../../components/ui/section.js';
|
|
7
7
|
/**
|
|
8
8
|
* A complete blog post screen: the title, the standfirst, the byline, the body at
|
|
9
9
|
* the measure, and the trail to the neighbouring posts.
|
|
@@ -19,14 +19,28 @@ import { Section } from '../../components/ui/section.js';
|
|
|
19
19
|
* its frontmatter to this Page. The Page is what makes the frontmatter a
|
|
20
20
|
* contract, because a field that is not a prop is a field nothing checks.
|
|
21
21
|
*
|
|
22
|
-
* **The date is two props
|
|
23
|
-
* the
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* search result, and neither reader can see
|
|
27
|
-
* the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
22
|
+
* **The date is two props, and the Page will not collapse them into one.** `date`
|
|
23
|
+
* is the words a reader sees and `dateTime` is the machine value on the `time`
|
|
24
|
+
* element, and they are kept apart because parsing a formatted date back into an
|
|
25
|
+
* ISO one is a guess about a locale. A wrong date in a `datetime` attribute is a
|
|
26
|
+
* wrong date in a feed reader and in a search result, and neither reader can see
|
|
27
|
+
* the string that was wrong.
|
|
28
|
+
*
|
|
29
|
+
* The other half of that is the one this Page now refuses. A Page that renders a
|
|
30
|
+
* date has no business making a reader parse `2026-09-26`, and the Page is the
|
|
31
|
+
* only place in a site where that sentence can be enforced rather than requested,
|
|
32
|
+
* because a consumer's own pipeline is where the date is a string and Prism is
|
|
33
|
+
* where it becomes a page. So `date` is the caller's to format, and handing this
|
|
34
|
+
* Page the same string in both slots is refused at render rather than shipped:
|
|
35
|
+
* all four NaniSoft sites did exactly that, because a prop called `date` taking a
|
|
36
|
+
* string is not obviously wrong, and the defect is invisible to every reviewer who
|
|
37
|
+
* does not open the page. `StackGrid01` refuses its own missing `ownLabel` for the
|
|
38
|
+
* same reason and by the same mechanism. The refusal is on the pair, not on the
|
|
39
|
+
* shape of either value: a site whose own convention writes dates as `2026-09-26`
|
|
40
|
+
* has the machine value on the element already, in the `datetime` attribute and in
|
|
41
|
+
* every feed, so displaying it a second time is a redundancy rather than a
|
|
42
|
+
* choice, and the `description` and `footer` slots are there for anything else the
|
|
43
|
+
* page wants to say about the post.
|
|
30
44
|
*
|
|
31
45
|
* The body is `children` and it is held by `Prose`, not by a set of per-element
|
|
32
46
|
* props. A post's body is authored somewhere else - a Markdown pipeline, a CMS -
|
|
@@ -44,6 +58,17 @@ import { Section } from '../../components/ui/section.js';
|
|
|
44
58
|
* document their pipeline produced.
|
|
45
59
|
*/
|
|
46
60
|
export function BlogPostPage({ title, description, date, dateTime, author, tags, readingTime, children, previous, next, trailLabels, trailLabel, indexLink, footer, className, }) {
|
|
47
|
-
|
|
61
|
+
// The one input this Page refuses, and the reason it is a refusal rather than a
|
|
62
|
+
// note in a JSDoc block. A JSDoc block is read by whoever is integrating Prism
|
|
63
|
+
// and by an agent reading the corpus; this line runs in the four consumer
|
|
64
|
+
// repositories the next time one of them builds, which is the only place the
|
|
65
|
+
// string that was wrong could still be changed.
|
|
66
|
+
if (date === dateTime) {
|
|
67
|
+
throw new Error('BlogPostPage: `date` and `dateTime` are the same string, so the Page has been handed no ' +
|
|
68
|
+
'display formatting and would put a machine value in front of a reader. `date` is the words ' +
|
|
69
|
+
'a reader sees and `dateTime` is the machine value on the `time` element; format `date` for ' +
|
|
70
|
+
'display and pass the ISO date as `dateTime`.');
|
|
71
|
+
}
|
|
72
|
+
return (_jsxs(Section, { className: className, children: [_jsxs("article", { "data-slot": "blog-post", className: "flex flex-col gap-8", children: [_jsxs("header", { className: "flex flex-col gap-4", children: [_jsx(Heading, { as: "h1", className: headingSizeClass('h1'), children: title }), description ? (_jsx(Text, { size: "lg", className: "text-muted-foreground max-w-measure", children: description })) : null, _jsxs("div", { className: "text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-2 text-sm", children: [author ? _jsx("span", { className: "text-foreground font-medium", children: author }) : null, _jsx("time", { dateTime: dateTime, children: date }), readingTime ? _jsx("span", { children: readingTime }) : null] }), tags && tags.length > 0 ? (_jsx("ul", { className: "flex flex-wrap items-center gap-2", children: tags.map((tag) => (_jsx("li", { children: tag.href ? (_jsx("a", { href: tag.href, className: "rounded-sm", "aria-label": `Posts tagged ${tag.label}`, children: _jsx(Badge, { variant: "secondary", children: tag.label }) })) : (_jsx(Badge, { variant: "secondary", children: tag.label })) }, tag.label))) })) : null] }), _jsx(Prose, { children: children }), previous || next ? (_jsxs("nav", { "aria-label": trailLabel, className: "border-border grid gap-4 border-t pt-6 sm:grid-cols-2", children: [previous ? (_jsxs("a", { href: previous.href, className: "flex flex-col gap-1 rounded-sm", children: [_jsx("span", { className: "text-muted-foreground text-xs font-medium tracking-wide uppercase", children: trailLabels.previous }), _jsx("span", { className: "text-sm font-semibold", children: previous.title })] })) : null, next ? (_jsxs("a", { href: next.href, className: "flex flex-col gap-1 rounded-sm sm:items-end sm:text-right", children: [_jsx("span", { className: "text-muted-foreground text-xs font-medium tracking-wide uppercase", children: trailLabels.next }), _jsx("span", { className: "text-sm font-semibold", children: next.title })] })) : null] })) : null, footer ? _jsx("div", { className: "border-border border-t pt-8", children: footer }) : null] }), indexLink ? (_jsx("div", { className: "mt-10 flex justify-center", children: _jsx(CtaLink, { href: indexLink.href, variant: "outline", children: indexLink.title }) })) : null] }));
|
|
48
73
|
}
|
|
49
74
|
export default BlogPostPage;
|