@caqophony/ui 0.4.0 → 0.5.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.
package/dist/material.js CHANGED
@@ -1,15 +1,24 @@
1
1
  'use client';
2
2
  import {
3
3
  IntentBadge
4
- } from "./chunk-Y6LCXEXZ.js";
4
+ } from "./chunk-44GEHNZP.js";
5
5
  import {
6
+ CONTENT_GUTTER,
6
7
  Icon,
7
8
  ScrollHintXMark
8
- } from "./chunk-BAW3RIMF.js";
9
+ } from "./chunk-ID7VBGJA.js";
9
10
  import {
10
11
  cn
11
12
  } from "./chunk-DP5WQAPO.js";
12
13
 
14
+ // src/material/sky.tsx
15
+ import {
16
+ Children,
17
+ createContext,
18
+ isValidElement,
19
+ useContext
20
+ } from "react";
21
+
13
22
  // src/material/tones.ts
14
23
  var FILL = {
15
24
  "night-0": "fill-night-0",
@@ -72,69 +81,255 @@ var BG = {
72
81
  page: "bg-background"
73
82
  };
74
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
+
75
249
  // src/material/sky.tsx
76
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
77
- function finite(piece, facts) {
250
+ import { Fragment as Fragment2, jsx as jsx2, jsxs } from "react/jsx-runtime";
251
+ function measuredAll(piece, field, facts) {
78
252
  for (const [name, value] of Object.entries(facts))
79
- if (!Number.isFinite(value))
80
- throw new Error(
81
- `${piece}: \`${name}\` must be a finite number (got ${value})`
82
- );
253
+ if (value !== void 0) measured(piece, name, name, value, field);
83
254
  }
84
- function aboveZero(piece, facts) {
85
- for (const [name, value] of Object.entries(facts))
255
+ function sized(piece, field) {
256
+ for (const [name, value] of Object.entries({
257
+ width: field.width,
258
+ height: field.height
259
+ }))
86
260
  if (!Number.isFinite(value) || value <= 0)
87
261
  throw new Error(
88
262
  `${piece}: \`${name}\` must be a finite number above 0 (got ${value})`
89
263
  );
90
264
  }
91
- function wholeWithin(piece, name, value, [low, high]) {
92
- if (!Number.isInteger(value) || value < low || value > high)
265
+ var MAX_BUMPS = 2e3;
266
+ var BLEED_BOTH_SIDES = { down: false };
267
+ var IN_SCENE = { down: true };
268
+ var SceneCopyContext = createContext(null);
269
+ var GroundContext = createContext(null);
270
+ var FieldContext = createContext(null);
271
+ var LayerContext = createContext("pieces");
272
+ function useField(piece) {
273
+ const field = useContext(FieldContext);
274
+ if (field === null)
93
275
  throw new Error(
94
- `${piece}: \`${name}\` must be a whole number from ${low} to ${high} (got ${value})`
276
+ `${piece}: a scene piece draws inside a SkyField, which gives it the field's size; this one has no SkyField around it`
95
277
  );
278
+ return field;
279
+ }
280
+ function reachOf(copy) {
281
+ return copy === null ? BLEED_BOTH_SIDES : IN_SCENE;
96
282
  }
97
- var MIN_BUMP_RADIUS = 0.1;
98
- var MAX_BUMPS = 2e3;
99
- var MAX_SWELLS = 512;
100
- var MAX_STARS = 5e3;
101
- var ANCHOR = {
102
- top: "xMidYMin slice",
103
- center: "xMidYMid slice",
104
- bottom: "xMidYMax slice",
105
- right: "xMaxYMid slice"
106
- };
107
283
  function SkyField({
108
284
  width,
109
285
  height,
110
286
  anchor,
287
+ fit,
111
288
  only,
112
289
  ground,
113
290
  children
114
291
  }) {
115
- aboveZero("SkyField", { width, height });
292
+ measuredAll("SkyField", null, { width, height });
293
+ const copy = useContext(SceneCopyContext);
294
+ const under = useContext(GroundContext);
295
+ const routes = Children.toArray(children).filter(
296
+ (c) => isValidElement(c) && c.type === RouteLine
297
+ );
298
+ const field = {
299
+ width,
300
+ height,
301
+ reach: reachOf(copy),
302
+ ground: ground === "none" ? under : ground,
303
+ routes: new Set(
304
+ routes.map(
305
+ (r) => isValidElement(r) ? r.props.points : r
306
+ )
307
+ )
308
+ };
116
309
  return /* @__PURE__ */ jsxs(
117
310
  "svg",
118
311
  {
119
312
  viewBox: `0 0 ${width} ${height}`,
120
- preserveAspectRatio: ANCHOR[anchor],
313
+ preserveAspectRatio: preserveOf(anchor, fit),
121
314
  "aria-hidden": "true",
122
- className: cn(
123
- "absolute inset-0 size-full",
124
- only === "phone" && "md:hidden",
125
- only === "desktop" && "hidden md:block"
126
- ),
315
+ className: cn("absolute inset-0", frameClass(only)),
127
316
  children: [
128
- /* @__PURE__ */ jsx("rect", { width, height, className: FILL[ground] }),
129
- children
317
+ ground !== "none" && /* @__PURE__ */ jsx2("rect", { width, height, className: FILL[ground] }),
318
+ /* @__PURE__ */ jsx2(GroundContext.Provider, { value: field.ground, children: /* @__PURE__ */ jsxs(FieldContext.Provider, { value: field, children: [
319
+ children,
320
+ routes.length > 0 && /* @__PURE__ */ jsx2("g", { "data-route-labels": "", children: /* @__PURE__ */ jsx2(LayerContext.Provider, { value: "labels", children: routes }) })
321
+ ] }) })
130
322
  ]
131
323
  }
132
324
  );
133
325
  }
134
326
  function Halo({ cx, cy, rings }) {
135
- finite("Halo", { cx, cy });
136
- rings.forEach((ring, i) => aboveZero("Halo", { [`rings[${i}].r`]: ring.r }));
137
- return /* @__PURE__ */ jsx(Fragment, { children: rings.map((ring, i) => /* @__PURE__ */ jsx(
327
+ const field = useField("Halo");
328
+ measuredAll("Halo", field, { cx, cy });
329
+ rings.forEach(
330
+ (ring, i) => measured("Halo", "rings.r", `rings[${i}].r`, ring.r, field)
331
+ );
332
+ return /* @__PURE__ */ jsx2(Fragment2, { children: rings.map((ring, i) => /* @__PURE__ */ jsx2(
138
333
  "circle",
139
334
  {
140
335
  cx,
@@ -154,81 +349,189 @@ function rng(seed) {
154
349
  return (s - 1) / (RNG_MODULUS - 1);
155
350
  };
156
351
  }
157
- function CloudBank({
158
- width,
159
- height,
160
- y,
161
- min,
162
- max,
163
- climb = 0,
164
- level = false,
165
- seed,
166
- tone
167
- }) {
168
- aboveZero("CloudBank", { width, height });
169
- finite("CloudBank", { y, min, max, climb, seed });
170
- if (min <= 0 || max < min)
171
- throw new Error(
172
- `CloudBank: the bump radius range must have min > 0 and max >= min (got min ${min}, max ${max}); a zero radius never reaches the field's edge`
173
- );
174
- if (min < MIN_BUMP_RADIUS)
352
+ var BLEED_STREAM = 7919;
353
+ var BLEED_WIDTHS = 2;
354
+ var DOWN_SHARE = 0.5;
355
+ function cloudBankPaths(field, bank, reach) {
356
+ const { y, min, max, climb = 0, level = false, seed } = bank;
357
+ const { width, height } = field;
358
+ sized("CloudBank", field);
359
+ measuredAll("CloudBank", field, { y, min, max, climb, seed });
360
+ if (max < min)
175
361
  throw new Error(
176
- `CloudBank: \`min\` must be at least ${MIN_BUMP_RADIUS} (got ${min}); a smaller radius rounds to nothing in the path`
362
+ `CloudBank: the bump radius range must have max >= min (got min ${min}, max ${max})`
177
363
  );
178
- const bumps = Math.ceil((width + 2 * max) / (2 * min));
364
+ const bumps = Math.ceil(
365
+ ((1 + 2 * BLEED_WIDTHS) * width + 2 * max) / (2 * min)
366
+ );
179
367
  if (bumps > MAX_BUMPS)
180
368
  throw new Error(
181
- `CloudBank: ${width} wide at min ${min} and max ${max} can take ${bumps} bumps; a bank draws at most ${MAX_BUMPS} bumps`
369
+ `CloudBank: a ${width}-wide field at min ${min} and max ${max} takes ${bumps} bumps with its bleed; a bank draws at most ${MAX_BUMPS}`
182
370
  );
371
+ const bottom = reach.down ? height * (1 + DOWN_SHARE) : height;
372
+ const under = max / 2;
373
+ const arc = (r, lift) => `a${r.toFixed(1)} ${r.toFixed(1)} 0 0 1 ${(r * 2).toFixed(1)} ${lift.toFixed(1)}`;
374
+ const rounded = (n) => Number(n.toFixed(1));
375
+ const step = (r, draw) => (draw() - 0.5) * r * 0.5 - climb * r * 2 / width;
183
376
  const next = rng(seed);
377
+ let frame = `M${-max} ${bottom}L${-max} ${y}`;
184
378
  let x = -max;
379
+ let endX = x;
380
+ let endY = y;
185
381
  let drift = 0;
186
- let d = `M${x} ${height}L${x} ${y}`;
187
382
  while (x < width + max) {
188
383
  const r = min + next() * (max - min);
189
- const step = (next() - 0.5) * r * 0.5 - climb * r * 2 / width;
190
- const lift = level ? step - drift : step;
384
+ const s = step(r, next);
385
+ const lift = level ? s - drift : s;
191
386
  drift += lift;
192
- d += `a${r.toFixed(1)} ${r.toFixed(1)} 0 0 1 ${(r * 2).toFixed(1)} ${lift.toFixed(1)}`;
193
387
  x += r * 2;
388
+ endX += rounded(r * 2);
389
+ endY += rounded(lift);
390
+ frame += arc(r, lift);
194
391
  }
195
- return /* @__PURE__ */ jsx("path", { d: `${d}L${x} ${height}Z`, className: FILL[tone] });
392
+ frame += `L${x} ${bottom}Z`;
393
+ const back = rng(seed + BLEED_STREAM);
394
+ const arcs = [];
395
+ let lx = -max;
396
+ let offset = 0;
397
+ while (lx > -BLEED_WIDTHS * width - max) {
398
+ const r = min + back() * (max - min);
399
+ const s = step(r, back);
400
+ const lift = level ? offset - s : s;
401
+ offset -= rounded(lift);
402
+ arcs.unshift(arc(r, lift));
403
+ lx -= rounded(r * 2);
404
+ }
405
+ const paths = [
406
+ `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`
407
+ ];
408
+ let right = `M${(endX - under).toFixed(1)} ${bottom}L${(endX - under).toFixed(1)} ${(endY + under).toFixed(1)}L${endX.toFixed(1)} ${endY.toFixed(1)}`;
409
+ let rx = x;
410
+ while (rx < (1 + BLEED_WIDTHS) * width + max) {
411
+ const r = min + next() * (max - min);
412
+ const s = step(r, next);
413
+ const lift = level ? s - drift : s;
414
+ drift += lift;
415
+ right += arc(r, lift);
416
+ rx += r * 2;
417
+ }
418
+ paths.push(`${right}L${rx} ${bottom}Z`);
419
+ paths.push(frame);
420
+ return paths;
196
421
  }
197
- function Ridge({
198
- width,
199
- height,
200
- y,
201
- rise,
202
- swells,
203
- seed,
204
- tone
205
- }) {
206
- aboveZero("Ridge", { width, height });
207
- finite("Ridge", { y, rise, seed });
208
- wholeWithin("Ridge", "swells", swells, [1, MAX_SWELLS]);
209
- const next = rng(seed);
422
+ function CloudBank({ tone, ...bank }) {
423
+ const field = useField("CloudBank");
424
+ const paths = cloudBankPaths(field, bank, field.reach);
425
+ return /* @__PURE__ */ jsx2("g", { "data-scene-piece": "cloud-bank", className: FILL[tone], children: paths.map((d, i) => /* @__PURE__ */ jsx2("path", { d }, i)) });
426
+ }
427
+ function ridgePath(field, ridge, reach) {
428
+ const { y, rise, swells, seed } = ridge;
429
+ const { width, height } = field;
430
+ sized("Ridge", field);
431
+ measuredAll("Ridge", field, { y, rise, swells, seed });
432
+ const bottom = reach.down ? height * (1 + DOWN_SHARE) : height;
210
433
  const step = width / swells;
211
- let d = `M0 ${height}L0 ${y}`;
212
- for (let i = 0; i < swells; i++) {
434
+ const back = rng(seed + BLEED_STREAM);
435
+ const from = -BLEED_WIDTHS * width;
436
+ let d = `M${from} ${bottom}L${from} ${(y - back() * rise * 0.3).toFixed(1)}`;
437
+ for (let i = -BLEED_WIDTHS * swells; i < 0; i++) {
438
+ const cy = y - back() * rise;
439
+ const end = i === -1 ? y : y - back() * rise * 0.3;
440
+ d += `Q${(i + 0.5) * step} ${cy.toFixed(1)} ${(i + 1) * step} ${end.toFixed(1)}`;
441
+ }
442
+ const next = rng(seed);
443
+ for (let i = 0; i < (1 + BLEED_WIDTHS) * swells; i++) {
213
444
  const cy = y - next() * rise;
214
- d += `Q${(i + 0.5) * step} ${cy.toFixed(1)} ${(i + 1) * step} ${(y - next() * rise * 0.3).toFixed(1)}`;
445
+ const tail = (y - next() * rise * 0.3).toFixed(1);
446
+ d += `Q${(i + 0.5) * step} ${cy.toFixed(1)} ${(i + 1) * step} ${tail}`;
215
447
  }
216
- return /* @__PURE__ */ jsx("path", { d: `${d}L${width} ${height}Z`, className: FILL[tone] });
448
+ return `${d}L${(1 + BLEED_WIDTHS) * width} ${bottom}Z`;
217
449
  }
218
- function Stars({ width, height, count, seed, tone }) {
219
- aboveZero("Stars", { width, height });
220
- finite("Stars", { seed });
221
- wholeWithin("Stars", "count", count, [0, MAX_STARS]);
450
+ function Ridge({ tone, ...ridge }) {
451
+ const field = useField("Ridge");
452
+ const d = ridgePath(field, ridge, field.reach);
453
+ return /* @__PURE__ */ jsx2("path", { "data-scene-piece": "ridge", d, className: FILL[tone] });
454
+ }
455
+ function starDots(field, { depth, count, seed }) {
456
+ sized("Stars", field);
457
+ measuredAll("Stars", field, { depth, count, seed });
222
458
  const next = rng(seed);
223
- return /* @__PURE__ */ jsx("g", { className: FILL[tone], children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx(
224
- "circle",
225
- {
226
- cx: (next() * width).toFixed(1),
227
- cy: (next() * height).toFixed(1),
228
- r: (0.8 + next() * 1.4).toFixed(2)
229
- },
230
- i
231
- )) });
459
+ const reach = depth * field.height;
460
+ return Array.from({ length: count }, () => ({
461
+ cx: (next() * field.width).toFixed(1),
462
+ cy: (next() * reach).toFixed(1),
463
+ r: (0.8 + next() * 1.4).toFixed(2)
464
+ }));
465
+ }
466
+ function Stars({ tone, ...stars }) {
467
+ const field = useField("Stars");
468
+ const dots = starDots(field, stars);
469
+ 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)) });
470
+ }
471
+ var LABEL_HALO = 0.12;
472
+ var LABEL_ADVANCE_EM = 0.62;
473
+ function routeLabelPlaces(points, labelSize, field) {
474
+ const gap = labelSize * 1.1;
475
+ const pad = labelSize * LABEL_HALO / 2;
476
+ const marks = points.map((p) => {
477
+ const r = labelSize * (p.state === "here" ? 0.85 : 0.34);
478
+ return {
479
+ what: `the mark at ${p.x},${p.y}`,
480
+ box: { left: p.x - r, top: p.y - r, right: p.x + r, bottom: p.y + r }
481
+ };
482
+ });
483
+ const placed = [];
484
+ return points.map((p, i) => {
485
+ if (!p.label) return null;
486
+ const words = p.label;
487
+ const width = words.length * LABEL_ADVANCE_EM * labelSize;
488
+ const before = points[Math.max(0, i - 1)];
489
+ const after = points[Math.min(points.length - 1, i + 1)];
490
+ const across = Math.abs(after.x - before.x) >= Math.abs(after.y - before.y);
491
+ const order = across ? ["above", "below", "right", "left"] : ["right", "left", "above", "below"];
492
+ const sides = p.side === void 0 ? order : [p.side, ...order.filter((side) => side !== p.side)];
493
+ const candidates = sides.map((side) => {
494
+ const x = side === "left" ? p.x - gap : side === "right" ? p.x + gap : p.x;
495
+ const y = side === "above" ? p.y - gap : side === "below" ? p.y + gap * 1.5 : p.y + labelSize * 0.35;
496
+ const anchor = side === "left" ? "end" : side === "right" ? "start" : "middle";
497
+ const left = anchor === "start" ? x : anchor === "end" ? x - width : x - width / 2;
498
+ return {
499
+ x,
500
+ y,
501
+ anchor,
502
+ side,
503
+ box: {
504
+ left: left - pad,
505
+ right: left + width + pad,
506
+ top: y - labelSize * 0.8 - pad,
507
+ bottom: y + labelSize * 0.25 + pad
508
+ }
509
+ };
510
+ });
511
+ const others = [...placed, ...marks.filter((_, k) => k !== i)];
512
+ const blocks = (c) => [
513
+ ...others.map((o) => ({
514
+ what: o.what,
515
+ across: Math.min(c.box.right, o.box.right) - Math.max(c.box.left, o.box.left),
516
+ down: Math.min(c.box.bottom, o.box.bottom) - Math.max(c.box.top, o.box.top)
517
+ })).filter((h) => h.across > 0 && h.down > 0).map(
518
+ (h) => `crosses ${h.what} by ${h.across.toFixed(1)} by ${h.down.toFixed(1)}`
519
+ ),
520
+ ...[
521
+ { edge: "left", by: -c.box.left },
522
+ { edge: "top", by: -c.box.top },
523
+ { edge: "right", by: c.box.right - field.width },
524
+ { edge: "bottom", by: c.box.bottom - field.height }
525
+ ].filter((e) => e.by > 0).map((e) => `runs ${e.by.toFixed(1)} past the field's ${e.edge}`)
526
+ ];
527
+ const clear = candidates.find((c) => blocks(c).length === 0);
528
+ if (clear === void 0)
529
+ throw new Error(
530
+ `RouteLine: the label "${words}" has no clear side, in field units: ${candidates.map((c) => `${c.side} ${blocks(c).join(" and ")}`).join("; ")}. Move its point, or what it meets, apart`
531
+ );
532
+ placed.push({ what: `the label "${words}"`, box: clear.box });
533
+ return clear;
534
+ });
232
535
  }
233
536
  function RouteLine({
234
537
  points,
@@ -236,19 +539,55 @@ function RouteLine({
236
539
  markTone,
237
540
  labelSize
238
541
  }) {
239
- aboveZero("RouteLine", { labelSize });
240
- points.forEach(
241
- (p, i) => finite("RouteLine", { [`points[${i}].x`]: p.x, [`points[${i}].y`]: p.y })
242
- );
542
+ const field = useField("RouteLine");
543
+ const layer = useContext(LayerContext);
544
+ measuredAll("RouteLine", field, { labelSize });
545
+ points.forEach((p, i) => {
546
+ measured("RouteLine", "points.x", `points[${i}].x`, p.x, field);
547
+ measured("RouteLine", "points.y", `points[${i}].y`, p.y, field);
548
+ });
549
+ if (!field.routes.has(points))
550
+ throw new Error(
551
+ "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"
552
+ );
553
+ const labelled = points.some((p) => p.label);
554
+ if (labelled && field.ground === null)
555
+ throw new Error(
556
+ `RouteLine: a label carries a halo in its field's ground tone; this SkyField has ground="none" and no Band around it gives one`
557
+ );
558
+ if (layer === "labels") {
559
+ if (!labelled || field.ground === null) return null;
560
+ const halo = STROKE[field.ground];
561
+ const places = routeLabelPlaces(points, labelSize, field);
562
+ return /* @__PURE__ */ jsx2(Fragment2, { children: points.map((p, i) => {
563
+ const place = places[i];
564
+ if (!p.label || !place) return null;
565
+ return /* @__PURE__ */ jsx2(
566
+ "text",
567
+ {
568
+ x: place.x,
569
+ y: place.y,
570
+ textAnchor: place.anchor,
571
+ fontSize: labelSize,
572
+ fontWeight: 600,
573
+ strokeWidth: labelSize * LABEL_HALO,
574
+ strokeLinejoin: "round",
575
+ paintOrder: "stroke",
576
+ className: cn(FILL[tone], halo),
577
+ children: p.label
578
+ },
579
+ i
580
+ );
581
+ }) });
582
+ }
243
583
  const at = (i) => points[Math.max(0, Math.min(points.length - 1, i))];
244
584
  let d = `M${points[0].x} ${points[0].y}`;
245
585
  for (let i = 0; i < points.length - 1; i++) {
246
586
  const [p0, p1, p2, p3] = [at(i - 1), at(i), at(i + 1), at(i + 2)];
247
587
  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}`;
248
588
  }
249
- const gap = labelSize * 1.1;
250
589
  return /* @__PURE__ */ jsxs("g", { children: [
251
- /* @__PURE__ */ jsx(
590
+ /* @__PURE__ */ jsx2(
252
591
  "path",
253
592
  {
254
593
  d,
@@ -259,60 +598,54 @@ function RouteLine({
259
598
  className: STROKE[tone]
260
599
  }
261
600
  ),
262
- points.map((p, i) => {
263
- const side = p.side ?? "above";
264
- const lx = side === "left" ? p.x - gap : side === "right" ? p.x + gap : p.x;
265
- const ly = side === "above" ? p.y - gap : side === "below" ? p.y + gap * 1.5 : p.y + labelSize * 0.35;
266
- const anchor = side === "left" ? "end" : side === "right" ? "start" : "middle";
267
- return /* @__PURE__ */ jsxs("g", { children: [
268
- p.state === "here" && /* @__PURE__ */ jsx(
269
- "circle",
270
- {
271
- cx: p.x,
272
- cy: p.y,
273
- r: labelSize * 0.85,
274
- className: cn(FILL[markTone], "opacity-30")
275
- }
276
- ),
277
- /* @__PURE__ */ jsx(
278
- "circle",
279
- {
280
- cx: p.x,
281
- cy: p.y,
282
- r: labelSize * 0.34,
283
- strokeWidth: labelSize * 0.14,
284
- className: cn(
285
- STROKE[tone],
286
- p.state === "ahead" ? FILL[markTone] : FILL[tone]
287
- )
288
- }
289
- ),
290
- p.label && /* @__PURE__ */ jsx(
291
- "text",
292
- {
293
- x: lx,
294
- y: ly,
295
- textAnchor: anchor,
296
- fontSize: labelSize,
297
- fontWeight: 600,
298
- className: FILL[tone],
299
- children: p.label
300
- }
301
- )
302
- ] }, i);
303
- })
601
+ points.map((p, i) => /* @__PURE__ */ jsxs("g", { children: [
602
+ p.state === "here" && /* @__PURE__ */ jsx2(
603
+ "circle",
604
+ {
605
+ cx: p.x,
606
+ cy: p.y,
607
+ r: labelSize * 0.85,
608
+ className: cn(FILL[markTone], "opacity-30")
609
+ }
610
+ ),
611
+ /* @__PURE__ */ jsx2(
612
+ "circle",
613
+ {
614
+ cx: p.x,
615
+ cy: p.y,
616
+ r: labelSize * 0.34,
617
+ strokeWidth: labelSize * 0.14,
618
+ className: cn(
619
+ STROKE[tone],
620
+ p.state === "ahead" ? FILL[markTone] : FILL[tone]
621
+ )
622
+ }
623
+ )
624
+ ] }, i))
304
625
  ] });
305
626
  }
306
627
 
307
628
  // src/material/band.tsx
308
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
629
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
309
630
  var PAD = {
310
- none: "",
311
- tight: "pb-16 md:pb-24",
631
+ none: "[&:has(+[data-band-edge])]:pb-16 md:[&:has(+[data-band-edge])]:pb-[120px]",
632
+ tight: "pb-16 md:pb-24 md:[&:has(+[data-band-edge])]:pb-[120px]",
312
633
  section: "py-20 md:py-28",
313
634
  open: "py-24 md:py-40"
314
635
  };
636
+ var UNDER_NIGHT_HEADER = [
637
+ "[[data-marketing-header='night']+&]:before:block [[data-marketing-header='night']+&]:before:h-16 [[data-marketing-header='night']+&]:before:content-['']",
638
+ "[[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-['']",
639
+ "[[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-['']"
640
+ ].join(" ");
315
641
  var EDGE_FIELD = { width: 3840, height: 240, seam: 120 };
642
+ function Bank({
643
+ field,
644
+ bank,
645
+ tone
646
+ }) {
647
+ return /* @__PURE__ */ jsx3("g", { className: FILL[tone], children: cloudBankPaths(field, bank, BLEED_BOTH_SIDES).map((d, i) => /* @__PURE__ */ jsx3("path", { d }, i)) });
648
+ }
316
649
  function BandEdge({ via, tone, seed }) {
317
650
  const { width, height, seam } = EDGE_FIELD;
318
651
  const overlap = 8;
@@ -320,46 +653,32 @@ function BandEdge({ via, tone, seed }) {
320
653
  "svg",
321
654
  {
322
655
  "aria-hidden": "true",
656
+ "data-band-edge-art": "",
323
657
  viewBox: `0 0 ${width} ${height}`,
324
658
  preserveAspectRatio: "xMidYMid slice",
325
659
  className: "pointer-events-none absolute inset-x-0 -top-16 h-32 w-full md:-top-[120px] md:h-[240px]",
326
660
  children: [
327
- via && /* @__PURE__ */ jsx2(
328
- CloudBank,
661
+ via && /* @__PURE__ */ jsx3(
662
+ Bank,
329
663
  {
330
- width,
331
- height: seam,
332
- y: 82,
333
- min: 24,
334
- max: 56,
335
- level: true,
336
- seed: seed + 3,
664
+ field: { width, height: seam },
665
+ bank: { y: 82, min: 24, max: 56, level: true, seed: seed + 3 },
337
666
  tone: via
338
667
  }
339
668
  ),
340
- /* @__PURE__ */ jsx2(
341
- CloudBank,
669
+ /* @__PURE__ */ jsx3(
670
+ Bank,
342
671
  {
343
- width,
344
- height: seam + overlap,
345
- y: 100,
346
- min: 22,
347
- max: 58,
348
- level: true,
349
- seed,
672
+ field: { width, height: seam + overlap },
673
+ bank: { y: 100, min: 22, max: 58, level: true, seed },
350
674
  tone
351
675
  }
352
676
  ),
353
- /* @__PURE__ */ jsx2("g", { transform: `matrix(1 0 0 -1 0 ${height})`, children: /* @__PURE__ */ jsx2(
354
- CloudBank,
677
+ /* @__PURE__ */ jsx3("g", { transform: `matrix(1 0 0 -1 0 ${height})`, children: /* @__PURE__ */ jsx3(
678
+ Bank,
355
679
  {
356
- width,
357
- height: seam + overlap,
358
- y: 72,
359
- min: 22,
360
- max: 58,
361
- level: true,
362
- seed: seed + 7,
680
+ field: { width, height: seam + overlap },
681
+ bank: { y: 72, min: 22, max: 58, level: true, seed: seed + 7 },
363
682
  tone
364
683
  }
365
684
  ) })
@@ -383,16 +702,18 @@ function Band({
383
702
  {
384
703
  id,
385
704
  "data-surface": surface,
705
+ "data-band-edge": edge ? "" : void 0,
386
706
  className: cn(
387
707
  "relative isolate scroll-mt-4",
388
708
  BG[tone],
389
709
  PAD[pad],
390
710
  under === "overhang" && "pt-44 md:pt-[23rem]",
391
711
  edge && "pt-28 md:pt-48",
392
- raised && "z-10"
712
+ raised && "z-10",
713
+ surface === "night" && UNDER_NIGHT_HEADER
393
714
  ),
394
715
  children: [
395
- backdrop && /* @__PURE__ */ jsx2(
716
+ backdrop && /* @__PURE__ */ jsx3(
396
717
  "div",
397
718
  {
398
719
  "aria-hidden": "true",
@@ -400,15 +721,15 @@ function Band({
400
721
  children: backdrop
401
722
  }
402
723
  ),
403
- edge && /* @__PURE__ */ jsx2(BandEdge, { ...edge, tone }),
404
- children
724
+ edge && /* @__PURE__ */ jsx3(BandEdge, { ...edge, tone }),
725
+ /* @__PURE__ */ jsx3(GroundContext.Provider, { value: tone, children })
405
726
  ]
406
727
  }
407
728
  );
408
729
  }
409
730
 
410
731
  // src/material/figure.tsx
411
- import { jsx as jsx3 } from "react/jsx-runtime";
732
+ import { jsx as jsx4 } from "react/jsx-runtime";
412
733
  var ASPECT = {
413
734
  "16:9": "aspect-video",
414
735
  "8:5": "aspect-[8/5]",
@@ -431,7 +752,7 @@ function recordable(spec) {
431
752
  }
432
753
  function Figure({ spec, alt, children, fit = "aspect" }) {
433
754
  recordable(spec);
434
- return /* @__PURE__ */ jsx3(
755
+ return /* @__PURE__ */ jsx4(
435
756
  "figure",
436
757
  {
437
758
  role: alt ? "img" : void 0,
@@ -450,8 +771,56 @@ function Figure({ spec, alt, children, fit = "aspect" }) {
450
771
  );
451
772
  }
452
773
 
774
+ // src/material/hero-scene.tsx
775
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
776
+ var STACK_ZONE_RATIO = 1.25;
777
+ var STACK_ZONE_MIN_WIDTH = 390;
778
+ var HEIGHT2 = {
779
+ fold: "lg:min-h-[min(calc(100svh-4rem),56rem)]",
780
+ tall: "lg:min-h-[46rem]"
781
+ };
782
+ var CLIP_ABOVE = "[clip-path:inset(0_0_-100vh_0)]";
783
+ function HeroScene({
784
+ height,
785
+ subject,
786
+ copySide = "start",
787
+ children
788
+ }) {
789
+ return /* @__PURE__ */ jsx5("div", { "data-hero-scene": "", className: cn("overflow-x-clip", CLIP_ABOVE), children: /* @__PURE__ */ jsxs3(
790
+ "div",
791
+ {
792
+ className: cn(
793
+ "mx-auto flex w-full max-w-content-xl flex-col lg:grid lg:grid-cols-2",
794
+ HEIGHT2[height]
795
+ ),
796
+ children: [
797
+ /* @__PURE__ */ jsx5(
798
+ "div",
799
+ {
800
+ "data-hero-copy": "",
801
+ className: cn(
802
+ "relative z-10 flex min-w-0 flex-col items-start justify-center gap-6 py-10 md:py-14",
803
+ CONTENT_GUTTER,
804
+ copySide === "end" && "lg:order-2"
805
+ ),
806
+ children
807
+ }
808
+ ),
809
+ /* @__PURE__ */ jsx5(
810
+ "div",
811
+ {
812
+ "data-hero-subject": "",
813
+ 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",
814
+ children: /* @__PURE__ */ jsx5(SceneCopyContext.Provider, { value: copySide, children: subject })
815
+ }
816
+ )
817
+ ]
818
+ }
819
+ ) });
820
+ }
821
+
453
822
  // src/material/layout.tsx
454
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
823
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
455
824
  var GAP = {
456
825
  1: "gap-1",
457
826
  2: "gap-2",
@@ -464,7 +833,7 @@ var GAP = {
464
833
  16: "gap-16"
465
834
  };
466
835
  function Stack({ gap, align = "stretch", children }) {
467
- return /* @__PURE__ */ jsx4(
836
+ return /* @__PURE__ */ jsx6(
468
837
  "div",
469
838
  {
470
839
  className: cn(
@@ -484,7 +853,7 @@ function Inline({
484
853
  fillOnPhone,
485
854
  children
486
855
  }) {
487
- return /* @__PURE__ */ jsx4(
856
+ return /* @__PURE__ */ jsx6(
488
857
  "div",
489
858
  {
490
859
  className: cn(
@@ -512,7 +881,7 @@ function Columns({
512
881
  connect,
513
882
  children
514
883
  }) {
515
- return /* @__PURE__ */ jsxs3(
884
+ return /* @__PURE__ */ jsxs4(
516
885
  "div",
517
886
  {
518
887
  className: cn(
@@ -526,13 +895,13 @@ function Columns({
526
895
  ),
527
896
  children: [
528
897
  children,
529
- phone === "scroll" && /* @__PURE__ */ jsx4(ScrollHintXMark, {})
898
+ phone === "scroll" && /* @__PURE__ */ jsx6(ScrollHintXMark, {})
530
899
  ]
531
900
  }
532
901
  );
533
902
  }
534
903
  function Split({ ratio, reverse, gap, children }) {
535
- return /* @__PURE__ */ jsxs3(
904
+ return /* @__PURE__ */ jsxs4(
536
905
  "div",
537
906
  {
538
907
  className: cn(
@@ -542,27 +911,26 @@ function Split({ ratio, reverse, gap, children }) {
542
911
  reverse && (ratio === "even" ? "md:[&>:first-child]:order-2" : "md:grid-cols-[7fr_5fr] md:[&>:first-child]:order-2")
543
912
  ),
544
913
  children: [
545
- /* @__PURE__ */ jsx4("div", { className: "min-w-0", children: children[0] }),
546
- /* @__PURE__ */ jsx4("div", { className: "min-w-0", children: children[1] })
914
+ /* @__PURE__ */ jsx6("div", { className: "min-w-0", children: children[0] }),
915
+ /* @__PURE__ */ jsx6("div", { className: "min-w-0", children: children[1] })
547
916
  ]
548
917
  }
549
918
  );
550
919
  }
551
920
  var SPACER = {
552
921
  hero: "h-10 md:h-14",
553
- section: "h-16 md:h-24",
554
- "phone-sky": "h-40 md:hidden"
922
+ section: "h-16 md:h-24"
555
923
  };
556
924
  function Spacer({ size }) {
557
- return /* @__PURE__ */ jsx4("div", { "aria-hidden": "true", className: SPACER[size] });
925
+ return /* @__PURE__ */ jsx6("div", { "aria-hidden": "true", className: SPACER[size] });
558
926
  }
559
927
  var INSET = { 3: "p-3", 4: "p-4", 6: "p-6" };
560
928
  function Inset({ pad, children }) {
561
- return /* @__PURE__ */ jsx4("div", { className: INSET[pad], children });
929
+ return /* @__PURE__ */ jsx6("div", { className: INSET[pad], children });
562
930
  }
563
931
 
564
932
  // src/material/type.tsx
565
- import { jsx as jsx5 } from "react/jsx-runtime";
933
+ import { jsx as jsx7 } from "react/jsx-runtime";
566
934
  var LEVEL = {
567
935
  poster: "text-display-poster",
568
936
  headline: "text-display-headline",
@@ -580,7 +948,7 @@ function Display({
580
948
  measure = "none",
581
949
  children
582
950
  }) {
583
- return /* @__PURE__ */ jsx5(
951
+ return /* @__PURE__ */ jsx7(
584
952
  Tag,
585
953
  {
586
954
  className: cn(
@@ -618,7 +986,7 @@ function Text({
618
986
  as: Tag = "p",
619
987
  children
620
988
  }) {
621
- return /* @__PURE__ */ jsx5(
989
+ return /* @__PURE__ */ jsx7(
622
990
  Tag,
623
991
  {
624
992
  className: cn(
@@ -634,7 +1002,7 @@ function Text({
634
1002
  }
635
1003
 
636
1004
  // src/material/vignette.tsx
637
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1005
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
638
1006
  function Vignette({
639
1007
  title,
640
1008
  icon,
@@ -644,7 +1012,7 @@ function Vignette({
644
1012
  crop = "none",
645
1013
  children
646
1014
  }) {
647
- return /* @__PURE__ */ jsxs4(
1015
+ return /* @__PURE__ */ jsxs5(
648
1016
  "div",
649
1017
  {
650
1018
  "data-surface": surface,
@@ -653,12 +1021,12 @@ function Vignette({
653
1021
  placement === "hang" && "z-10 -mb-32 md:-mb-52"
654
1022
  ),
655
1023
  children: [
656
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 border-b border-border bg-card px-4 py-2.5", children: [
657
- /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground", children: /* @__PURE__ */ jsx6(Icon, { name: icon, size: "sm" }) }),
658
- /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground", children: title }),
659
- /* @__PURE__ */ jsx6(IntentBadge, { intent: "neutral", children: sampleLabel })
1024
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 border-b border-border bg-card px-4 py-2.5", children: [
1025
+ /* @__PURE__ */ jsx8("span", { className: "text-muted-foreground", children: /* @__PURE__ */ jsx8(Icon, { name: icon, size: "sm" }) }),
1026
+ /* @__PURE__ */ jsx8("span", { className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground", children: title }),
1027
+ /* @__PURE__ */ jsx8(IntentBadge, { intent: "neutral", children: sampleLabel })
660
1028
  ] }),
661
- /* @__PURE__ */ jsx6(
1029
+ /* @__PURE__ */ jsx8(
662
1030
  "div",
663
1031
  {
664
1032
  className: cn(
@@ -675,31 +1043,36 @@ function Vignette({
675
1043
  }
676
1044
 
677
1045
  // src/material/wordmark.tsx
678
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1046
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
679
1047
  function Wordmark({ name, mark, href }) {
680
- return /* @__PURE__ */ jsxs5(
1048
+ return /* @__PURE__ */ jsxs6(
681
1049
  "a",
682
1050
  {
683
1051
  href,
684
1052
  className: "flex items-center gap-2 text-lg font-bold tracking-tight text-foreground [font-family:var(--font-heading,inherit)]",
685
1053
  children: [
686
- /* @__PURE__ */ jsx7("span", { className: "text-primary-text size-6 shrink-0", children: mark }),
1054
+ /* @__PURE__ */ jsx9("span", { className: "text-primary-text size-6 shrink-0", children: mark }),
687
1055
  name
688
1056
  ]
689
1057
  }
690
1058
  );
691
1059
  }
692
1060
  export {
1061
+ BLEED_BOTH_SIDES,
693
1062
  Band,
694
1063
  CloudBank,
695
1064
  Columns,
696
1065
  Display,
697
1066
  Figure,
698
1067
  Halo,
1068
+ HeroScene,
699
1069
  Inline,
700
1070
  Inset,
701
1071
  Ridge,
702
1072
  RouteLine,
1073
+ SCENE_MEASURES,
1074
+ STACK_ZONE_MIN_WIDTH,
1075
+ STACK_ZONE_RATIO,
703
1076
  SkyField,
704
1077
  Spacer,
705
1078
  Split,
@@ -707,5 +1080,8 @@ export {
707
1080
  Stars,
708
1081
  Text,
709
1082
  Vignette,
710
- Wordmark
1083
+ Wordmark,
1084
+ cloudBankPaths,
1085
+ ridgePath,
1086
+ starDots
711
1087
  };