@caqophony/ui 0.3.1 → 0.5.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.
@@ -0,0 +1,1066 @@
1
+ 'use client';
2
+ import {
3
+ IntentBadge
4
+ } from "./chunk-44GEHNZP.js";
5
+ import {
6
+ CONTENT_GUTTER,
7
+ Icon,
8
+ ScrollHintXMark
9
+ } from "./chunk-ID7VBGJA.js";
10
+ import {
11
+ cn
12
+ } from "./chunk-DP5WQAPO.js";
13
+
14
+ // src/material/sky.tsx
15
+ import {
16
+ Children,
17
+ createContext,
18
+ isValidElement,
19
+ useContext
20
+ } from "react";
21
+
22
+ // src/material/tones.ts
23
+ var FILL = {
24
+ "night-0": "fill-night-0",
25
+ "night-1": "fill-night-1",
26
+ "night-2": "fill-night-2",
27
+ "sky-0": "fill-sky-0",
28
+ "sky-1": "fill-sky-1",
29
+ "sky-2": "fill-sky-2",
30
+ "sky-3": "fill-sky-3",
31
+ "sky-4": "fill-sky-4",
32
+ glow: "fill-glow",
33
+ "glow-soft": "fill-glow-soft",
34
+ "glow-pale": "fill-glow-pale",
35
+ canvas: "fill-canvas",
36
+ "mist-1": "fill-mist-1",
37
+ "mist-2": "fill-mist-2",
38
+ "mist-3": "fill-mist-3",
39
+ paper: "fill-paper",
40
+ ink: "fill-ink",
41
+ page: "fill-background"
42
+ };
43
+ var STROKE = {
44
+ "night-0": "stroke-night-0",
45
+ "night-1": "stroke-night-1",
46
+ "night-2": "stroke-night-2",
47
+ "sky-0": "stroke-sky-0",
48
+ "sky-1": "stroke-sky-1",
49
+ "sky-2": "stroke-sky-2",
50
+ "sky-3": "stroke-sky-3",
51
+ "sky-4": "stroke-sky-4",
52
+ glow: "stroke-glow",
53
+ "glow-soft": "stroke-glow-soft",
54
+ "glow-pale": "stroke-glow-pale",
55
+ canvas: "stroke-canvas",
56
+ "mist-1": "stroke-mist-1",
57
+ "mist-2": "stroke-mist-2",
58
+ "mist-3": "stroke-mist-3",
59
+ paper: "stroke-paper",
60
+ ink: "stroke-ink",
61
+ page: "stroke-background"
62
+ };
63
+ var BG = {
64
+ "night-0": "bg-night-0",
65
+ "night-1": "bg-night-1",
66
+ "night-2": "bg-night-2",
67
+ "sky-0": "bg-sky-0",
68
+ "sky-1": "bg-sky-1",
69
+ "sky-2": "bg-sky-2",
70
+ "sky-3": "bg-sky-3",
71
+ "sky-4": "bg-sky-4",
72
+ glow: "bg-glow",
73
+ "glow-soft": "bg-glow-soft",
74
+ "glow-pale": "bg-glow-pale",
75
+ canvas: "bg-canvas",
76
+ "mist-1": "bg-mist-1",
77
+ "mist-2": "bg-mist-2",
78
+ "mist-3": "bg-mist-3",
79
+ paper: "bg-paper",
80
+ ink: "bg-ink",
81
+ page: "bg-background"
82
+ };
83
+
84
+ // src/material/art.tsx
85
+ import { Fragment, jsx } from "react/jsx-runtime";
86
+ var ANCHOR = {
87
+ top: "xMidYMin",
88
+ center: "xMidYMid",
89
+ bottom: "xMidYMax",
90
+ right: "xMaxYMid"
91
+ };
92
+ var preserveOf = (anchor, fit = "cover") => `${ANCHOR[anchor]} ${fit === "contain" ? "meet" : "slice"}`;
93
+ var SHOWN = {
94
+ phone: "size-full md:hidden",
95
+ desktop: "hidden size-full md:block",
96
+ stack: "size-full lg:hidden",
97
+ side: "hidden size-full lg:block"
98
+ };
99
+ var frameClass = (only) => only ? SHOWN[only] : "size-full";
100
+
101
+ // src/material/scene-measures.ts
102
+ var FIELD_UNITS = "field units";
103
+ var WIDTH = { of: "width", times: 1 };
104
+ var HEIGHT = { of: "height", times: 1 };
105
+ var ACROSS = {
106
+ unit: FIELD_UNITS,
107
+ whole: false,
108
+ min: 0,
109
+ minOpen: false,
110
+ max: WIDTH
111
+ };
112
+ var DOWN = {
113
+ unit: FIELD_UNITS,
114
+ whole: false,
115
+ min: 0,
116
+ minOpen: false,
117
+ max: HEIGHT
118
+ };
119
+ var SEED = {
120
+ unit: "a seed: the same one draws the same shape",
121
+ whole: true,
122
+ min: 0,
123
+ minOpen: false,
124
+ max: 2147483646
125
+ };
126
+ var SCENE_MEASURES = {
127
+ SkyField: {
128
+ width: {
129
+ unit: "field units, the frame's own coordinates",
130
+ whole: false,
131
+ min: 0,
132
+ minOpen: true,
133
+ max: null
134
+ },
135
+ height: {
136
+ unit: "field units, the frame's own coordinates",
137
+ whole: false,
138
+ min: 0,
139
+ minOpen: true,
140
+ max: null
141
+ }
142
+ },
143
+ Halo: {
144
+ cx: ACROSS,
145
+ cy: DOWN,
146
+ "rings.r": {
147
+ unit: FIELD_UNITS,
148
+ whole: false,
149
+ min: 0,
150
+ minOpen: true,
151
+ max: WIDTH
152
+ }
153
+ },
154
+ CloudBank: {
155
+ y: DOWN,
156
+ /** Below 0.1 a radius rounds to 0.0 in the path's one decimal. */
157
+ min: {
158
+ unit: FIELD_UNITS,
159
+ whole: false,
160
+ min: 0.1,
161
+ minOpen: false,
162
+ max: HEIGHT
163
+ },
164
+ max: {
165
+ unit: FIELD_UNITS,
166
+ whole: false,
167
+ min: 0.1,
168
+ minOpen: false,
169
+ max: HEIGHT
170
+ },
171
+ climb: {
172
+ unit: FIELD_UNITS,
173
+ whole: false,
174
+ min: { of: "height", times: -1 },
175
+ minOpen: false,
176
+ max: HEIGHT
177
+ },
178
+ seed: SEED
179
+ },
180
+ Ridge: {
181
+ y: DOWN,
182
+ rise: DOWN,
183
+ swells: {
184
+ unit: "swells across the field",
185
+ whole: true,
186
+ min: 1,
187
+ minOpen: false,
188
+ max: 512
189
+ },
190
+ seed: SEED
191
+ },
192
+ Stars: {
193
+ depth: {
194
+ unit: "a fraction of the field's height",
195
+ whole: false,
196
+ min: 0,
197
+ minOpen: true,
198
+ max: 1
199
+ },
200
+ count: {
201
+ unit: "stars",
202
+ whole: true,
203
+ min: 0,
204
+ minOpen: false,
205
+ max: 5e3
206
+ },
207
+ seed: SEED
208
+ },
209
+ RouteLine: {
210
+ "points.x": ACROSS,
211
+ "points.y": DOWN,
212
+ labelSize: {
213
+ unit: FIELD_UNITS,
214
+ whole: false,
215
+ min: 0,
216
+ minOpen: true,
217
+ max: HEIGHT
218
+ }
219
+ }
220
+ };
221
+ var boundText = (b) => typeof b === "number" ? String(b) : b.times === 1 ? `the field's ${b.of}` : b.times === -1 ? `minus the field's ${b.of}` : `${b.times} x the field's ${b.of}`;
222
+ function measureText(m) {
223
+ const low = boundText(m.min);
224
+ const range = m.max === null ? `${m.minOpen ? "above" : "at least"} ${low}` : m.minOpen ? `above ${low}, at most ${boundText(m.max)}` : `from ${low} to ${boundText(m.max)}`;
225
+ return `${m.unit}: ${m.whole ? `a whole number ${range}` : range}`;
226
+ }
227
+ var boundIn = (b, field) => {
228
+ if (typeof b === "number") return b;
229
+ if (field === null)
230
+ throw new Error(
231
+ `a range bound of the field's ${b.of} needs a field to read it in`
232
+ );
233
+ return field[b.of] * b.times;
234
+ };
235
+ function measured(piece, key, name, value, field) {
236
+ const table = SCENE_MEASURES[piece];
237
+ const m = table[key];
238
+ if (m === void 0)
239
+ throw new Error(`${piece}: \`${key}\` has no measure in SCENE_MEASURES`);
240
+ const low = boundIn(m.min, field);
241
+ const high = m.max === null ? Number.POSITIVE_INFINITY : boundIn(m.max, field);
242
+ const inside = Number.isFinite(value) && (m.minOpen ? value > low : value >= low) && value <= high && (!m.whole || Number.isInteger(value));
243
+ if (!inside)
244
+ throw new Error(
245
+ `${piece}: \`${name}\` is ${measureText(m)} (got ${value})`
246
+ );
247
+ }
248
+
249
+ // src/material/sky.tsx
250
+ import { Fragment as Fragment2, jsx as jsx2, jsxs } from "react/jsx-runtime";
251
+ function measuredAll(piece, field, facts) {
252
+ for (const [name, value] of Object.entries(facts))
253
+ if (value !== void 0) measured(piece, name, name, value, field);
254
+ }
255
+ function sized(piece, field) {
256
+ for (const [name, value] of Object.entries({
257
+ width: field.width,
258
+ height: field.height
259
+ }))
260
+ if (!Number.isFinite(value) || value <= 0)
261
+ throw new Error(
262
+ `${piece}: \`${name}\` must be a finite number above 0 (got ${value})`
263
+ );
264
+ }
265
+ var MAX_BUMPS = 2e3;
266
+ var BLEED_BOTH_SIDES = {
267
+ left: "bleed",
268
+ right: "bleed",
269
+ down: false
270
+ };
271
+ var SceneCopyContext = createContext(null);
272
+ var GroundContext = createContext(null);
273
+ var FieldContext = createContext(null);
274
+ var LayerContext = createContext("pieces");
275
+ function useField(piece) {
276
+ const field = useContext(FieldContext);
277
+ if (field === null)
278
+ throw new Error(
279
+ `${piece}: a scene piece draws inside a SkyField, which gives it the field's size; this one has no SkyField around it`
280
+ );
281
+ return field;
282
+ }
283
+ function reachOf(copy, only) {
284
+ if (copy === null) return BLEED_BOTH_SIDES;
285
+ if (only === "stack" || only === "phone")
286
+ return { left: "bleed", right: "bleed", down: true };
287
+ return copy === "start" ? { left: "close", right: "bleed", down: true } : { left: "bleed", right: "close", down: true };
288
+ }
289
+ function SkyField({
290
+ width,
291
+ height,
292
+ anchor,
293
+ fit,
294
+ only,
295
+ ground,
296
+ children
297
+ }) {
298
+ measuredAll("SkyField", null, { width, height });
299
+ const copy = useContext(SceneCopyContext);
300
+ const under = useContext(GroundContext);
301
+ const routes = Children.toArray(children).filter(
302
+ (c) => isValidElement(c) && c.type === RouteLine
303
+ );
304
+ const field = {
305
+ width,
306
+ height,
307
+ reach: reachOf(copy, only),
308
+ ground: ground === "none" ? under : ground,
309
+ routes: new Set(
310
+ routes.map(
311
+ (r) => isValidElement(r) ? r.props.points : r
312
+ )
313
+ )
314
+ };
315
+ return /* @__PURE__ */ jsxs(
316
+ "svg",
317
+ {
318
+ viewBox: `0 0 ${width} ${height}`,
319
+ preserveAspectRatio: preserveOf(anchor, fit),
320
+ "aria-hidden": "true",
321
+ className: cn("absolute inset-0", frameClass(only)),
322
+ children: [
323
+ ground !== "none" && /* @__PURE__ */ jsx2("rect", { width, height, className: FILL[ground] }),
324
+ /* @__PURE__ */ jsx2(GroundContext.Provider, { value: field.ground, children: /* @__PURE__ */ jsxs(FieldContext.Provider, { value: field, children: [
325
+ children,
326
+ routes.length > 0 && /* @__PURE__ */ jsx2("g", { "data-route-labels": "", children: /* @__PURE__ */ jsx2(LayerContext.Provider, { value: "labels", children: routes }) })
327
+ ] }) })
328
+ ]
329
+ }
330
+ );
331
+ }
332
+ function Halo({ cx, cy, rings }) {
333
+ const field = useField("Halo");
334
+ measuredAll("Halo", field, { cx, cy });
335
+ rings.forEach(
336
+ (ring, i) => measured("Halo", "rings.r", `rings[${i}].r`, ring.r, field)
337
+ );
338
+ return /* @__PURE__ */ jsx2(Fragment2, { children: rings.map((ring, i) => /* @__PURE__ */ jsx2(
339
+ "circle",
340
+ {
341
+ cx,
342
+ cy,
343
+ r: ring.r,
344
+ className: FILL[ring.tone]
345
+ },
346
+ i
347
+ )) });
348
+ }
349
+ var RNG_MODULUS = 2147483647;
350
+ function rng(seed) {
351
+ let s = (Math.trunc(seed) % (RNG_MODULUS - 1) + RNG_MODULUS - 1) % (RNG_MODULUS - 1);
352
+ s += 1;
353
+ return () => {
354
+ s = s * 16807 % RNG_MODULUS;
355
+ return (s - 1) / (RNG_MODULUS - 1);
356
+ };
357
+ }
358
+ var BLEED_STREAM = 7919;
359
+ var BLEED_WIDTHS = 2;
360
+ var DOWN_SHARE = 0.5;
361
+ function cloudBankPaths(field, bank, reach) {
362
+ const { y, min, max, climb = 0, level = false, seed } = bank;
363
+ const { width, height } = field;
364
+ sized("CloudBank", field);
365
+ measuredAll("CloudBank", field, { y, min, max, climb, seed });
366
+ if (max < min)
367
+ throw new Error(
368
+ `CloudBank: the bump radius range must have max >= min (got min ${min}, max ${max})`
369
+ );
370
+ const bumps = Math.ceil(
371
+ ((1 + 2 * BLEED_WIDTHS) * width + 2 * max) / (2 * min)
372
+ );
373
+ if (bumps > MAX_BUMPS)
374
+ throw new Error(
375
+ `CloudBank: a ${width}-wide field at min ${min} and max ${max} takes ${bumps} bumps with its bleed; a bank draws at most ${MAX_BUMPS}`
376
+ );
377
+ const bottom = reach.down ? height * (1 + DOWN_SHARE) : height;
378
+ const under = max / 2;
379
+ const arc = (r, lift) => `a${r.toFixed(1)} ${r.toFixed(1)} 0 0 1 ${(r * 2).toFixed(1)} ${lift.toFixed(1)}`;
380
+ const rounded = (n) => Number(n.toFixed(1));
381
+ const step = (r, draw) => (draw() - 0.5) * r * 0.5 - climb * r * 2 / width;
382
+ const next = rng(seed);
383
+ const run = [];
384
+ let x = -max;
385
+ let endX = x;
386
+ let endY = y;
387
+ let drift = 0;
388
+ while (x < width + max) {
389
+ const r = min + next() * (max - min);
390
+ const s = step(r, next);
391
+ const lift = level ? s - drift : s;
392
+ drift += lift;
393
+ x += r * 2;
394
+ endX += rounded(r * 2);
395
+ endY += rounded(lift);
396
+ run.push({ d: arc(r, lift), x, endX, endY });
397
+ }
398
+ const end = (from, edge, up) => {
399
+ const rx = Math.abs(edge - from.x).toFixed(1);
400
+ const ry = bottom - from.y;
401
+ if (ry <= 0)
402
+ throw new Error(
403
+ `CloudBank: its end at the frame's edge has no depth to fall (top ${from.y.toFixed(1)}, fill to ${bottom}); a closed side fills on below the field`
404
+ );
405
+ return up ? `A${rx} ${ry.toFixed(1)} 0 0 1 ${from.x.toFixed(1)} ${from.y.toFixed(1)}` : `A${rx} ${ry.toFixed(1)} 0 0 1 ${edge} ${bottom}`;
406
+ };
407
+ const first = reach.left === "close" ? run.findIndex((b) => b.endX >= max) : -1;
408
+ let last = run.length - 1;
409
+ if (reach.right === "close") {
410
+ last = -1;
411
+ run.forEach((b, i) => {
412
+ if (b.endX <= width - max && i > first) last = i;
413
+ });
414
+ }
415
+ const startPoint = first === -1 ? { x: -max, y } : { x: run[first].endX, y: run[first].endY };
416
+ let frame = reach.left === "close" ? `M0 ${bottom}${end(startPoint, 0, true)}` : `M${-max} ${bottom}L${-max} ${y}`;
417
+ for (let i = first + 1; i <= last; i++) frame += run[i].d;
418
+ if (reach.right === "close") {
419
+ const at = last === -1 ? startPoint : { x: run[last].endX, y: run[last].endY };
420
+ frame += `${end(at, width, false)}Z`;
421
+ } else frame += `L${x} ${bottom}Z`;
422
+ const paths = [];
423
+ if (reach.left === "bleed") {
424
+ const back = rng(seed + BLEED_STREAM);
425
+ const arcs = [];
426
+ let lx = -max;
427
+ let offset = 0;
428
+ while (lx > -BLEED_WIDTHS * width - max) {
429
+ const r = min + back() * (max - min);
430
+ const s = step(r, back);
431
+ const lift = level ? offset - s : s;
432
+ offset -= rounded(lift);
433
+ arcs.unshift(arc(r, lift));
434
+ lx -= rounded(r * 2);
435
+ }
436
+ paths.push(
437
+ `M${lx.toFixed(1)} ${bottom}L${lx.toFixed(1)} ${(y + offset).toFixed(1)}${arcs.join("")}L${-max} ${y}L${-max + under} ${y + under}L${-max + under} ${bottom}Z`
438
+ );
439
+ }
440
+ if (reach.right === "bleed") {
441
+ let right = `M${(endX - under).toFixed(1)} ${bottom}L${(endX - under).toFixed(1)} ${(endY + under).toFixed(1)}L${endX.toFixed(1)} ${endY.toFixed(1)}`;
442
+ let rx = x;
443
+ while (rx < (1 + BLEED_WIDTHS) * width + max) {
444
+ const r = min + next() * (max - min);
445
+ const s = step(r, next);
446
+ const lift = level ? s - drift : s;
447
+ drift += lift;
448
+ right += arc(r, lift);
449
+ rx += r * 2;
450
+ }
451
+ paths.push(`${right}L${rx} ${bottom}Z`);
452
+ }
453
+ paths.push(frame);
454
+ return paths;
455
+ }
456
+ function CloudBank({ tone, ...bank }) {
457
+ const field = useField("CloudBank");
458
+ const paths = cloudBankPaths(field, bank, field.reach);
459
+ return /* @__PURE__ */ jsx2("g", { "data-scene-piece": "cloud-bank", className: FILL[tone], children: paths.map((d, i) => /* @__PURE__ */ jsx2("path", { d }, i)) });
460
+ }
461
+ function ridgePath(field, ridge, reach) {
462
+ const { y, rise, swells, seed } = ridge;
463
+ const { width, height } = field;
464
+ sized("Ridge", field);
465
+ measuredAll("Ridge", field, { y, rise, swells, seed });
466
+ const bottom = reach.down ? height * (1 + DOWN_SHARE) : height;
467
+ const step = width / swells;
468
+ let d;
469
+ if (reach.left === "close") d = `M0 ${bottom}`;
470
+ else {
471
+ const back = rng(seed + BLEED_STREAM);
472
+ const from = -BLEED_WIDTHS * width;
473
+ d = `M${from} ${bottom}L${from} ${(y - back() * rise * 0.3).toFixed(1)}`;
474
+ for (let i = -BLEED_WIDTHS * swells; i < 0; i++) {
475
+ const cy = y - back() * rise;
476
+ const end = i === -1 ? y : y - back() * rise * 0.3;
477
+ d += `Q${(i + 0.5) * step} ${cy.toFixed(1)} ${(i + 1) * step} ${end.toFixed(1)}`;
478
+ }
479
+ }
480
+ const next = rng(seed);
481
+ const count = reach.right === "close" ? swells : (1 + BLEED_WIDTHS) * swells;
482
+ for (let i = 0; i < count; i++) {
483
+ const cy = y - next() * rise;
484
+ const tail = (y - next() * rise * 0.3).toFixed(1);
485
+ const end = reach.right === "close" && i === count - 1 ? `${bottom}` : tail;
486
+ d += `Q${(i + 0.5) * step} ${cy.toFixed(1)} ${(i + 1) * step} ${end}`;
487
+ }
488
+ if (reach.right === "close") return `${d}Z`;
489
+ return `${d}L${(1 + BLEED_WIDTHS) * width} ${bottom}Z`;
490
+ }
491
+ function Ridge({ tone, ...ridge }) {
492
+ const field = useField("Ridge");
493
+ const d = ridgePath(field, ridge, field.reach);
494
+ return /* @__PURE__ */ jsx2("path", { "data-scene-piece": "ridge", d, className: FILL[tone] });
495
+ }
496
+ function starDots(field, { depth, count, seed }) {
497
+ sized("Stars", field);
498
+ measuredAll("Stars", field, { depth, count, seed });
499
+ const next = rng(seed);
500
+ const reach = depth * field.height;
501
+ return Array.from({ length: count }, () => ({
502
+ cx: (next() * field.width).toFixed(1),
503
+ cy: (next() * reach).toFixed(1),
504
+ r: (0.8 + next() * 1.4).toFixed(2)
505
+ }));
506
+ }
507
+ function Stars({ tone, ...stars }) {
508
+ const field = useField("Stars");
509
+ const dots = starDots(field, stars);
510
+ return /* @__PURE__ */ jsx2("g", { className: FILL[tone], children: dots.map((dot, i) => /* @__PURE__ */ jsx2("circle", { cx: dot.cx, cy: dot.cy, r: dot.r }, i)) });
511
+ }
512
+ var LABEL_HALO = 0.4;
513
+ function RouteLine({
514
+ points,
515
+ tone,
516
+ markTone,
517
+ labelSize
518
+ }) {
519
+ const field = useField("RouteLine");
520
+ const layer = useContext(LayerContext);
521
+ measuredAll("RouteLine", field, { labelSize });
522
+ points.forEach((p, i) => {
523
+ measured("RouteLine", "points.x", `points[${i}].x`, p.x, field);
524
+ measured("RouteLine", "points.y", `points[${i}].y`, p.y, field);
525
+ });
526
+ if (!field.routes.has(points))
527
+ throw new Error(
528
+ "RouteLine: its labels paint in its SkyField's top layer, so it is a direct child of the SkyField; this one is inside another element"
529
+ );
530
+ const labelled = points.some((p) => p.label);
531
+ if (labelled && field.ground === null)
532
+ throw new Error(
533
+ `RouteLine: a label carries a halo in its field's ground tone; this SkyField has ground="none" and no Band around it gives one`
534
+ );
535
+ if (layer === "labels") {
536
+ if (!labelled || field.ground === null) return null;
537
+ const halo = STROKE[field.ground];
538
+ const gap = labelSize * 1.1;
539
+ return /* @__PURE__ */ jsx2(Fragment2, { children: points.map((p, i) => {
540
+ if (!p.label) return null;
541
+ const side = p.side ?? "above";
542
+ const lx = side === "left" ? p.x - gap : side === "right" ? p.x + gap : p.x;
543
+ const ly = side === "above" ? p.y - gap : side === "below" ? p.y + gap * 1.5 : p.y + labelSize * 0.35;
544
+ const anchor = side === "left" ? "end" : side === "right" ? "start" : "middle";
545
+ return /* @__PURE__ */ jsx2(
546
+ "text",
547
+ {
548
+ x: lx,
549
+ y: ly,
550
+ textAnchor: anchor,
551
+ fontSize: labelSize,
552
+ fontWeight: 600,
553
+ strokeWidth: labelSize * LABEL_HALO,
554
+ strokeLinejoin: "round",
555
+ paintOrder: "stroke",
556
+ className: cn(FILL[tone], halo),
557
+ children: p.label
558
+ },
559
+ i
560
+ );
561
+ }) });
562
+ }
563
+ const at = (i) => points[Math.max(0, Math.min(points.length - 1, i))];
564
+ let d = `M${points[0].x} ${points[0].y}`;
565
+ for (let i = 0; i < points.length - 1; i++) {
566
+ const [p0, p1, p2, p3] = [at(i - 1), at(i), at(i + 1), at(i + 2)];
567
+ d += `C${(p1.x + (p2.x - p0.x) / 6).toFixed(1)} ${(p1.y + (p2.y - p0.y) / 6).toFixed(1)} ${(p2.x - (p3.x - p1.x) / 6).toFixed(1)} ${(p2.y - (p3.y - p1.y) / 6).toFixed(1)} ${p2.x} ${p2.y}`;
568
+ }
569
+ return /* @__PURE__ */ jsxs("g", { children: [
570
+ /* @__PURE__ */ jsx2(
571
+ "path",
572
+ {
573
+ d,
574
+ fill: "none",
575
+ strokeWidth: labelSize * 0.16,
576
+ strokeDasharray: `${labelSize * 0.1} ${labelSize * 0.55}`,
577
+ strokeLinecap: "round",
578
+ className: STROKE[tone]
579
+ }
580
+ ),
581
+ points.map((p, i) => /* @__PURE__ */ jsxs("g", { children: [
582
+ p.state === "here" && /* @__PURE__ */ jsx2(
583
+ "circle",
584
+ {
585
+ cx: p.x,
586
+ cy: p.y,
587
+ r: labelSize * 0.85,
588
+ className: cn(FILL[markTone], "opacity-30")
589
+ }
590
+ ),
591
+ /* @__PURE__ */ jsx2(
592
+ "circle",
593
+ {
594
+ cx: p.x,
595
+ cy: p.y,
596
+ r: labelSize * 0.34,
597
+ strokeWidth: labelSize * 0.14,
598
+ className: cn(
599
+ STROKE[tone],
600
+ p.state === "ahead" ? FILL[markTone] : FILL[tone]
601
+ )
602
+ }
603
+ )
604
+ ] }, i))
605
+ ] });
606
+ }
607
+
608
+ // src/material/band.tsx
609
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
610
+ var PAD = {
611
+ none: "[&:has(+[data-band-edge])]:pb-16 md:[&:has(+[data-band-edge])]:pb-[120px]",
612
+ tight: "pb-16 md:pb-24 md:[&:has(+[data-band-edge])]:pb-[120px]",
613
+ section: "py-20 md:py-28",
614
+ open: "py-24 md:py-40"
615
+ };
616
+ var UNDER_NIGHT_HEADER = [
617
+ "[[data-marketing-header='night']+&]:before:block [[data-marketing-header='night']+&]:before:h-16 [[data-marketing-header='night']+&]:before:content-['']",
618
+ "[[data-marketing-header='night']+*>&:first-child]:before:block [[data-marketing-header='night']+*>&:first-child]:before:h-16 [[data-marketing-header='night']+*>&:first-child]:before:content-['']",
619
+ "[[data-marketing-header='night']+*>:first-child>&:first-child]:before:block [[data-marketing-header='night']+*>:first-child>&:first-child]:before:h-16 [[data-marketing-header='night']+*>:first-child>&:first-child]:before:content-['']"
620
+ ].join(" ");
621
+ var EDGE_FIELD = { width: 3840, height: 240, seam: 120 };
622
+ function Bank({
623
+ field,
624
+ bank,
625
+ tone
626
+ }) {
627
+ return /* @__PURE__ */ jsx3("g", { className: FILL[tone], children: cloudBankPaths(field, bank, BLEED_BOTH_SIDES).map((d, i) => /* @__PURE__ */ jsx3("path", { d }, i)) });
628
+ }
629
+ function BandEdge({ via, tone, seed }) {
630
+ const { width, height, seam } = EDGE_FIELD;
631
+ const overlap = 8;
632
+ return /* @__PURE__ */ jsxs2(
633
+ "svg",
634
+ {
635
+ "aria-hidden": "true",
636
+ "data-band-edge-art": "",
637
+ viewBox: `0 0 ${width} ${height}`,
638
+ preserveAspectRatio: "xMidYMid slice",
639
+ className: "pointer-events-none absolute inset-x-0 -top-16 h-32 w-full md:-top-[120px] md:h-[240px]",
640
+ children: [
641
+ via && /* @__PURE__ */ jsx3(
642
+ Bank,
643
+ {
644
+ field: { width, height: seam },
645
+ bank: { y: 82, min: 24, max: 56, level: true, seed: seed + 3 },
646
+ tone: via
647
+ }
648
+ ),
649
+ /* @__PURE__ */ jsx3(
650
+ Bank,
651
+ {
652
+ field: { width, height: seam + overlap },
653
+ bank: { y: 100, min: 22, max: 58, level: true, seed },
654
+ tone
655
+ }
656
+ ),
657
+ /* @__PURE__ */ jsx3("g", { transform: `matrix(1 0 0 -1 0 ${height})`, children: /* @__PURE__ */ jsx3(
658
+ Bank,
659
+ {
660
+ field: { width, height: seam + overlap },
661
+ bank: { y: 72, min: 22, max: 58, level: true, seed: seed + 7 },
662
+ tone
663
+ }
664
+ ) })
665
+ ]
666
+ }
667
+ );
668
+ }
669
+ function Band({
670
+ tone,
671
+ surface = "day",
672
+ pad = "section",
673
+ under = "none",
674
+ edge,
675
+ backdrop,
676
+ raised,
677
+ id,
678
+ children
679
+ }) {
680
+ return /* @__PURE__ */ jsxs2(
681
+ "section",
682
+ {
683
+ id,
684
+ "data-surface": surface,
685
+ "data-band-edge": edge ? "" : void 0,
686
+ className: cn(
687
+ "relative isolate scroll-mt-4",
688
+ BG[tone],
689
+ PAD[pad],
690
+ under === "overhang" && "pt-44 md:pt-[23rem]",
691
+ edge && "pt-28 md:pt-48",
692
+ raised && "z-10",
693
+ surface === "night" && UNDER_NIGHT_HEADER
694
+ ),
695
+ children: [
696
+ backdrop && /* @__PURE__ */ jsx3(
697
+ "div",
698
+ {
699
+ "aria-hidden": "true",
700
+ className: "absolute inset-0 -z-10 overflow-hidden",
701
+ children: backdrop
702
+ }
703
+ ),
704
+ edge && /* @__PURE__ */ jsx3(BandEdge, { ...edge, tone }),
705
+ /* @__PURE__ */ jsx3(GroundContext.Provider, { value: tone, children })
706
+ ]
707
+ }
708
+ );
709
+ }
710
+
711
+ // src/material/figure.tsx
712
+ import { jsx as jsx4 } from "react/jsx-runtime";
713
+ var ASPECT = {
714
+ "16:9": "aspect-video",
715
+ "8:5": "aspect-[8/5]",
716
+ "4:3": "aspect-[4/3]",
717
+ "3:2": "aspect-[3/2]",
718
+ "1:1": "aspect-square",
719
+ "4:5": "aspect-[4/5]",
720
+ "full-bleed": "h-full"
721
+ };
722
+ function recordable(spec) {
723
+ const blank = [
724
+ !spec.role.trim() && "`role`",
725
+ !spec.prompt.trim() && "`prompt`",
726
+ spec.palette.length === 0 && "`palette`"
727
+ ].filter(Boolean);
728
+ if (blank.length > 0)
729
+ throw new Error(
730
+ `Figure: ${blank.join(", ")} of the spec for "${spec.role}" must not be empty; the image step reads them`
731
+ );
732
+ }
733
+ function Figure({ spec, alt, children, fit = "aspect" }) {
734
+ recordable(spec);
735
+ return /* @__PURE__ */ jsx4(
736
+ "figure",
737
+ {
738
+ role: alt ? "img" : void 0,
739
+ "aria-label": alt || void 0,
740
+ "aria-hidden": alt ? void 0 : "true",
741
+ "data-figure-role": spec.role,
742
+ "data-figure-aspect": spec.aspect,
743
+ "data-figure-palette": spec.palette.join(" "),
744
+ "data-figure-prompt": spec.prompt,
745
+ className: cn(
746
+ "relative m-0 overflow-hidden [&>svg]:absolute [&>svg]:inset-0 [&>svg]:size-full",
747
+ fit === "fill" ? "h-full w-full" : ASPECT[spec.aspect]
748
+ ),
749
+ children
750
+ }
751
+ );
752
+ }
753
+
754
+ // src/material/hero-scene.tsx
755
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
756
+ var STACK_ZONE_RATIO = 1.25;
757
+ var STACK_ZONE_MIN_WIDTH = 390;
758
+ var HEIGHT2 = {
759
+ fold: "lg:min-h-[min(calc(100svh-4rem),56rem)]",
760
+ tall: "lg:min-h-[46rem]"
761
+ };
762
+ function HeroScene({
763
+ height,
764
+ subject,
765
+ copySide = "start",
766
+ children
767
+ }) {
768
+ return /* @__PURE__ */ jsx5("div", { "data-hero-scene": "", className: "overflow-x-clip", children: /* @__PURE__ */ jsxs3(
769
+ "div",
770
+ {
771
+ className: cn(
772
+ "mx-auto flex w-full max-w-content-xl flex-col lg:grid lg:grid-cols-2",
773
+ HEIGHT2[height]
774
+ ),
775
+ children: [
776
+ /* @__PURE__ */ jsx5(
777
+ "div",
778
+ {
779
+ "data-hero-copy": "",
780
+ className: cn(
781
+ "relative z-10 flex min-w-0 flex-col items-start justify-center gap-6 py-10 md:py-14",
782
+ CONTENT_GUTTER,
783
+ copySide === "end" && "lg:order-2"
784
+ ),
785
+ children
786
+ }
787
+ ),
788
+ /* @__PURE__ */ jsx5(
789
+ "div",
790
+ {
791
+ "data-hero-subject": "",
792
+ className: "relative mx-auto aspect-[5/4] w-full max-w-2xl lg:aspect-auto lg:h-full lg:max-w-none [&_figure]:overflow-visible [&_svg]:overflow-visible",
793
+ children: /* @__PURE__ */ jsx5(SceneCopyContext.Provider, { value: copySide, children: subject })
794
+ }
795
+ )
796
+ ]
797
+ }
798
+ ) });
799
+ }
800
+
801
+ // src/material/layout.tsx
802
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
803
+ var GAP = {
804
+ 1: "gap-1",
805
+ 2: "gap-2",
806
+ 3: "gap-3",
807
+ 4: "gap-4",
808
+ 6: "gap-6",
809
+ 8: "gap-8",
810
+ 10: "gap-10",
811
+ 12: "gap-12",
812
+ 16: "gap-16"
813
+ };
814
+ function Stack({ gap, align = "stretch", children }) {
815
+ return /* @__PURE__ */ jsx6(
816
+ "div",
817
+ {
818
+ className: cn(
819
+ "flex min-w-0 flex-col",
820
+ GAP[gap],
821
+ align === "center" && "items-center",
822
+ align === "start" && "items-start"
823
+ ),
824
+ children
825
+ }
826
+ );
827
+ }
828
+ function Inline({
829
+ gap,
830
+ justify = "start",
831
+ align = "center",
832
+ fillOnPhone,
833
+ children
834
+ }) {
835
+ return /* @__PURE__ */ jsx6(
836
+ "div",
837
+ {
838
+ className: cn(
839
+ "flex min-w-0 flex-wrap",
840
+ GAP[gap],
841
+ align === "center" ? "items-center" : "items-start",
842
+ justify === "between" && "justify-between",
843
+ justify === "center" && "justify-center",
844
+ fillOnPhone && "max-sm:w-full max-sm:flex-col max-sm:items-stretch max-sm:[&>*]:w-full"
845
+ ),
846
+ children
847
+ }
848
+ );
849
+ }
850
+ var COUNT = {
851
+ 2: "md:grid-cols-2",
852
+ 3: "md:grid-cols-3",
853
+ 4: "md:grid-cols-4"
854
+ };
855
+ var CONNECT = "md:[&>*]:relative md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:absolute md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:left-[calc(50%+2.5rem)] md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:top-6 md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:h-0 md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:w-[calc(100%-3rem)] md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:border-t-2 md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:border-dashed md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:border-mist-3 md:[&>*:not(:last-child,:has(+[data-scroll-hint]))]:after:content-['']";
856
+ function Columns({
857
+ count,
858
+ gap,
859
+ phone,
860
+ connect,
861
+ children
862
+ }) {
863
+ return /* @__PURE__ */ jsxs4(
864
+ "div",
865
+ {
866
+ className: cn(
867
+ GAP[gap],
868
+ connect && CONNECT,
869
+ phone === "stack" ? cn("grid grid-cols-1", COUNT[count]) : cn(
870
+ "flex overflow-x-auto md:grid md:overflow-visible md:[&>[data-scroll-hint]]:hidden",
871
+ "[&>:not([data-scroll-hint])]:w-64 [&>:not([data-scroll-hint])]:shrink-0 md:[&>:not([data-scroll-hint])]:w-auto",
872
+ COUNT[count]
873
+ )
874
+ ),
875
+ children: [
876
+ children,
877
+ phone === "scroll" && /* @__PURE__ */ jsx6(ScrollHintXMark, {})
878
+ ]
879
+ }
880
+ );
881
+ }
882
+ function Split({ ratio, reverse, gap, children }) {
883
+ return /* @__PURE__ */ jsxs4(
884
+ "div",
885
+ {
886
+ className: cn(
887
+ "grid grid-cols-1 items-center",
888
+ GAP[gap],
889
+ ratio === "even" ? "md:grid-cols-2" : "md:grid-cols-[5fr_7fr]",
890
+ reverse && (ratio === "even" ? "md:[&>:first-child]:order-2" : "md:grid-cols-[7fr_5fr] md:[&>:first-child]:order-2")
891
+ ),
892
+ children: [
893
+ /* @__PURE__ */ jsx6("div", { className: "min-w-0", children: children[0] }),
894
+ /* @__PURE__ */ jsx6("div", { className: "min-w-0", children: children[1] })
895
+ ]
896
+ }
897
+ );
898
+ }
899
+ var SPACER = {
900
+ hero: "h-10 md:h-14",
901
+ section: "h-16 md:h-24"
902
+ };
903
+ function Spacer({ size }) {
904
+ return /* @__PURE__ */ jsx6("div", { "aria-hidden": "true", className: SPACER[size] });
905
+ }
906
+ var INSET = { 3: "p-3", 4: "p-4", 6: "p-6" };
907
+ function Inset({ pad, children }) {
908
+ return /* @__PURE__ */ jsx6("div", { className: INSET[pad], children });
909
+ }
910
+
911
+ // src/material/type.tsx
912
+ import { jsx as jsx7 } from "react/jsx-runtime";
913
+ var LEVEL = {
914
+ poster: "text-display-poster",
915
+ headline: "text-display-headline",
916
+ title: "text-display-title"
917
+ };
918
+ var MEASURE = {
919
+ narrow: "max-w-[14ch]",
920
+ wide: "max-w-[22ch]",
921
+ none: ""
922
+ };
923
+ function Display({
924
+ level,
925
+ as: Tag,
926
+ align = "start",
927
+ measure = "none",
928
+ children
929
+ }) {
930
+ return /* @__PURE__ */ jsx7(
931
+ Tag,
932
+ {
933
+ className: cn(
934
+ "text-foreground",
935
+ LEVEL[level],
936
+ MEASURE[measure],
937
+ align === "center" && "mx-auto text-center"
938
+ ),
939
+ children
940
+ }
941
+ );
942
+ }
943
+ var SIZE = {
944
+ lede: "text-lede",
945
+ body: "text-base leading-relaxed",
946
+ small: "text-sm leading-snug",
947
+ micro: "text-xs leading-snug"
948
+ };
949
+ var TONE = {
950
+ default: "text-foreground",
951
+ muted: "text-muted-foreground",
952
+ accent: "text-primary-text"
953
+ };
954
+ var WEIGHT = {
955
+ normal: "font-normal",
956
+ medium: "font-medium",
957
+ bold: "font-semibold"
958
+ };
959
+ function Text({
960
+ size = "body",
961
+ tone = "default",
962
+ weight = "normal",
963
+ align = "start",
964
+ measure,
965
+ as: Tag = "p",
966
+ children
967
+ }) {
968
+ return /* @__PURE__ */ jsx7(
969
+ Tag,
970
+ {
971
+ className: cn(
972
+ SIZE[size],
973
+ TONE[tone],
974
+ WEIGHT[weight],
975
+ align === "center" && "mx-auto text-center",
976
+ measure && "max-w-[58ch]"
977
+ ),
978
+ children
979
+ }
980
+ );
981
+ }
982
+
983
+ // src/material/vignette.tsx
984
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
985
+ function Vignette({
986
+ title,
987
+ icon,
988
+ sampleLabel,
989
+ surface = "day",
990
+ placement = "inline",
991
+ crop = "none",
992
+ children
993
+ }) {
994
+ return /* @__PURE__ */ jsxs5(
995
+ "div",
996
+ {
997
+ "data-surface": surface,
998
+ className: cn(
999
+ "relative overflow-hidden rounded-2xl border border-border bg-background shadow-[0_2px_4px_-2px_hsl(var(--night-1)),0_30px_70px_-30px_hsl(var(--night-1))]",
1000
+ placement === "hang" && "z-10 -mb-32 md:-mb-52"
1001
+ ),
1002
+ children: [
1003
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 border-b border-border bg-card px-4 py-2.5", children: [
1004
+ /* @__PURE__ */ jsx8("span", { className: "text-muted-foreground", children: /* @__PURE__ */ jsx8(Icon, { name: icon, size: "sm" }) }),
1005
+ /* @__PURE__ */ jsx8("span", { className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground", children: title }),
1006
+ /* @__PURE__ */ jsx8(IntentBadge, { intent: "neutral", children: sampleLabel })
1007
+ ] }),
1008
+ /* @__PURE__ */ jsx8(
1009
+ "div",
1010
+ {
1011
+ className: cn(
1012
+ "bg-muted/50 p-3 md:p-4",
1013
+ // a mask reads alpha only, so any opaque token draws the same crop
1014
+ crop === "bottom" && "relative max-h-[24rem] overflow-hidden [mask-image:linear-gradient(to_bottom,hsl(var(--foreground))_70%,transparent)]"
1015
+ ),
1016
+ children
1017
+ }
1018
+ )
1019
+ ]
1020
+ }
1021
+ );
1022
+ }
1023
+
1024
+ // src/material/wordmark.tsx
1025
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1026
+ function Wordmark({ name, mark, href }) {
1027
+ return /* @__PURE__ */ jsxs6(
1028
+ "a",
1029
+ {
1030
+ href,
1031
+ className: "flex items-center gap-2 text-lg font-bold tracking-tight text-foreground [font-family:var(--font-heading,inherit)]",
1032
+ children: [
1033
+ /* @__PURE__ */ jsx9("span", { className: "text-primary-text size-6 shrink-0", children: mark }),
1034
+ name
1035
+ ]
1036
+ }
1037
+ );
1038
+ }
1039
+ export {
1040
+ BLEED_BOTH_SIDES,
1041
+ Band,
1042
+ CloudBank,
1043
+ Columns,
1044
+ Display,
1045
+ Figure,
1046
+ Halo,
1047
+ HeroScene,
1048
+ Inline,
1049
+ Inset,
1050
+ Ridge,
1051
+ RouteLine,
1052
+ SCENE_MEASURES,
1053
+ STACK_ZONE_MIN_WIDTH,
1054
+ STACK_ZONE_RATIO,
1055
+ SkyField,
1056
+ Spacer,
1057
+ Split,
1058
+ Stack,
1059
+ Stars,
1060
+ Text,
1061
+ Vignette,
1062
+ Wordmark,
1063
+ cloudBankPaths,
1064
+ ridgePath,
1065
+ starDots
1066
+ };