@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.
Files changed (47) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/blocks/data-table-01/data-table.d.ts +10 -0
  3. package/dist/blocks/data-table-01/data-table.d.ts.map +1 -1
  4. package/dist/blocks/data-table-01/data-table.js +2 -1
  5. package/dist/blocks/page-header-01/page-header.d.ts +7 -0
  6. package/dist/blocks/page-header-01/page-header.d.ts.map +1 -1
  7. package/dist/blocks/page-header-01/page-header.js +9 -1
  8. package/dist/blocks/run-console-01/run-console.d.ts.map +1 -1
  9. package/dist/blocks/run-console-01/run-console.js +2 -1
  10. package/dist/blocks/site-footer/site-footer.d.ts +16 -0
  11. package/dist/blocks/site-footer/site-footer.d.ts.map +1 -1
  12. package/dist/blocks/site-footer/site-footer.js +17 -1
  13. package/dist/blocks/stack-grid-01/stack-grid.d.ts +13 -0
  14. package/dist/blocks/stack-grid-01/stack-grid.d.ts.map +1 -1
  15. package/dist/blocks/stack-grid-01/stack-grid.js +14 -1
  16. package/dist/components/ui/diagram.d.ts +38 -7
  17. package/dist/components/ui/diagram.d.ts.map +1 -1
  18. package/dist/components/ui/diagram.js +134 -52
  19. package/dist/components/ui/fact-list.d.ts +17 -0
  20. package/dist/components/ui/fact-list.d.ts.map +1 -1
  21. package/dist/components/ui/fact-list.js +18 -1
  22. package/dist/components/ui/prose.d.ts +11 -0
  23. package/dist/components/ui/prose.d.ts.map +1 -1
  24. package/dist/components/ui/prose.js +12 -1
  25. package/dist/components/ui/pulse-graph.d.ts +36 -4
  26. package/dist/components/ui/pulse-graph.d.ts.map +1 -1
  27. package/dist/components/ui/pulse-graph.js +174 -60
  28. package/dist/components/ui/section.d.ts +33 -31
  29. package/dist/components/ui/section.d.ts.map +1 -1
  30. package/dist/components/ui/section.js +33 -31
  31. package/dist/lib/figure.d.ts +149 -0
  32. package/dist/lib/figure.d.ts.map +1 -0
  33. package/dist/lib/figure.js +242 -0
  34. package/dist/live/run-stream-01/run-stream.d.ts.map +1 -1
  35. package/dist/live/run-stream-01/run-stream.js +2 -1
  36. package/dist/pages/blog-post-page/index.d.ts +36 -15
  37. package/dist/pages/blog-post-page/index.d.ts.map +1 -1
  38. package/dist/pages/blog-post-page/index.js +35 -10
  39. package/dist/pages/docs-shell/docs-shell.d.ts +120 -18
  40. package/dist/pages/docs-shell/docs-shell.d.ts.map +1 -1
  41. package/dist/pages/docs-shell/docs-shell.js +208 -24
  42. package/dist/pages/error-page/index.d.ts.map +1 -1
  43. package/dist/pages/error-page/index.js +7 -3
  44. package/dist/pages/not-found-page/index.d.ts.map +1 -1
  45. package/dist/pages/not-found-page/index.js +4 -3
  46. package/dist/styles.css +107 -12
  47. 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":"AAQA;;;;;;;;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,+BA8HlB;AAED,eAAe,WAAW,CAAA"}
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: "text-lg font-semibold tracking-tight", 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
+ 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, already formatted for display, and the machine value
65
- * for the `time` element's `dateTime`. Both are props, and the machine one is
66
- * required: `dateTime` is what a reader's browser, a feed reader and a search
67
- * engine use to order posts, and a formatted string is none of those.
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
- * in a `datetime` attribute is a wrong date in a feed.
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: string;
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.** `date` is the words a reader sees and `dateTime` is
149
- * the machine value on the `time` element, and they are kept apart because
150
- * parsing a formatted date back into an ISO one is a guess about a locale. A
151
- * wrong date in a `datetime` attribute is a wrong date in a feed reader and in a
152
- * search result, and neither reader can see the string that was wrong. This is
153
- * the arrangement every one of the four sites needs and none of them has, because
154
- * each of them formats its date in one place and has nowhere else to put the ISO
155
- * value.
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;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,+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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;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,+BAgFnB;AAED,eAAe,YAAY,CAAA"}
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.** `date` is the words a reader sees and `dateTime` is
23
- * the machine value on the `time` element, and they are kept apart because
24
- * parsing a formatted date back into an ISO one is a guess about a locale. A
25
- * wrong date in a `datetime` attribute is a wrong date in a feed reader and in a
26
- * search result, and neither reader can see the string that was wrong. This is
27
- * the arrangement every one of the four sites needs and none of them has, because
28
- * each of them formats its date in one place and has nowhere else to put the ISO
29
- * value.
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
- 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", size: "3xl", 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] }));
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;