@nanisoft/prism-ui 0.15.0 → 0.16.1

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 +131 -17
  40. package/dist/pages/docs-shell/docs-shell.d.ts.map +1 -1
  41. package/dist/pages/docs-shell/docs-shell.js +230 -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 +131 -12
  47. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { INK_CLEARANCE, fitPoints, labelHalfHeight, labelHalfWidth, strokeBetween, } from '../../lib/figure.js';
2
3
  import { cn } from '../../lib/utils.js';
3
4
  /**
4
5
  * The drawing's accessible name, derived from the text its own marks carry.
@@ -37,9 +38,40 @@ function derivedName(nodes) {
37
38
  * drawing would come out with 3px labels in one call and 40px labels in another.
38
39
  * Fitting the caller's points into a fixed canvas makes the label a constant
39
40
  * and makes the drawing a function of the shape rather than of the scale.
41
+ *
42
+ * **A fixed canvas fixes the ratio, and the ratio was the half of the argument
43
+ * that was missing.** Every label's rendered size is its size in user units times
44
+ * the rendered width over this number, so a drawing that is right at a desktop
45
+ * width shrinks with its container until it says nothing. In the 342 pixels a
46
+ * phone leaves a Diagram, the largest label drawn here came to just under seven
47
+ * pixels. The canvas is still the right answer and it is what keeps a label a
48
+ * constant rather than a function of the caller's arithmetic; what it does not do
49
+ * is set a floor, and `LEGIBLE_MIN_WIDTH` is that floor.
40
50
  */
41
51
  const CANVAS_WIDTH = 640;
42
52
  const CANVAS_HEIGHT = 400;
53
+ /**
54
+ * The width below which the drawing stops being legible, as a spacing multiple.
55
+ *
56
+ * The floor is `CANVAS_WIDTH` itself, so the rule is one sentence: the drawing is
57
+ * never rendered narrower than its own coordinate space, and one user unit is
58
+ * therefore never less than one pixel. Every label is then at least its own size
59
+ * in user units, and the smallest of them is `RELATION_NAME_SIZE`, which is the
60
+ * size `--text-xs` resolves to. So no label this drawing sets is smaller than
61
+ * `text-xs`, and at the floor the smallest is exactly it.
62
+ *
63
+ * **A container narrower than the floor scrolls rather than shrinking, and that
64
+ * is the answer this repository already gives a table and a board.** A table that
65
+ * restacks has stopped being a table, and a schematic whose labels are six pixels
66
+ * tall has stopped being a schematic: it is the shape with nothing written on it
67
+ * a reader can read. So the drawing holds its size and the container moves under
68
+ * it, which is what `Table` does with seven columns and what `Gantt01` does with
69
+ * a schedule of names and bars.
70
+ *
71
+ * `160` is `CANVAS_WIDTH` as a multiple of the authored spacing base, which is
72
+ * how a width nobody held a separate decision for is written.
73
+ */
74
+ const LEGIBLE_MIN_WIDTH = 'min-w-160';
43
75
  /**
44
76
  * The band left empty around the fitted points, in user units.
45
77
  *
@@ -65,11 +97,17 @@ const NODE_RADIUS = 5;
65
97
  * a font size is a coordinate and not a length on the page. These are geometry,
66
98
  * the one thing Tailwind stays authoritative for, and they are the only numbers
67
99
  * in this file that are not a token.
100
+ *
101
+ * The two supporting sizes are twelve user units, which is what `--text-xs`
102
+ * resolves to, so at `LEGIBLE_MIN_WIDTH` they render at exactly that step and
103
+ * never below it. The name is one unit larger, which is the same ratio between
104
+ * the two this drawing has always drawn, so the hierarchy between a name and the
105
+ * line under it is the hierarchy it was.
68
106
  */
69
107
  const NODE_NAME_SIZE = 13;
70
108
  const NODE_NAME_OFFSET = 20;
71
- const NODE_SUBTITLE_SIZE = 11;
72
- const RELATION_NAME_SIZE = 11;
109
+ const NODE_SUBTITLE_SIZE = 12;
110
+ const RELATION_NAME_SIZE = 12;
73
111
  /**
74
112
  * The subtitle's baseline, measured down from the mark's centre.
75
113
  *
@@ -80,35 +118,51 @@ const RELATION_NAME_SIZE = 11;
80
118
  */
81
119
  const NODE_SUBTITLE_OFFSET = NODE_NAME_OFFSET + NODE_SUBTITLE_SIZE + 5;
82
120
  /**
83
- * How far a relation's label sits off its line, in user units. Perpendicular to
84
- * the line, so the label never sits on the stroke it belongs to.
121
+ * Where a relation's label sits, which is beside the line and never on it.
122
+ *
123
+ * The offset is the label's own half-width measured across the line plus its half
124
+ * height, then the clearance. A single number cannot do this: on a horizontal
125
+ * line a label's width runs along the line and its height across it, and on a
126
+ * vertical line those two swap, so a constant that clears a horizontal relation
127
+ * leaves a vertical one sitting on its own stroke.
128
+ *
129
+ * It is measured from the middle of the stroke a reader can see rather than of
130
+ * the line between the two mark centres, so the label stays beside the line that
131
+ * was drawn.
85
132
  */
86
- const RELATION_NAME_LIFT = 7;
133
+ function relationLabel(label, segment) {
134
+ const dx = segment.to.x - segment.from.x;
135
+ const dy = segment.to.y - segment.from.y;
136
+ const length = Math.hypot(dx, dy);
137
+ const middle = {
138
+ x: (segment.from.x + segment.to.x) / 2,
139
+ y: (segment.from.y + segment.to.y) / 2,
140
+ };
141
+ if (length === 0)
142
+ return middle;
143
+ const across = { x: -dy / length, y: dx / length };
144
+ const extent = Math.abs(across.x) * labelHalfWidth(label, RELATION_NAME_SIZE) +
145
+ Math.abs(across.y) * labelHalfHeight(RELATION_NAME_SIZE);
146
+ const offset = extent + INK_CLEARANCE;
147
+ return { x: middle.x + across.x * offset, y: middle.y + across.y * offset };
148
+ }
87
149
  /**
88
- * The caller's points, fitted into the canvas with their aspect ratio kept and
89
- * the leftover shared out evenly.
150
+ * The labels this drawing prints under one node, which are what a stroke leaving it
151
+ * has to be trimmed against.
90
152
  *
91
- * A caller whose points span one axis only, or who passes a single node, gets
92
- * the other axis centred rather than a division by zero: the scale is taken
93
- * over the axes that have an extent, and a diagram with no extent at all keeps
94
- * the caller's own numbers.
153
+ * Each label is its own obstacle rather than one box enclosing both, and that is the
154
+ * whole of the trim rule as it applies here: a node's name and its second line are
155
+ * both below its mark, so a relation leaving sideways runs along the mark's centre
156
+ * line and clears both, while one leaving downward enters both. The arithmetic is
157
+ * `lib/figure`, which is where the shared answer lives; what is left here is the two
158
+ * labels this drawing prints and where it prints them.
95
159
  */
96
- function fit(nodes) {
97
- if (nodes.length === 0)
98
- return () => ({ x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2 });
99
- const xs = nodes.map((node) => node.x);
100
- const ys = nodes.map((node) => node.y);
101
- const minX = Math.min(...xs);
102
- const minY = Math.min(...ys);
103
- const spanX = Math.max(...xs) - minX;
104
- const spanY = Math.max(...ys) - minY;
105
- const roomX = CANVAS_WIDTH - PADDING * 2;
106
- const roomY = CANVAS_HEIGHT - PADDING * 2;
107
- const scale = Math.min(spanX === 0 ? Number.POSITIVE_INFINITY : roomX / spanX, spanY === 0 ? Number.POSITIVE_INFINITY : roomY / spanY);
108
- const unit = Number.isFinite(scale) && scale > 0 ? scale : 1;
109
- const offsetX = PADDING + (roomX - spanX * unit) / 2 - minX * unit;
110
- const offsetY = PADDING + (roomY - spanY * unit) / 2 - minY * unit;
111
- return (node) => ({ x: node.x * unit + offsetX, y: node.y * unit + offsetY });
160
+ function nodeLabels(node) {
161
+ const labels = [{ text: node.name, size: NODE_NAME_SIZE, offset: NODE_NAME_OFFSET }];
162
+ if (node.subtitle !== undefined) {
163
+ labels.push({ text: node.subtitle, size: NODE_SUBTITLE_SIZE, offset: NODE_SUBTITLE_OFFSET });
164
+ }
165
+ return labels;
112
166
  }
113
167
  /**
114
168
  * A diagram of named things and the relations between them.
@@ -175,15 +229,38 @@ function fit(nodes) {
175
229
  * caller `stroke`, so the surface is `nodes`, `relations`, `label`,
176
230
  * `decorative` and `className` and nothing else.
177
231
  *
178
- * Positions are the caller's: the Component fits and draws, and it does not lay
179
- * out a graph, so it cannot know that two labels will not collide. A relation
232
+ * **No stroke crosses a label, and every relation still reaches the marks it
233
+ * joins.** A relation used to run from one mark's centre to the other, and a node's
234
+ * name is printed under its mark, so every edge heading downward struck through the
235
+ * name of the node it left. Each label a node prints is now its own obstacle rather
236
+ * than part of one box enclosing the whole node, and a relation is trimmed only
237
+ * against the labels it would actually enter. That direction is the whole of the
238
+ * rule: a name and its second line both sit below their mark, so a relation
239
+ * leaving sideways runs along the mark's centre line and clears both, and it stops
240
+ * at the mark's own edge with the ink clearance between. Backing every relation off
241
+ * the widest label at its node instead detached them from the marks they connect,
242
+ * which is a drawing of a shape with no relations on it. The Component cannot know
243
+ * that two labels will not collide with one another, which is the caller's
244
+ * positions to settle, and it does know every mark's radius, both label offsets and
245
+ * every label's own width, because a monospaced label's width is its character
246
+ * count. A relation's
247
+ * own label is offset by its own extent across the line. Those are two pieces of
248
+ * geometry the drawing had and did not use, and `test/diagram.test.tsx` asserts
249
+ * both halves as geometry rather than the presence of a class name.
250
+ *
251
+ * **No label is drawn below `text-xs`.** The drawing is wrapped in a container
252
+ * that scrolls sideways, and the drawing itself holds a floor of its own
253
+ * coordinate space, so a phone sees the whole diagram at a legible size rather
254
+ * than the whole diagram at six pixels. That is the same bargain `Table` strikes
255
+ * with seven columns, and it is a bargain rather than an accident because the
256
+ * alternative is a picture of a shape with nothing written on it. A relation
180
257
  * naming a node that is not in `nodes` is dropped, and the count of dropped
181
258
  * relations is on the element as `data-unresolved-relations` rather than
182
259
  * swallowed, because a line that silently did not draw is the failure this
183
260
  * Component exists to make impossible.
184
261
  */
185
262
  function Diagram({ nodes, relations, label, decorative = false, className, }) {
186
- const place = fit(nodes);
263
+ const place = fitPoints(nodes, { width: CANVAS_WIDTH, height: CANVAS_HEIGHT, padding: PADDING });
187
264
  const byId = new Map(nodes.map((node) => [node.id, node]));
188
265
  // An empty `label` is the same answer as no `label`, so the drawing is named by
189
266
  // what its own marks carry rather than announced as an unnamed image. A blank
@@ -193,28 +270,33 @@ function Diagram({ nodes, relations, label, decorative = false, className, }) {
193
270
  const drawn = relations.flatMap((relation) => {
194
271
  const from = byId.get(relation.from);
195
272
  const to = byId.get(relation.to);
196
- return from && to ? [{ relation, from: place(from), to: place(to) }] : [];
273
+ if (!from || !to)
274
+ return [];
275
+ // Each end carries its mark's centre and the labels printed under it, because
276
+ // a stroke is trimmed against the labels it would enter and the two are not the
277
+ // same thing.
278
+ const end = (node) => ({ at: place(node), labels: nodeLabels(node) });
279
+ return [{ relation, from: end(from), to: end(to) }];
197
280
  });
198
- return (_jsxs("svg", { "data-slot": "diagram", "data-unresolved-relations": relations.length - drawn.length, viewBox: `0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`, xmlns: "http://www.w3.org/2000/svg", role: decorative ? undefined : 'img', "aria-label": decorative ? undefined : name, "aria-hidden": decorative || undefined, className: cn('h-auto w-full', className), children: [drawn.map(({ relation, from, to }) => {
199
- // A label placed beside its line rather than on it. The offset is the
200
- // line's normal, so a vertical relation's label sits beside it and a
201
- // horizontal one's sits above it, and neither ever sits on the stroke.
202
- const dx = to.x - from.x;
203
- const dy = to.y - from.y;
204
- const length = Math.hypot(dx, dy);
205
- const lift = length === 0 ? 0 : RELATION_NAME_LIFT / length;
206
- return (_jsxs("g", { "data-slot": "diagram-relation", "data-relation": `${relation.from}-${relation.to}`, children: [_jsx("path", { d: `M ${from.x} ${from.y} L ${to.x} ${to.y}`, strokeWidth: 1, strokeDasharray: relation.indirect ? '4 4' : undefined, className: "stroke-muted-foreground fill-none" }), _jsx("text", { x: (from.x + to.x) / 2 - dy * lift, y: (from.y + to.y) / 2 + dx * lift, fontSize: RELATION_NAME_SIZE, textAnchor: "middle", className: "fill-muted-foreground font-mono", children: relation.label })] }, `${relation.from}-${relation.to}`));
207
- }), nodes.map((node) => {
208
- const at = place(node);
209
- // `default` is the absence of the attribute, spelled the way the token
210
- // build spells it, so a node on the base pack resolves its fill and its
211
- // stroke from the page rather than from a selector nothing emits.
212
- const boundary = node.pack !== undefined && node.pack !== 'default' ? node.pack : undefined;
213
- return (_jsxs("g", { "data-slot": "diagram-node", "data-node": node.id, "data-emphasis": node.emphasis || undefined, children: [_jsx("circle", { cx: at.x, cy: at.y, r: NODE_RADIUS, "data-pack": boundary, strokeWidth: node.emphasis ? 2 : 1, className: cn(node.emphasis ? 'fill-accent stroke-brand-ink' : 'fill-card stroke-border') }), _jsx("text", { x: at.x, y: at.y + NODE_NAME_OFFSET, fontSize: NODE_NAME_SIZE, textAnchor: "middle", className: "fill-foreground font-mono", children: node.name }), node.subtitle ? (_jsx("text", { "data-slot": "diagram-node-subtitle", x: at.x, y: at.y + NODE_SUBTITLE_OFFSET, fontSize: NODE_SUBTITLE_SIZE, textAnchor: "middle",
214
- // Muted ink rather than the name's own: the line below a name is
215
- // supporting, and a reader's eye reaches the name first. Both are
216
- // contract roles, so a boundary above either moves it.
217
- className: "fill-muted-foreground font-mono", children: node.subtitle })) : null] }, node.id));
218
- })] }));
281
+ return (
282
+ // The sideways region, for the reason `LEGIBLE_MIN_WIDTH` states: the drawing
283
+ // holds a legible floor and a container narrower than it moves under the
284
+ // drawing rather than the drawing shrinking below what it says.
285
+ _jsx("div", { "data-slot": "diagram-container", className: "w-full overflow-x-auto", children: _jsxs("svg", { "data-slot": "diagram", "data-unresolved-relations": relations.length - drawn.length, viewBox: `0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`, xmlns: "http://www.w3.org/2000/svg", role: decorative ? undefined : 'img', "aria-label": decorative ? undefined : name, "aria-hidden": decorative || undefined, className: cn('h-auto w-full', LEGIBLE_MIN_WIDTH, className), children: [drawn.map(({ relation, from, to }) => {
286
+ const line = strokeBetween(from.at, to.at, NODE_RADIUS, from.labels, to.labels);
287
+ const at = relationLabel(relation.label, line);
288
+ return (_jsxs("g", { "data-slot": "diagram-relation", "data-relation": `${relation.from}-${relation.to}`, children: [_jsx("path", { d: `M ${line.from.x} ${line.from.y} L ${line.to.x} ${line.to.y}`, strokeWidth: 1, strokeDasharray: relation.indirect ? '4 4' : undefined, className: "stroke-muted-foreground fill-none" }), _jsx("text", { x: at.x, y: at.y, fontSize: RELATION_NAME_SIZE, textAnchor: "middle", className: "fill-muted-foreground font-mono", children: relation.label })] }, `${relation.from}-${relation.to}`));
289
+ }), nodes.map((node) => {
290
+ const at = place(node);
291
+ // `default` is the absence of the attribute, spelled the way the token
292
+ // build spells it, so a node on the base pack resolves its fill and its
293
+ // stroke from the page rather than from a selector nothing emits.
294
+ const boundary = node.pack !== undefined && node.pack !== 'default' ? node.pack : undefined;
295
+ return (_jsxs("g", { "data-slot": "diagram-node", "data-node": node.id, "data-emphasis": node.emphasis || undefined, children: [_jsx("circle", { cx: at.x, cy: at.y, r: NODE_RADIUS, "data-pack": boundary, strokeWidth: node.emphasis ? 2 : 1, className: cn(node.emphasis ? 'fill-accent stroke-brand-ink' : 'fill-card stroke-border') }), _jsx("text", { x: at.x, y: at.y + NODE_NAME_OFFSET, fontSize: NODE_NAME_SIZE, textAnchor: "middle", className: "fill-foreground font-mono", children: node.name }), node.subtitle ? (_jsx("text", { "data-slot": "diagram-node-subtitle", x: at.x, y: at.y + NODE_SUBTITLE_OFFSET, fontSize: NODE_SUBTITLE_SIZE, textAnchor: "middle",
296
+ // Muted ink rather than the name's own: the line below a name is
297
+ // supporting, and a reader's eye reaches the name first. Both are
298
+ // contract roles, so a boundary above either moves it.
299
+ className: "fill-muted-foreground font-mono", children: node.subtitle })) : null] }, node.id));
300
+ })] }) }));
219
301
  }
220
302
  export { Diagram };
@@ -75,6 +75,23 @@ export type FactListProps = ComponentProps<'dl'> & {
75
75
  * wraps and a value that does not are both common and centring one of them reads
76
76
  * as an error.
77
77
  *
78
+ * **The value is set flush left, and the reason is the values rather than the
79
+ * rows.** A right-aligned value is the right answer in a column of figures, where
80
+ * the digits are the same width and the comparison a reader makes is between the
81
+ * ends of them. This Component has no numeric mode, no `tabular-nums` and no
82
+ * monospaced arm, so nothing here is a figure and nothing lines up on its last
83
+ * digit: what lines up is the first letter of each answer, and right alignment
84
+ * moves that edge to a different place on every row. A value long enough to wrap
85
+ * is the case that shows it, because the second line starts wherever the first
86
+ * one ended and the column reads as a mistake rather than as a decision. `text-
87
+ * balance` stays, because it is the same reason it was there: a two-line answer
88
+ * should break where it breaks well, not where the measure runs out.
89
+ *
90
+ * A fact whose value *is* a figure is not the case this loses. A caller that wants
91
+ * a right-aligned figure column composes it: the value is a `ReactNode`, so a
92
+ * caller can pass the span it wants, and Prism holds no opinion about a value it
93
+ * did not lay out.
94
+ *
78
95
  * The hairline between rows is `border-border`, so a list of facts reads as a
79
96
  * table without becoming one: no header row, because a fact has no column
80
97
  * header, and no `Table` semantics, because these are terms and their answers
@@ -1 +1 @@
1
- {"version":3,"file":"fact-list.d.ts","sourceRoot":"","sources":["../../../src/components/ui/fact-list.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG;IACjB;;;;OAIG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB,6DAA6D;IAC7D,KAAK,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG;IACjD;;;;OAIG;IACH,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,iBAAS,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,sCAuCrE;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
1
+ {"version":3,"file":"fact-list.d.ts","sourceRoot":"","sources":["../../../src/components/ui/fact-list.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG;IACjB;;;;OAIG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB,6DAA6D;IAC7D,KAAK,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG;IACjD;;;;OAIG;IACH,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,iBAAS,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,sCAuCrE;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
@@ -20,6 +20,23 @@ import { cn } from '../../lib/utils.js';
20
20
  * wraps and a value that does not are both common and centring one of them reads
21
21
  * as an error.
22
22
  *
23
+ * **The value is set flush left, and the reason is the values rather than the
24
+ * rows.** A right-aligned value is the right answer in a column of figures, where
25
+ * the digits are the same width and the comparison a reader makes is between the
26
+ * ends of them. This Component has no numeric mode, no `tabular-nums` and no
27
+ * monospaced arm, so nothing here is a figure and nothing lines up on its last
28
+ * digit: what lines up is the first letter of each answer, and right alignment
29
+ * moves that edge to a different place on every row. A value long enough to wrap
30
+ * is the case that shows it, because the second line starts wherever the first
31
+ * one ended and the column reads as a mistake rather than as a decision. `text-
32
+ * balance` stays, because it is the same reason it was there: a two-line answer
33
+ * should break where it breaks well, not where the measure runs out.
34
+ *
35
+ * A fact whose value *is* a figure is not the case this loses. A caller that wants
36
+ * a right-aligned figure column composes it: the value is a `ReactNode`, so a
37
+ * caller can pass the span it wants, and Prism holds no opinion about a value it
38
+ * did not lay out.
39
+ *
23
40
  * The hairline between rows is `border-border`, so a list of facts reads as a
24
41
  * table without becoming one: no header row, because a fact has no column
25
42
  * header, and no `Table` semantics, because these are terms and their answers
@@ -33,6 +50,6 @@ import { cn } from '../../lib/utils.js';
33
50
  function FactList({ facts, label, className, ...props }) {
34
51
  if (facts.length === 0)
35
52
  return null;
36
- return (_jsxs("dl", { "data-slot": "fact-list", className: cn('flex w-full flex-col text-sm', className), ...props, children: [label ? (_jsx("dt", { className: "text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase", children: label })) : null, facts.map((fact, index) => (_jsxs("div", { "data-slot": "fact-list-row", className: "border-border flex items-start justify-between gap-6 border-b py-2 last:border-b-0", children: [_jsx("dt", { className: "text-muted-foreground shrink-0 font-medium", children: fact.label }), _jsx("dd", { className: "text-right text-balance", children: fact.href ? (_jsx("a", { href: fact.href, rel: fact.newTab ? 'noopener noreferrer' : undefined, target: fact.newTab ? '_blank' : undefined, className: "underline underline-offset-4 hover:underline", children: fact.value })) : (fact.value) })] }, index)))] }));
53
+ return (_jsxs("dl", { "data-slot": "fact-list", className: cn('flex w-full flex-col text-sm', className), ...props, children: [label ? (_jsx("dt", { className: "text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase", children: label })) : null, facts.map((fact, index) => (_jsxs("div", { "data-slot": "fact-list-row", className: "border-border flex items-start justify-between gap-6 border-b py-2 last:border-b-0", children: [_jsx("dt", { className: "text-muted-foreground shrink-0 font-medium", children: fact.label }), _jsx("dd", { className: "text-left text-balance", children: fact.href ? (_jsx("a", { href: fact.href, rel: fact.newTab ? 'noopener noreferrer' : undefined, target: fact.newTab ? '_blank' : undefined, className: "underline underline-offset-4 hover:underline", children: fact.value })) : (fact.value) })] }, index)))] }));
37
54
  }
38
55
  export { FactList };
@@ -105,6 +105,17 @@ export type ProseProps = ComponentProps<'div'> & {
105
105
  * is: the caller's own flow content.
106
106
  *
107
107
  * It is a server Component: no hook, no context and no client code.
108
+ *
109
+ * **The heading ladder here is the one `SectionHeading` walks, entered one rung
110
+ * down, and the reason is the measure rather than a second decision.** A run of
111
+ * prose is held to `--container-measure`, 42rem. The Display step is 3rem, and a
112
+ * 42rem column is fourteen of them, so a document title set at Display is a
113
+ * title broken into fragments rather than a title. So the child treatments set
114
+ * `h1` at `4xl` and descend one authored step per level to `lg` at `h5` and
115
+ * `h6`, which is the same scale, the same floor and the same rule about a
116
+ * heading never rendering smaller than the copy it introduces, entered at the
117
+ * rung that fits the column. A document and a page are therefore one hierarchy
118
+ * at different reading widths rather than two answers to one question.
108
119
  */
109
120
  declare function Prose({ children, size, fullWidth, className, ...props }: ProseProps): import("react").JSX.Element;
110
121
  export { Prose };
@@ -1 +1 @@
1
- {"version":3,"file":"prose.d.ts","sourceRoot":"","sources":["../../../src/components/ui/prose.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItD;;;;;;;GAOG;AACH,QAAA,MAAM,WAAW;;;CAGP,CAAA;AAEV;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,WAAW,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG;IAC/C;;;;OAIG;IACH,QAAQ,EAAE,SAAS,CAAA;IACnB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,IAAa,EAAE,SAAiB,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,UAAU,+BAwC7F;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}
1
+ {"version":3,"file":"prose.d.ts","sourceRoot":"","sources":["../../../src/components/ui/prose.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItD;;;;;;;GAOG;AACH,QAAA,MAAM,WAAW;;;CAGP,CAAA;AAEV;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,WAAW,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG;IAC/C;;;;OAIG;IACH,QAAQ,EAAE,SAAS,CAAA;IACnB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,iBAAS,KAAK,CAAC,EAAE,QAAQ,EAAE,IAAa,EAAE,SAAiB,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,UAAU,+BA0C7F;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -65,13 +65,24 @@ const PROSE_SIZES = {
65
65
  * is: the caller's own flow content.
66
66
  *
67
67
  * It is a server Component: no hook, no context and no client code.
68
+ *
69
+ * **The heading ladder here is the one `SectionHeading` walks, entered one rung
70
+ * down, and the reason is the measure rather than a second decision.** A run of
71
+ * prose is held to `--container-measure`, 42rem. The Display step is 3rem, and a
72
+ * 42rem column is fourteen of them, so a document title set at Display is a
73
+ * title broken into fragments rather than a title. So the child treatments set
74
+ * `h1` at `4xl` and descend one authored step per level to `lg` at `h5` and
75
+ * `h6`, which is the same scale, the same floor and the same rule about a
76
+ * heading never rendering smaller than the copy it introduces, entered at the
77
+ * rung that fits the column. A document and a page are therefore one hierarchy
78
+ * at different reading widths rather than two answers to one question.
68
79
  */
69
80
  function Prose({ children, size = 'base', fullWidth = false, className, ...props }) {
70
81
  return (_jsx("div", { "data-slot": "prose", "data-size": size, className: cn('text-pretty flex flex-col gap-4 leading-relaxed', fullWidth ? 'max-w-none' : 'max-w-measure', PROSE_SIZES[size],
71
82
  // The block treatments. Every child is styled by its own tag so a
72
83
  // consumer's plain elements read as the system, and the gaps the flex
73
84
  // column owns are the rhythm between blocks.
74
- '[&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:underline', '[&_blockquote]:border-border [&_blockquote]:text-muted-foreground [&_blockquote]:border-l-2 [&_blockquote]:pl-4 [&_blockquote]:italic', '[&_code]:bg-muted [&_code]:text-muted-foreground [&_code]:rounded-sm [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs', '[&_h1]:text-balance [&_h1]:text-3xl [&_h1]:font-semibold [&_h1]:tracking-tight', '[&_h2]:text-balance [&_h2]:mt-2 [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:tracking-tight', '[&_h3]:text-balance [&_h3]:mt-2 [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:tracking-tight', '[&_h4]:text-balance [&_h4]:text-lg [&_h4]:font-semibold', '[&_hr]:bg-border [&_hr]:h-px [&_hr]:my-2', '[&_img]:rounded-lg', '[&_li]:leading-relaxed [&_ol]:list-decimal [&_ol]:pl-6 [&_ul]:list-disc [&_ul]:pl-6', '[&_p]:leading-relaxed', '[&_pre]:bg-muted [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-xs', '[&_pre_code]:bg-transparent [&_pre_code]:p-0', '[&_strong]:font-semibold',
85
+ '[&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:underline', '[&_blockquote]:border-border [&_blockquote]:text-muted-foreground [&_blockquote]:border-l-2 [&_blockquote]:pl-4 [&_blockquote]:italic', '[&_code]:bg-muted [&_code]:text-muted-foreground [&_code]:rounded-sm [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs', '[&_h1]:text-balance [&_h1]:text-4xl [&_h1]:font-semibold [&_h1]:tracking-tight', '[&_h2]:text-balance [&_h2]:mt-2 [&_h2]:text-3xl [&_h2]:font-semibold [&_h2]:tracking-tight', '[&_h3]:text-balance [&_h3]:mt-2 [&_h3]:text-2xl [&_h3]:font-semibold [&_h3]:tracking-tight', '[&_h4]:text-balance [&_h4]:text-xl [&_h4]:font-semibold', '[&_h5]:text-balance [&_h5]:text-lg [&_h5]:font-semibold', '[&_h6]:text-balance [&_h6]:text-lg [&_h6]:font-semibold', '[&_hr]:bg-border [&_hr]:h-px [&_hr]:my-2', '[&_img]:rounded-lg', '[&_li]:leading-relaxed [&_ol]:list-decimal [&_ol]:pl-6 [&_ul]:list-disc [&_ul]:pl-6', '[&_p]:leading-relaxed', '[&_pre]:bg-muted [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-xs', '[&_pre_code]:bg-transparent [&_pre_code]:p-0', '[&_strong]:font-semibold',
75
86
  // The one treatment that changes a box type rather than decorating one.
76
87
  // The reasoning, and the two spec rules it rests on, are in this
77
88
  // component's JSDoc: a `display: table` box cannot be a scroll container,
@@ -87,7 +87,12 @@ type PulseFigure = {
87
87
  relations?: readonly PulseRelation[];
88
88
  /**
89
89
  * Layout only, exactly as on every Component: grid placement and width.
90
- * Changing a Prism-owned visual property from here is prohibited.
90
+ * Changing a Prism-owned visual property from here is prohibited, and this
91
+ * Component spreads no other prop, so there is no second way to do it.
92
+ *
93
+ * It lands on the drawing inside the sideways region rather than on the region
94
+ * itself, so a caller narrowing the figure narrows the drawing and the region
95
+ * still holds the floor.
91
96
  */
92
97
  className?: string;
93
98
  };
@@ -175,9 +180,36 @@ export type PulseGraphProps = PulseFigure & ({
175
180
  * sentence is what a screen reader gets. Pass `decorative` when that sentence
176
181
  * already says what the drawing shows.
177
182
  *
178
- * Positions are the caller's. The Component fits and draws, and it does not lay
179
- * out a graph, so it cannot know that two labels will not collide. A relation
180
- * naming a node that is not in `nodes` is dropped and counted on the element.
183
+ * **No stroke crosses a label, and every connector still reaches the marks it
184
+ * joins.** An edge used to run from one mark's centre to the other, and a node's
185
+ * name is printed under its mark, so every edge heading downward struck through the
186
+ * name of the node it left. Each label a node prints is now its own obstacle rather
187
+ * than part of one box enclosing the whole node, and an edge is trimmed only against
188
+ * the labels it would actually enter. That direction is the whole of the rule: a
189
+ * name and a note both sit below their mark, so an edge leaving sideways runs along
190
+ * the mark's centre line and clears both, and it stops at the mark's own edge with
191
+ * the ink clearance between. Backing every edge off the widest label at its node
192
+ * instead detached all five connectors on a six-stage rail from the circles they
193
+ * connect, which is a drawing of a shape with no relations on it. The Component
194
+ * cannot know that two labels will not collide with one another, which is the
195
+ * caller's positions to settle, and it does know every mark's radius, both label
196
+ * offsets and every label's own width, because a monospaced label's width is its
197
+ * character count. The rail is held to
198
+ * the same clearance rather than to a fixed distance from the canvas bottom,
199
+ * because a note is the deepest ink a node draws and a lane whose mark lands low
200
+ * put it under the rail. `test/pulse-graph.test.tsx` asserts both halves as
201
+ * geometry rather than the presence of a class name.
202
+ *
203
+ * **No label is drawn below `text-xs`.** The drawing is wrapped in a container
204
+ * that scrolls sideways, and the drawing itself holds a floor of its own
205
+ * coordinate space, so a phone sees the whole graph at a legible size rather than
206
+ * the whole graph at six pixels. That is the same bargain `Table` strikes with
207
+ * seven columns, and it is a bargain rather than an accident because the
208
+ * alternative is a picture of a shape with nothing written on it.
209
+ *
210
+ * Positions are the caller's, and two labels crossing one another is still the
211
+ * caller's to settle. A relation naming a node that is not in `nodes` is dropped
212
+ * and counted on the element.
181
213
  */
182
214
  declare function PulseGraph({ nodes, relations, label, decorative, className, }: PulseGraphProps): import("react").JSX.Element;
183
215
  export { PulseGraph };
@@ -1 +1 @@
1
- {"version":3,"file":"pulse-graph.d.ts","sourceRoot":"","sources":["../../../src/components/ui/pulse-graph.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,CAAC,EAAE,MAAM,CAAA;IACT,0EAA0E;IAC1E,CAAC,EAAE,MAAM,CAAA;IACT;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAA;IACV,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gEAAgE;IAChE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,iDAAiD;AACjD,KAAK,WAAW,GAAG;IACjB;;;;;OAKG;IACH,KAAK,EAAE,SAAS,SAAS,EAAE,CAAA;IAC3B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GACvC,CACI;IACE;;;;OAIG;IACH,UAAU,EAAE,IAAI,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAA;CACd,GACD;IACE,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,KAAK,CAAA;CACnB,CACJ,CAAA;AAsHH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,iBAAS,UAAU,CAAC,EAClB,KAAK,EACL,SAAc,EACd,KAAK,EACL,UAAkB,EAClB,SAAS,GACV,EAAE,eAAe,+BA4MjB;AAED,OAAO,EAAE,UAAU,EAAE,CAAA"}
1
+ {"version":3,"file":"pulse-graph.d.ts","sourceRoot":"","sources":["../../../src/components/ui/pulse-graph.tsx"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,CAAC,EAAE,MAAM,CAAA;IACT,0EAA0E;IAC1E,CAAC,EAAE,MAAM,CAAA;IACT;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAA;IACV,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gEAAgE;IAChE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,iDAAiD;AACjD,KAAK,WAAW,GAAG;IACjB;;;;;OAKG;IACH,KAAK,EAAE,SAAS,SAAS,EAAE,CAAA;IAC3B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;IACpC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GACvC,CACI;IACE;;;;OAIG;IACH,UAAU,EAAE,IAAI,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAA;CACd,GACD;IACE,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,KAAK,CAAA;CACnB,CACJ,CAAA;AA2MH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FG;AACH,iBAAS,UAAU,CAAC,EAClB,KAAK,EACL,SAAc,EACd,KAAK,EACL,UAAkB,EAClB,SAAS,GACV,EAAE,eAAe,+BAoMjB;AAED,OAAO,EAAE,UAAU,EAAE,CAAA"}