@nanisoft/prism-ui 0.9.0 → 0.10.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/cta-01/cta.d.ts +28 -0
- package/dist/blocks/cta-01/cta.d.ts.map +1 -1
- package/dist/blocks/hero-01/hero.d.ts +78 -12
- package/dist/blocks/hero-01/hero.d.ts.map +1 -1
- package/dist/blocks/hero-01/hero.js +47 -15
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +30 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -0
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +14 -1
- package/dist/components/ui/cta-link.d.ts.map +1 -1
- package/dist/components/ui/cta-link.js +16 -1
- package/dist/components/ui/pulse-graph.d.ts +184 -0
- package/dist/components/ui/pulse-graph.d.ts.map +1 -0
- package/dist/components/ui/pulse-graph.js +212 -0
- package/dist/components/ui/pulse-series.d.ts +123 -0
- package/dist/components/ui/pulse-series.d.ts.map +1 -0
- package/dist/components/ui/pulse-series.js +94 -0
- package/dist/components/ui/signal-field.d.ts +66 -0
- package/dist/components/ui/signal-field.d.ts.map +1 -0
- package/dist/components/ui/signal-field.js +77 -0
- package/dist/styles.css +190 -0
- package/package.json +5 -4
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
/**
|
|
4
|
+
* The canvas the graph draws into, in its own user units.
|
|
5
|
+
*
|
|
6
|
+
* A fixed canvas rather than a viewBox derived from the caller's numbers, for
|
|
7
|
+
* the reason `Diagram` gives: type inside a scaled SVG is sized in the SVG's
|
|
8
|
+
* own user units, so a viewBox derived from a caller's coordinates would make
|
|
9
|
+
* the label size a function of how large a number the caller happened to use.
|
|
10
|
+
*/
|
|
11
|
+
const CANVAS_WIDTH = 640;
|
|
12
|
+
const CANVAS_HEIGHT = 360;
|
|
13
|
+
/**
|
|
14
|
+
* The band left empty around the fitted points, in user units.
|
|
15
|
+
*
|
|
16
|
+
* Wider than `Diagram`'s, and the extra is on the bottom rather than split. A
|
|
17
|
+
* pulse graph is read as a sequence running left to right, so the bottom band
|
|
18
|
+
* carries the rail, the lane marks and the stage names set under them, and all
|
|
19
|
+
* three are horizontal extents that a graph with more vertical spread would
|
|
20
|
+
* collide with. Reserving it once here is cheaper than every caller learning
|
|
21
|
+
* where not to put a node.
|
|
22
|
+
*/
|
|
23
|
+
const PADDING = 56;
|
|
24
|
+
/** The node mark's radius. A circle, so no corner radius for the pack to move. */
|
|
25
|
+
const NODE_RADIUS = 6;
|
|
26
|
+
/**
|
|
27
|
+
* Type sizes, in user units rather than in `rem`.
|
|
28
|
+
*
|
|
29
|
+
* The authored type scale is in `rem` and cannot be used inside a viewBox, where
|
|
30
|
+
* a font size is a coordinate and not a length on the page. These are geometry,
|
|
31
|
+
* the one thing Tailwind stays authoritative for.
|
|
32
|
+
*/
|
|
33
|
+
const NODE_NAME_SIZE = 13;
|
|
34
|
+
const NODE_NAME_OFFSET = 22;
|
|
35
|
+
const NODE_NOTE_SIZE = 10;
|
|
36
|
+
const NODE_NOTE_OFFSET = 35;
|
|
37
|
+
/**
|
|
38
|
+
* The rail the marker rides, in user units, measured from the bottom of the
|
|
39
|
+
* fitted area.
|
|
40
|
+
*
|
|
41
|
+
* It is a separate element from the edges because it is a different claim: the
|
|
42
|
+
* edges say what connects to what, and the rail says there is an order. A graph
|
|
43
|
+
* whose nodes are not in lanes has no rail, which is why this is drawn from the
|
|
44
|
+
* lane count rather than always being present.
|
|
45
|
+
*/
|
|
46
|
+
const RAIL_OFFSET = 74;
|
|
47
|
+
const RAIL_THICKNESS = 1.5;
|
|
48
|
+
/**
|
|
49
|
+
* The caller's points, fitted into the canvas with their aspect ratio kept and
|
|
50
|
+
* the leftover shared out evenly.
|
|
51
|
+
*
|
|
52
|
+
* A caller whose points span one axis only, or who passes a single node, gets
|
|
53
|
+
* the other axis centred rather than a division by zero.
|
|
54
|
+
*/
|
|
55
|
+
function fit(nodes) {
|
|
56
|
+
if (nodes.length === 0)
|
|
57
|
+
return () => ({ x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2 });
|
|
58
|
+
const xs = nodes.map((node) => node.x);
|
|
59
|
+
const ys = nodes.map((node) => node.y);
|
|
60
|
+
const minX = Math.min(...xs);
|
|
61
|
+
const minY = Math.min(...ys);
|
|
62
|
+
const spanX = Math.max(...xs) - minX;
|
|
63
|
+
const spanY = Math.max(...ys) - minY;
|
|
64
|
+
const roomX = CANVAS_WIDTH - PADDING * 2;
|
|
65
|
+
const roomY = CANVAS_HEIGHT - PADDING * 2;
|
|
66
|
+
const scale = Math.min(spanX === 0 ? Number.POSITIVE_INFINITY : roomX / spanX, spanY === 0 ? Number.POSITIVE_INFINITY : roomY / spanY);
|
|
67
|
+
const unit = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
68
|
+
const offsetX = PADDING + (roomX - spanX * unit) / 2 - minX * unit;
|
|
69
|
+
const offsetY = PADDING + (roomY - spanY * unit) / 2 - minY * unit;
|
|
70
|
+
return (node) => ({ x: node.x * unit + offsetX, y: node.y * unit + offsetY });
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The lanes present, in ascending order.
|
|
74
|
+
*
|
|
75
|
+
* A lane number is the caller's own indexing and is not required to start at
|
|
76
|
+
* zero or to be contiguous, so the set is derived and sorted rather than
|
|
77
|
+
* assumed. A caller that skipped a lane gets a rail with the lanes it named
|
|
78
|
+
* and no invented one between them, which is the only answer that does not
|
|
79
|
+
* draw a stage that is not there.
|
|
80
|
+
*/
|
|
81
|
+
function lanesOf(nodes) {
|
|
82
|
+
return [...new Set(nodes.flatMap((node) => (node.lane === undefined ? [] : [node.lane])))].sort((a, b) => a - b);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A three-stop stagger for the ambient cycles.
|
|
86
|
+
*
|
|
87
|
+
* A figure of nodes that all breathe on the same beat is a heartbeat, and a
|
|
88
|
+
* heartbeat is decoration: it is the same signal everywhere and so it carries
|
|
89
|
+
* no information about this system. Offsetting by lane means the rhythm runs
|
|
90
|
+
* along the rail instead, which is a claim about the sequence. The classes are
|
|
91
|
+
* the three the stylesheet publishes, chosen by lane position rather than by
|
|
92
|
+
* anything about the node, so a caller cannot accidentally author an
|
|
93
|
+
* unsynchronised field.
|
|
94
|
+
*/
|
|
95
|
+
function stagger(lane) {
|
|
96
|
+
return ['prism-ambient-delay-1', 'prism-ambient-delay-2', 'prism-ambient-delay-3'][Math.abs(lane) % 3];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A graph of named things, the relations between them, and a marker travelling
|
|
100
|
+
* the sequence.
|
|
101
|
+
*
|
|
102
|
+
* This is the Component the four NaniSoft product sites each lost when they
|
|
103
|
+
* moved off the retired line, rebuilt rather than recovered. Their old heroes
|
|
104
|
+
* drew the right thing, in the wrong way: a `<canvas>` with a
|
|
105
|
+
* `requestAnimationFrame` loop, its colours read out of the DOM once at mount,
|
|
106
|
+
* and numbers invented by a sine hash so the drawing would have something to
|
|
107
|
+
* show. Every part of that was a defect with a real cost, and this file is the
|
|
108
|
+
* argument for each of them in turn.
|
|
109
|
+
*
|
|
110
|
+
* **It is a server Component.** No `'use client'`, no hook, no effect, no
|
|
111
|
+
* context, no runtime. The motion is CSS on the server-rendered markup, so a
|
|
112
|
+
* consumer gets a running figure in a static export with zero bytes of
|
|
113
|
+
* JavaScript, and `check-client-budget.mjs` keeps it off the client roster. The
|
|
114
|
+
* old heroes needed a client runtime on a static-export site; this one needs
|
|
115
|
+
* nothing.
|
|
116
|
+
*
|
|
117
|
+
* **It is SVG, not a canvas, and that is the pack-boundary reason.** A canvas
|
|
118
|
+
* paints pixels it has already resolved, and a `data-pack` boundary is an
|
|
119
|
+
* attribute on an ancestor, so a resolved value does not move when the pack
|
|
120
|
+
* beneath it does. Every stroke and fill below names a semantic token as a
|
|
121
|
+
* utility, so a scoped pack boundary above the drawing restyles it through the
|
|
122
|
+
* cascade exactly as it restyles a heading. This is why `check-vector-ink.mjs`
|
|
123
|
+
* scans this file.
|
|
124
|
+
*
|
|
125
|
+
* **Nothing is hidden and nothing waits.** No rule sets `opacity: 0`; the
|
|
126
|
+
* figure is complete and fully legible at first paint; and the animation only
|
|
127
|
+
* moves things that are already visible. A reader with scripting off, a slow
|
|
128
|
+
* connection, a print stylesheet, or a crawler sees the whole drawing. The
|
|
129
|
+
* reduced-motion block in the stylesheet is one `animation: none`, and because
|
|
130
|
+
* every element's resting state is its full form, that reader gets the same
|
|
131
|
+
* figure, still. This is the `hidden-state` gate's requirement met by the
|
|
132
|
+
* design rather than by an exception, which is why a consumer needs no
|
|
133
|
+
* exception to enable it.
|
|
134
|
+
*
|
|
135
|
+
* **The motion is the token scale's.** Every `prism-ambient-*` class reads its
|
|
136
|
+
* length from `var(--ambient-*)`; nothing in this file states a duration, an
|
|
137
|
+
* easing, or a keyframe name in a shorthand. The one distance this file does set
|
|
138
|
+
* is a geometry length in user units, because the keyframe that consumes it
|
|
139
|
+
* translates by a distance and that distance is a property of this drawing's
|
|
140
|
+
* coordinates rather than a timing.
|
|
141
|
+
*
|
|
142
|
+
* **What it will not do.** It does not invent data. The old canvas filled itself
|
|
143
|
+
* with a sine hash, and every site that shipped it inherited a chart of numbers
|
|
144
|
+
* nobody collected; a reader who screenshotted one and read the axis got a lie
|
|
145
|
+
* with a real typeface on it. Here, a node is a node a caller named, an edge is
|
|
146
|
+
* an edge a caller declared, and an empty graph is an empty graph. The motion
|
|
147
|
+
* says a system is running. It does not say what the numbers were.
|
|
148
|
+
*
|
|
149
|
+
* **The rail.** Nodes carrying a `lane` are stages of one sequence, and the
|
|
150
|
+
* graph draws a rail under them with a marker travelling it, so the order is
|
|
151
|
+
* visible as an order rather than inferred from left-to-right spacing. Nodes
|
|
152
|
+
* with no lane are a field: no rail, no marker, and every node present from the
|
|
153
|
+
* first frame. A graph may have both, and the rail then describes only the lanes
|
|
154
|
+
* that exist.
|
|
155
|
+
*
|
|
156
|
+
* **Accessibility.** `role="img"` with the `label` the caller passes, the same
|
|
157
|
+
* contract `Diagram` holds. Everything inside a `role="img"` is presentational,
|
|
158
|
+
* so node names and notes are drawn for sighted readers and the surrounding
|
|
159
|
+
* sentence is what a screen reader gets. Pass `decorative` when that sentence
|
|
160
|
+
* already says what the drawing shows.
|
|
161
|
+
*
|
|
162
|
+
* Positions are the caller's. The Component fits and draws, and it does not lay
|
|
163
|
+
* out a graph, so it cannot know that two labels will not collide. A relation
|
|
164
|
+
* naming a node that is not in `nodes` is dropped and counted on the element.
|
|
165
|
+
*/
|
|
166
|
+
function PulseGraph({ nodes, relations = [], label, decorative = false, className, }) {
|
|
167
|
+
const place = fit(nodes);
|
|
168
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
169
|
+
const lanes = lanesOf(nodes);
|
|
170
|
+
const drawn = relations.flatMap((relation) => {
|
|
171
|
+
const from = byId.get(relation.from);
|
|
172
|
+
const to = byId.get(relation.to);
|
|
173
|
+
return from && to ? [{ relation, from: place(from), to: place(to) }] : [];
|
|
174
|
+
});
|
|
175
|
+
/**
|
|
176
|
+
* The rail's two ends, taken from the lanes that exist rather than from the
|
|
177
|
+
* canvas. A rail as wide as the canvas regardless of where the nodes are would
|
|
178
|
+
* say the sequence continues past its first and last stage, which is a claim
|
|
179
|
+
* about a caller's data that the caller did not make.
|
|
180
|
+
*/
|
|
181
|
+
const railNodes = nodes.filter((node) => node.lane !== undefined);
|
|
182
|
+
const railAt = railNodes.length
|
|
183
|
+
? {
|
|
184
|
+
x0: Math.min(...railNodes.map((node) => place(node).x)) - NODE_RADIUS - 10,
|
|
185
|
+
x1: Math.max(...railNodes.map((node) => place(node).x)) + NODE_RADIUS + 10,
|
|
186
|
+
}
|
|
187
|
+
: null;
|
|
188
|
+
const railY = CANVAS_HEIGHT - RAIL_OFFSET;
|
|
189
|
+
return (_jsxs("svg", { "data-slot": "pulse-graph", "data-unresolved-relations": relations.length - drawn.length, "data-lanes": lanes.length, viewBox: `0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`, xmlns: "http://www.w3.org/2000/svg", role: decorative ? undefined : 'img', "aria-label": decorative ? undefined : label, "aria-hidden": decorative || undefined, className: cn('h-auto w-full', className), children: [railAt ? (_jsxs("g", { "data-slot": "pulse-graph-rail", children: [_jsx("line", { "data-slot": "pulse-graph-rail-line", x1: railAt.x0, y1: railY, x2: railAt.x1, y2: railY, strokeWidth: RAIL_THICKNESS, className: "stroke-border" }), _jsx("g", { "data-slot": "pulse-graph-marker", className: "prism-ambient-travel prism-ambient-delay-1", style: { '--ambient-travel-distance': `${railAt.x1 - railAt.x0}px` }, children: _jsx("rect", { x: railAt.x0 - 2.5, y: railY - 2.5, width: 5, height: 5, rx: 2.5, className: "fill-brand-ink" }) })] })) : null, drawn.map(({ relation, from, to }) => (_jsxs("g", { "data-slot": "pulse-graph-relation", "data-relation": `${relation.from}-${relation.to}`, "data-carries": relation.carries || undefined, children: [_jsx("line", { x1: from.x, y1: from.y, x2: to.x, y2: to.y, strokeWidth: 1, strokeDasharray: relation.indirect ? '4 4' : undefined, className: "stroke-muted-foreground/50 fill-none" }), relation.carries
|
|
190
|
+
? (() => {
|
|
191
|
+
const dx = to.x - from.x;
|
|
192
|
+
const dy = to.y - from.y;
|
|
193
|
+
const length = Math.hypot(dx, dy);
|
|
194
|
+
if (length === 0)
|
|
195
|
+
return null;
|
|
196
|
+
// The head sits back from the target mark so it never overlaps
|
|
197
|
+
// it, and is scaled to the line's own length so a short edge and
|
|
198
|
+
// a long one get the same size head rather than a stretched one.
|
|
199
|
+
const inset = NODE_RADIUS + 4;
|
|
200
|
+
const ux = dx / length;
|
|
201
|
+
const uy = dy / length;
|
|
202
|
+
const hx = to.x - ux * inset;
|
|
203
|
+
const hy = to.y - uy * inset;
|
|
204
|
+
const head = 5;
|
|
205
|
+
return (_jsx("path", { "data-slot": "pulse-graph-flow", d: `M ${hx - uy * head} ${hy + ux * head} L ${hx} ${hy} L ${hx + uy * head} ${hy - ux * head}`, strokeWidth: 1.5, fill: "none", className: "stroke-brand-ink" }));
|
|
206
|
+
})()
|
|
207
|
+
: null] }, `${relation.from}-${relation.to}`))), nodes.map((node) => {
|
|
208
|
+
const at = place(node);
|
|
209
|
+
return (_jsxs("g", { "data-slot": "pulse-graph-node", "data-node": node.id, "data-emphasis": node.emphasis || undefined, "data-lane": node.lane, children: [node.emphasis ? (_jsx("circle", { "data-slot": "pulse-graph-halo", cx: at.x, cy: at.y, r: NODE_RADIUS, strokeWidth: 0, className: cn('fill-brand-ink/25', 'prism-ambient-pulse', stagger(node.lane ?? 0)) })) : null, _jsx("circle", { cx: at.x, cy: at.y, r: NODE_RADIUS, 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.note ? (_jsx("text", { x: at.x, y: at.y + NODE_NOTE_OFFSET, fontSize: NODE_NOTE_SIZE, textAnchor: "middle", className: "fill-muted-foreground font-mono", children: node.note })) : null] }, node.id));
|
|
210
|
+
})] }));
|
|
211
|
+
}
|
|
212
|
+
export { PulseGraph };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One column in a PulseSeries.
|
|
3
|
+
*
|
|
4
|
+
* `value` is a magnitude in the caller's own units and the Component scales it
|
|
5
|
+
* against the tallest column, so a caller passes the numbers it has rather than
|
|
6
|
+
* percentages of a maximum it had to compute first. The reason is honesty: a
|
|
7
|
+
* caller that pre-normalises has two chances to get the axis wrong, and a
|
|
8
|
+
* mislabelled axis is a lie with a typeface on it.
|
|
9
|
+
*
|
|
10
|
+
* `emphasis` marks the column the reader is meant to land on, and it is the one
|
|
11
|
+
* difference a column may carry. The emphasised column takes the brand ink and
|
|
12
|
+
* takes a label; every other column takes muted ink and takes none. Two
|
|
13
|
+
* emphasised columns are a caller's mistake rather than a second level, because
|
|
14
|
+
* a figure with two focal points has none.
|
|
15
|
+
*/
|
|
16
|
+
export type PulseBar = {
|
|
17
|
+
/** A key unique within the series. It is carried on the markup as `data-column`. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** The column's height, in the caller's own units. */
|
|
20
|
+
value: number;
|
|
21
|
+
/**
|
|
22
|
+
* The name printed under the column. Required on the emphasised column and
|
|
23
|
+
* optional elsewhere, because a label under every column of a wide series is
|
|
24
|
+
* the axis the caller would have drawn anyway, and this Component leaves room
|
|
25
|
+
* for it rather than making the caller draw one behind it.
|
|
26
|
+
*/
|
|
27
|
+
label?: string;
|
|
28
|
+
/** Marks the column the drawing is about. See the Brand Ink Rule. */
|
|
29
|
+
emphasis?: boolean;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* What both arms of `PulseSeriesProps` carry.
|
|
33
|
+
*
|
|
34
|
+
* The name is required unless the drawing is decorative, and it is forbidden
|
|
35
|
+
* when it is. That is a union rather than `label?: string` for the reason
|
|
36
|
+
* `Diagram` states: only a union lets the type system see the exception, because
|
|
37
|
+
* an optional label on one arm can only be optional on both.
|
|
38
|
+
*/
|
|
39
|
+
type PulseSeriesFigure = {
|
|
40
|
+
/**
|
|
41
|
+
* Hides the drawing from assistive technology and drops the name. Use it
|
|
42
|
+
* when the surrounding sentence already says what the series shows, and
|
|
43
|
+
* nothing inside the hidden tree is named either.
|
|
44
|
+
*/
|
|
45
|
+
decorative: true;
|
|
46
|
+
label?: never;
|
|
47
|
+
} | {
|
|
48
|
+
/** The name of the drawing, and the only thing a screen reader reads from it. */
|
|
49
|
+
label: string;
|
|
50
|
+
decorative?: false;
|
|
51
|
+
};
|
|
52
|
+
export type PulseSeriesProps = PulseSeriesFigure & {
|
|
53
|
+
/**
|
|
54
|
+
* The columns drawn, left to right in the order given. At least one: a series
|
|
55
|
+
* with nothing in it is a mistake in the caller's data rather than a state
|
|
56
|
+
* worth an empty message.
|
|
57
|
+
*/
|
|
58
|
+
bars: PulseBar[];
|
|
59
|
+
/**
|
|
60
|
+
* The line the columns rise from: the zero of the caller's units, named so a
|
|
61
|
+
* reader is told what the columns are counted from rather than left to infer
|
|
62
|
+
* it from a shape. Optional, and omitted rather than defaulted, because a
|
|
63
|
+
* default here is a claim about the caller's units.
|
|
64
|
+
*/
|
|
65
|
+
baseline?: string;
|
|
66
|
+
/**
|
|
67
|
+
* A reticle crossing the series once per cycle, marking which column is being
|
|
68
|
+
* read at this instant. This is the figure's only motion beyond the columns'
|
|
69
|
+
* own rise, and it is the part that makes a static bar chart read as a live
|
|
70
|
+
* view rather than a report.
|
|
71
|
+
*/
|
|
72
|
+
scan?: boolean;
|
|
73
|
+
/** Layout only, exactly as on every Component. */
|
|
74
|
+
className?: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* A series of columns, a reticle crossing them, and a baseline they rise from.
|
|
78
|
+
*
|
|
79
|
+
* This is the instrument the retired line drew on a canvas, rebuilt. Its
|
|
80
|
+
* predecessor read open interest out of a sine hash, so the drawing was of
|
|
81
|
+
* numbers that had never been collected, and a reader who screenshotted it and
|
|
82
|
+
* read the axis got a fabrication set in a real typeface. That defect is not
|
|
83
|
+
* repeated here and is not repeatable here: the columns are the caller's
|
|
84
|
+
* numbers, the tallest one sets the scale, and a caller with no numbers passes
|
|
85
|
+
* no bars.
|
|
86
|
+
*
|
|
87
|
+
* **A server Component with no runtime.** The rise and the scan are CSS on
|
|
88
|
+
* server-rendered markup, so a live view costs zero bytes of JavaScript and
|
|
89
|
+
* works in a static export. See `PulseGraph` for the full argument; the short
|
|
90
|
+
* form is that the old instrument needed a client runtime, a `requestAnimationFrame`
|
|
91
|
+
* loop and a colour read at mount, and this needs none of the three.
|
|
92
|
+
*
|
|
93
|
+
* **The columns are visible from the first frame.** The rise animation
|
|
94
|
+
* interpolates from the baseline, so at 0% the column has no height and the
|
|
95
|
+
* reader is looking at a baseline with labels on it. That is the one moment this
|
|
96
|
+
* figure is less informative than its resting state, and it lasts one cycle
|
|
97
|
+
* rather than persisting, it never hides a column permanently, and under
|
|
98
|
+
* `prefers-reduced-motion` it is not run at all. The figure is not gated on a
|
|
99
|
+
* script, an intersection or a scroll position, so a reader with scripting off
|
|
100
|
+
* and a print stylesheet both get every column at full height.
|
|
101
|
+
*
|
|
102
|
+
* **The scan is the honest part.** A reticle crossing the series says a column
|
|
103
|
+
* is being read at this instant, and it is drawn at a real position on the
|
|
104
|
+
* scale rather than sweeping an empty field: a sweep with nothing under it is
|
|
105
|
+
* atmosphere, and atmosphere was what the old heroes had too much of. Under
|
|
106
|
+
* reduced motion the scan is off and the columns stand, which is the correct
|
|
107
|
+
* still frame for a chart.
|
|
108
|
+
*
|
|
109
|
+
* **The emphasis is the caller's.** One column may be emphasised, and it is the
|
|
110
|
+
* one that takes the brand ink and the label. The reticle does not choose it,
|
|
111
|
+
* because the reticle is a claim about time and the emphasis is a claim about
|
|
112
|
+
* importance, and a component that conflated the two would be moving the
|
|
113
|
+
* reader's attention on a schedule.
|
|
114
|
+
*
|
|
115
|
+
* **Accessibility.** `role="img"` and the caller's `label`, the same contract
|
|
116
|
+
* the rest of the drawing set holds. The labels under columns are inside a
|
|
117
|
+
* `role="img"` and so are presentational; the surrounding sentence is what a
|
|
118
|
+
* screen reader gets. Pass `decorative` when it already says what the drawing
|
|
119
|
+
* shows.
|
|
120
|
+
*/
|
|
121
|
+
declare function PulseSeries({ bars, baseline, scan, label, decorative, className, }: PulseSeriesProps): import("react").JSX.Element;
|
|
122
|
+
export { PulseSeries };
|
|
123
|
+
//# sourceMappingURL=pulse-series.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pulse-series.d.ts","sourceRoot":"","sources":["../../../src/components/ui/pulse-series.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,oFAAoF;IACpF,EAAE,EAAE,MAAM,CAAA;IACV,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED;;;;;;;GAOG;AACH,KAAK,iBAAiB,GAClB;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,CAAA;AAEL,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG;IACjD;;;;OAIG;IACD,IAAI,EAAE,QAAQ,EAAE,CAAA;IAChB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAoCH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,iBAAS,WAAW,CAAC,EACnB,IAAI,EACJ,QAAQ,EACR,IAAY,EACZ,KAAK,EACL,UAAkB,EAClB,SAAS,GACV,EAAE,gBAAgB,+BAsIlB;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
/**
|
|
4
|
+
* The canvas the series draws into, in its own user units.
|
|
5
|
+
*
|
|
6
|
+
* Shorter than `PulseGraph`'s and for the reason a bar chart is not a graph: a
|
|
7
|
+
* series reads its height against its baseline, so a canvas with more vertical
|
|
8
|
+
* room than it needs pushes the baseline up and makes every column look
|
|
9
|
+
* shorter than it is.
|
|
10
|
+
*/
|
|
11
|
+
const CANVAS_WIDTH = 640;
|
|
12
|
+
const CANVAS_HEIGHT = 300;
|
|
13
|
+
/**
|
|
14
|
+
* The band left empty around the columns, in user units.
|
|
15
|
+
*
|
|
16
|
+
* The bottom band carries the baseline and the labels, so it is generous, and
|
|
17
|
+
* the top band is small because a column that reaches the top of the canvas has
|
|
18
|
+
* no headroom to breathe in and reads as clipped rather than as tall.
|
|
19
|
+
*/
|
|
20
|
+
const PADDING = 52;
|
|
21
|
+
const PADDING_TOP = 28;
|
|
22
|
+
/** The widest a column is drawn, in user units, before the series narrows them. */
|
|
23
|
+
const MAX_BAR_WIDTH = 18;
|
|
24
|
+
/** The gap between two columns, as a fraction of a column's own width. */
|
|
25
|
+
const BAR_GAP = 0.45;
|
|
26
|
+
/**
|
|
27
|
+
* Type sizes, in user units rather than in `rem`, for the reason every figure in
|
|
28
|
+
* this package states: a font size inside a viewBox is a coordinate.
|
|
29
|
+
*/
|
|
30
|
+
const LABEL_SIZE = 11;
|
|
31
|
+
const BASELINE_SIZE = 10;
|
|
32
|
+
/**
|
|
33
|
+
* A series of columns, a reticle crossing them, and a baseline they rise from.
|
|
34
|
+
*
|
|
35
|
+
* This is the instrument the retired line drew on a canvas, rebuilt. Its
|
|
36
|
+
* predecessor read open interest out of a sine hash, so the drawing was of
|
|
37
|
+
* numbers that had never been collected, and a reader who screenshotted it and
|
|
38
|
+
* read the axis got a fabrication set in a real typeface. That defect is not
|
|
39
|
+
* repeated here and is not repeatable here: the columns are the caller's
|
|
40
|
+
* numbers, the tallest one sets the scale, and a caller with no numbers passes
|
|
41
|
+
* no bars.
|
|
42
|
+
*
|
|
43
|
+
* **A server Component with no runtime.** The rise and the scan are CSS on
|
|
44
|
+
* server-rendered markup, so a live view costs zero bytes of JavaScript and
|
|
45
|
+
* works in a static export. See `PulseGraph` for the full argument; the short
|
|
46
|
+
* form is that the old instrument needed a client runtime, a `requestAnimationFrame`
|
|
47
|
+
* loop and a colour read at mount, and this needs none of the three.
|
|
48
|
+
*
|
|
49
|
+
* **The columns are visible from the first frame.** The rise animation
|
|
50
|
+
* interpolates from the baseline, so at 0% the column has no height and the
|
|
51
|
+
* reader is looking at a baseline with labels on it. That is the one moment this
|
|
52
|
+
* figure is less informative than its resting state, and it lasts one cycle
|
|
53
|
+
* rather than persisting, it never hides a column permanently, and under
|
|
54
|
+
* `prefers-reduced-motion` it is not run at all. The figure is not gated on a
|
|
55
|
+
* script, an intersection or a scroll position, so a reader with scripting off
|
|
56
|
+
* and a print stylesheet both get every column at full height.
|
|
57
|
+
*
|
|
58
|
+
* **The scan is the honest part.** A reticle crossing the series says a column
|
|
59
|
+
* is being read at this instant, and it is drawn at a real position on the
|
|
60
|
+
* scale rather than sweeping an empty field: a sweep with nothing under it is
|
|
61
|
+
* atmosphere, and atmosphere was what the old heroes had too much of. Under
|
|
62
|
+
* reduced motion the scan is off and the columns stand, which is the correct
|
|
63
|
+
* still frame for a chart.
|
|
64
|
+
*
|
|
65
|
+
* **The emphasis is the caller's.** One column may be emphasised, and it is the
|
|
66
|
+
* one that takes the brand ink and the label. The reticle does not choose it,
|
|
67
|
+
* because the reticle is a claim about time and the emphasis is a claim about
|
|
68
|
+
* importance, and a component that conflated the two would be moving the
|
|
69
|
+
* reader's attention on a schedule.
|
|
70
|
+
*
|
|
71
|
+
* **Accessibility.** `role="img"` and the caller's `label`, the same contract
|
|
72
|
+
* the rest of the drawing set holds. The labels under columns are inside a
|
|
73
|
+
* `role="img"` and so are presentational; the surrounding sentence is what a
|
|
74
|
+
* screen reader gets. Pass `decorative` when it already says what the drawing
|
|
75
|
+
* shows.
|
|
76
|
+
*/
|
|
77
|
+
function PulseSeries({ bars, baseline, scan = false, label, decorative = false, className, }) {
|
|
78
|
+
const roomX = CANVAS_WIDTH - PADDING * 2;
|
|
79
|
+
const roomY = CANVAS_HEIGHT - PADDING - PADDING_TOP;
|
|
80
|
+
const baselineY = PADDING + roomY;
|
|
81
|
+
const tallest = Math.max(...bars.map((bar) => bar.value), 0);
|
|
82
|
+
const slot = bars.length ? roomX / bars.length : roomX;
|
|
83
|
+
const barWidth = Math.max(2, Math.min(MAX_BAR_WIDTH, slot * (1 / (1 + BAR_GAP))));
|
|
84
|
+
return (_jsxs("svg", { "data-slot": "pulse-series", "data-columns": bars.length, "data-scan": scan || undefined, viewBox: `0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`, xmlns: "http://www.w3.org/2000/svg", role: decorative ? undefined : 'img', "aria-label": decorative ? undefined : label, "aria-hidden": decorative || undefined, className: cn('h-auto w-full', className), children: [scan ? (_jsx("g", { "data-slot": "pulse-series-scan", "aria-hidden": true, children: _jsxs("g", { className: "prism-ambient-scan", style: { '--ambient-scan-distance': `${roomX + slot}px` }, children: [_jsx("rect", { "data-slot": "pulse-series-scan-wash", x: PADDING - slot, y: PADDING_TOP - 12, width: slot, height: roomY + 12, className: "fill-brand-ink/10" }), _jsx("line", { "data-slot": "pulse-series-scan-line", x1: 0, y1: PADDING_TOP - 12, x2: 0, y2: baselineY, strokeWidth: 1, className: "stroke-brand-ink/60" })] }) })) : null, bars.map((bar, index) => {
|
|
85
|
+
const centre = PADDING + slot * (index + 0.5);
|
|
86
|
+
// A column of value zero is drawn as a hairline rather than as nothing,
|
|
87
|
+
// because a gap in a series is a missing reading and a missing reading
|
|
88
|
+
// that looks identical to a value of zero is a data claim the drawing
|
|
89
|
+
// cannot support.
|
|
90
|
+
const height = tallest === 0 ? 1 : Math.max(1, (bar.value / tallest) * roomY);
|
|
91
|
+
return (_jsxs("g", { "data-slot": "pulse-series-column", "data-column": bar.id, "data-emphasis": bar.emphasis || undefined, children: [_jsx("rect", { x: centre - barWidth / 2, y: baselineY - height, width: barWidth, height: height, rx: Math.min(2, barWidth / 2), className: cn('origin-bottom', 'prism-ambient-rise', bar.emphasis ? 'fill-brand-ink' : 'fill-muted-foreground/35', `prism-ambient-delay-${(index % 3) + 1}`) }), bar.label ? (_jsx("text", { x: centre, y: baselineY + 18, fontSize: LABEL_SIZE, textAnchor: "middle", className: cn('font-mono', bar.emphasis ? 'fill-foreground' : 'fill-muted-foreground'), children: bar.label })) : null] }, bar.id));
|
|
92
|
+
}), _jsxs("g", { "data-slot": "pulse-series-baseline", children: [_jsx("line", { x1: PADDING - 12, y1: baselineY, x2: CANVAS_WIDTH - PADDING + 12, y2: baselineY, strokeWidth: 1, className: "stroke-border" }), baseline ? (_jsx("text", { x: PADDING - 12, y: baselineY - 8, fontSize: BASELINE_SIZE, textAnchor: "start", className: "fill-muted-foreground font-mono", children: baseline })) : null] })] }));
|
|
93
|
+
}
|
|
94
|
+
export { PulseSeries };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many marks a field draws, and how they are arranged.
|
|
3
|
+
*
|
|
4
|
+
* A field is atmosphere, and atmosphere is the one thing in this system that is
|
|
5
|
+
* allowed to be a number rather than data. The count is a prop rather than a
|
|
6
|
+
* constant because a hero panel beside a two-column hero and a full-bleed
|
|
7
|
+
* background behind a title want very different densities, and a fixed count
|
|
8
|
+
* would have one of them too sparse or the other too busy.
|
|
9
|
+
*/
|
|
10
|
+
export type SignalFieldProps = {
|
|
11
|
+
/**
|
|
12
|
+
* How many marks the field draws. Read as a suggestion and clamped to a small
|
|
13
|
+
* range, because a field is a texture and a texture with four hundred marks is
|
|
14
|
+
* a chart that lost its labels.
|
|
15
|
+
*/
|
|
16
|
+
count?: number;
|
|
17
|
+
/**
|
|
18
|
+
* A mark the drawing is about, given as a fraction of the field. The field
|
|
19
|
+
* centres the emphasis and dims the rest, which is what makes a field read as
|
|
20
|
+
* a system with a focus rather than as scattered dots.
|
|
21
|
+
*/
|
|
22
|
+
emphasisAt?: number;
|
|
23
|
+
/** The name of the drawing, or `decorative`. See `PulseGraphProps`. */
|
|
24
|
+
label?: string;
|
|
25
|
+
decorative?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the marks drift. Off by default: a field behind a reading passage
|
|
28
|
+
* is the worst place for movement a reader did not ask for, and the drift is
|
|
29
|
+
* the first thing a reader with vestibular sensitivity will ask to have gone.
|
|
30
|
+
*/
|
|
31
|
+
drift?: boolean;
|
|
32
|
+
/** Layout only, exactly as on every Component. */
|
|
33
|
+
className?: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A field of marks: the atmosphere a figure is drawn over, and the one thing in
|
|
37
|
+
* this system that is allowed to be nothing more than texture.
|
|
38
|
+
*
|
|
39
|
+
* The retired line drew this as a canvas behind the hero type, and the defect
|
|
40
|
+
* was never the idea: it was that the canvas was opaque, animated continuously,
|
|
41
|
+
* and had to be redrawn by a `requestAnimationFrame` loop to keep a decorative
|
|
42
|
+
* sweep moving. This is the same idea with the three costs removed. It is SVG,
|
|
43
|
+
* so it scales, prints and costs no runtime; it is `aria-hidden` by default,
|
|
44
|
+
* because a field of dots is not information and a screen reader being made to
|
|
45
|
+
* read one is a cost with no reader attached; and its movement is off unless a
|
|
46
|
+
* caller asks for it.
|
|
47
|
+
*
|
|
48
|
+
* **It is atmosphere, and it is labelled as such.** The reason it exists is that
|
|
49
|
+
* a hero with a hard flat ground behind it reads as a document, and a hero with
|
|
50
|
+
* a field behind it reads as a product with a surface. Everything else about it
|
|
51
|
+
* is restraint: no mark is emphasised by default, no two marks are the same
|
|
52
|
+
* colour, and nothing in the field moves unless `drift` is set. It is the
|
|
53
|
+
* weakest mark in the system and the easiest one to overdo, and the props are
|
|
54
|
+
* shaped to make the restrained version the easy one.
|
|
55
|
+
*
|
|
56
|
+
* **It never carries the meaning.** A field is `aria-hidden` unless a caller
|
|
57
|
+
* deliberately names it, and the default is that it names nothing, because a
|
|
58
|
+
* caller reaching for this has atmosphere and not data. When a figure needs to
|
|
59
|
+
* say what it is showing, that figure is `PulseGraph` or `PulseSeries`, both of
|
|
60
|
+
* which require a name and both of which a screen reader can be given.
|
|
61
|
+
*
|
|
62
|
+
* **It is a server Component.** No hook, no client runtime, nothing to hydrate.
|
|
63
|
+
*/
|
|
64
|
+
declare function SignalField({ count, emphasisAt, label, decorative, drift, className, }: SignalFieldProps): import("react").JSX.Element;
|
|
65
|
+
export { SignalField };
|
|
66
|
+
//# sourceMappingURL=signal-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal-field.d.ts","sourceRoot":"","sources":["../../../src/components/ui/signal-field.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAwCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,iBAAS,WAAW,CAAC,EACnB,KAAU,EACV,UAAU,EACV,KAAK,EACL,UAAiB,EACjB,KAAa,EACb,SAAS,GACV,EAAE,gBAAgB,+BA0ClB;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
/**
|
|
4
|
+
* The canvas the field draws into, in its own user units.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately oversized relative to the panel it usually fills, because a field
|
|
7
|
+
* that ends where the panel ends is a grid and a grid reads as content. The
|
|
8
|
+
* marks are placed on a low-discrepancy walk rather than at random, so no two
|
|
9
|
+
* fields ever look alike and none of them looks hand-scattered, while remaining
|
|
10
|
+
* reproducible between server render and client hydration. A field that changed
|
|
11
|
+
* on every render would be a hydration mismatch, which is a correctness bug
|
|
12
|
+
* dressed as variety.
|
|
13
|
+
*/
|
|
14
|
+
const CANVAS_WIDTH = 1200;
|
|
15
|
+
const CANVAS_HEIGHT = 700;
|
|
16
|
+
/** The largest a mark is drawn, in user units. */
|
|
17
|
+
const MAX_MARK = 3;
|
|
18
|
+
/**
|
|
19
|
+
* The marks, placed by a golden-angle walk.
|
|
20
|
+
*
|
|
21
|
+
* A golden angle is the one arrangement that looks uncorrelated while being
|
|
22
|
+
* fully determined by its index, which is what this needs: reproducible
|
|
23
|
+
* without a seed, and evenly spread without a grid. The modulus keeps every
|
|
24
|
+
* mark inside the canvas without a rejection loop, so the count is honoured
|
|
25
|
+
* exactly rather than approximately.
|
|
26
|
+
*/
|
|
27
|
+
function marks(count) {
|
|
28
|
+
const golden = 2.399963229728653;
|
|
29
|
+
return Array.from({ length: count }, (_, index) => {
|
|
30
|
+
const t = (index + 0.5) / count;
|
|
31
|
+
return {
|
|
32
|
+
x: ((index * golden) % 1) * CANVAS_WIDTH,
|
|
33
|
+
y: t * CANVAS_HEIGHT,
|
|
34
|
+
r: MAX_MARK * (0.5 + ((index * 7919) % 5) / 10),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A field of marks: the atmosphere a figure is drawn over, and the one thing in
|
|
40
|
+
* this system that is allowed to be nothing more than texture.
|
|
41
|
+
*
|
|
42
|
+
* The retired line drew this as a canvas behind the hero type, and the defect
|
|
43
|
+
* was never the idea: it was that the canvas was opaque, animated continuously,
|
|
44
|
+
* and had to be redrawn by a `requestAnimationFrame` loop to keep a decorative
|
|
45
|
+
* sweep moving. This is the same idea with the three costs removed. It is SVG,
|
|
46
|
+
* so it scales, prints and costs no runtime; it is `aria-hidden` by default,
|
|
47
|
+
* because a field of dots is not information and a screen reader being made to
|
|
48
|
+
* read one is a cost with no reader attached; and its movement is off unless a
|
|
49
|
+
* caller asks for it.
|
|
50
|
+
*
|
|
51
|
+
* **It is atmosphere, and it is labelled as such.** The reason it exists is that
|
|
52
|
+
* a hero with a hard flat ground behind it reads as a document, and a hero with
|
|
53
|
+
* a field behind it reads as a product with a surface. Everything else about it
|
|
54
|
+
* is restraint: no mark is emphasised by default, no two marks are the same
|
|
55
|
+
* colour, and nothing in the field moves unless `drift` is set. It is the
|
|
56
|
+
* weakest mark in the system and the easiest one to overdo, and the props are
|
|
57
|
+
* shaped to make the restrained version the easy one.
|
|
58
|
+
*
|
|
59
|
+
* **It never carries the meaning.** A field is `aria-hidden` unless a caller
|
|
60
|
+
* deliberately names it, and the default is that it names nothing, because a
|
|
61
|
+
* caller reaching for this has atmosphere and not data. When a figure needs to
|
|
62
|
+
* say what it is showing, that figure is `PulseGraph` or `PulseSeries`, both of
|
|
63
|
+
* which require a name and both of which a screen reader can be given.
|
|
64
|
+
*
|
|
65
|
+
* **It is a server Component.** No hook, no client runtime, nothing to hydrate.
|
|
66
|
+
*/
|
|
67
|
+
function SignalField({ count = 28, emphasisAt, label, decorative = true, drift = false, className, }) {
|
|
68
|
+
const placed = marks(Math.max(4, Math.min(120, Math.round(count))));
|
|
69
|
+
const emphasised = emphasisAt === undefined ? -1 : Math.round(emphasisAt * (placed.length - 1));
|
|
70
|
+
return (_jsx("svg", { "data-slot": "signal-field", "data-marks": placed.length, viewBox: `0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`, xmlns: "http://www.w3.org/2000/svg", role: decorative ? undefined : 'img', "aria-label": decorative ? undefined : label, "aria-hidden": decorative || undefined, preserveAspectRatio: "xMidYMid slice", className: cn('h-full w-full', className), children: placed.map((mark, index) => (_jsx("circle", { "data-slot": "signal-field-mark", "data-emphasis": index === emphasised || undefined, cx: mark.x, cy: mark.y, r: index === emphasised ? mark.r * 1.9 : mark.r, strokeWidth: 0, className: cn(index === emphasised ? 'fill-brand-ink/45' : 'fill-foreground/12', drift && index % 3 === 0 ? 'prism-ambient-drift' : undefined, drift && index % 3 === 1 ? 'prism-ambient-delay-1' : undefined, drift && index % 3 === 2 ? 'prism-ambient-delay-2' : undefined), style: drift
|
|
71
|
+
? {
|
|
72
|
+
'--ambient-drift-x': `${(index % 2 === 0 ? 1 : -1) * (0.4 + (index % 5) * 0.15)}rem`,
|
|
73
|
+
'--ambient-drift-y': `${(index % 3 === 0 ? 1 : -1) * 0.35}rem`,
|
|
74
|
+
}
|
|
75
|
+
: undefined }))) }));
|
|
76
|
+
}
|
|
77
|
+
export { SignalField };
|