@rizom/site-rizom 0.2.0-alpha.142

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/index.d.ts +135 -0
  2. package/dist/index.js +31089 -0
  3. package/dist/index.js.map +259 -0
  4. package/package.json +54 -0
  5. package/src/content.ts +9 -0
  6. package/src/contracts.ts +157 -0
  7. package/src/create-site.ts +108 -0
  8. package/src/index.ts +63 -0
  9. package/src/runtime/base-site.ts +13 -0
  10. package/src/runtime/boot/boot.boot.d.ts +2 -0
  11. package/src/runtime/boot/boot.boot.js +83 -0
  12. package/src/runtime/canvases/constellation.canvas.d.ts +2 -0
  13. package/src/runtime/canvases/constellation.canvas.js +307 -0
  14. package/src/runtime/canvases/prelude.canvas.d.ts +2 -0
  15. package/src/runtime/canvases/prelude.canvas.js +193 -0
  16. package/src/runtime/canvases/products.canvas.d.ts +2 -0
  17. package/src/runtime/canvases/roots.canvas.d.ts +2 -0
  18. package/src/runtime/canvases/roots.canvas.js +358 -0
  19. package/src/runtime/canvases/tree.canvas.d.ts +2 -0
  20. package/src/runtime/canvases/tree.canvas.js +544 -0
  21. package/src/runtime/default-layout.tsx +10 -0
  22. package/src/runtime/index.ts +8 -0
  23. package/src/runtime/plugin.ts +147 -0
  24. package/src/ui/Badge.tsx +14 -0
  25. package/src/ui/Button.tsx +56 -0
  26. package/src/ui/Divider.tsx +12 -0
  27. package/src/ui/Footer.tsx +76 -0
  28. package/src/ui/Header.tsx +45 -0
  29. package/src/ui/Section.tsx +22 -0
  30. package/src/ui/SideNav.tsx +22 -0
  31. package/src/ui/cn.ts +1 -0
  32. package/src/ui/external-link.ts +8 -0
  33. package/src/ui/frame.tsx +28 -0
  34. package/src/ui/highlighted-text.tsx +45 -0
  35. package/src/ui/index.ts +21 -0
  36. package/src/ui/site-info-links.ts +24 -0
  37. package/src/ui/types.ts +6 -0
@@ -0,0 +1,544 @@
1
+ (function () {
2
+ const canvas = document.getElementById("heroCanvas");
3
+ if (!canvas) return;
4
+ const W = window.innerWidth;
5
+ const H = window.innerHeight;
6
+ canvas.width = W * dpr;
7
+ canvas.height = H * dpr;
8
+ canvas.style.width = "100%";
9
+ canvas.style.height = "100%";
10
+ const ctx = canvas.getContext("2d");
11
+ ctx.scale(dpr, dpr);
12
+
13
+ // Full document height for the tree
14
+ const docH = Math.max(document.body.scrollHeight, 5000);
15
+ // Tree trunk runs along the right third of the page
16
+ const trunkX = W * 0.7;
17
+
18
+ const stripH = Math.min(docH, 8000);
19
+ let staticCanvas = document.createElement("canvas");
20
+ staticCanvas.width = canvas.width;
21
+ staticCanvas.height = stripH * dpr;
22
+
23
+ let nodes = [];
24
+ let builtTheme = null;
25
+ // Captured at build time so the animation loop can send pulses along them
26
+ let trunkPath = [];
27
+ let branchPaths = []; // each entry: [x0,y0,c1x,c1y,c2x,c2y,x1,y1]
28
+
29
+ function buildStatic() {
30
+ const light = isLightMode();
31
+ if (builtTheme === light) return;
32
+ builtTheme = light;
33
+ nodes = [];
34
+ trunkPath = [];
35
+ branchPaths = [];
36
+ const rng = createRand(42);
37
+ // Completely replace the static canvas — no chance of state leak
38
+ staticCanvas = document.createElement("canvas");
39
+ staticCanvas.width = canvas.width;
40
+ staticCanvas.height = stripH * dpr;
41
+ const sctx = staticCanvas.getContext("2d");
42
+ sctx.scale(dpr, dpr);
43
+
44
+ // Ambient glows at different heights
45
+ const ambients = [
46
+ { y: stripH * 0.08, r: W * 0.4 },
47
+ { y: stripH * 0.35, r: W * 0.35 },
48
+ { y: stripH * 0.6, r: W * 0.3 },
49
+ { y: stripH * 0.85, r: W * 0.25 },
50
+ ];
51
+ ambients.forEach((a) => {
52
+ const ag = sctx.createRadialGradient(trunkX, a.y, 0, trunkX, a.y, a.r);
53
+ if (!light) {
54
+ ag.addColorStop(0, rgba(C.AMBER, 0.04));
55
+ ag.addColorStop(0.5, rgba(C.PURPLE, 0.01));
56
+ ag.addColorStop(1, rgba(C.BG_DEEP, 0));
57
+ } else {
58
+ ag.addColorStop(0, rgba(C.AMBER_DK, 0.03));
59
+ ag.addColorStop(1, rgba(C.AMBER_DK, 0));
60
+ }
61
+ sctx.fillStyle = ag;
62
+ sctx.fillRect(0, a.y - a.r, W, a.r * 2);
63
+ });
64
+
65
+ // Branch helper
66
+ function branch(x, y, angle, len, w, depth, color, op) {
67
+ if (depth <= 0 || w < 0.15 || len < 4) return;
68
+ const j = rng.range(-0.15, 0.15);
69
+ const ex = x + Math.cos(angle + j) * len;
70
+ const ey = y + Math.sin(angle + j) * len;
71
+ const c1x = x + Math.cos(angle + rng.range(-0.4, 0.4)) * len * 0.35;
72
+ const c1y = y + Math.sin(angle + rng.range(-0.4, 0.4)) * len * 0.35;
73
+ const c2x = x + Math.cos(angle + rng.range(-0.25, 0.25)) * len * 0.7;
74
+ const c2y = y + Math.sin(angle + rng.range(-0.25, 0.25)) * len * 0.7;
75
+ drawGlowBezier(
76
+ sctx,
77
+ [x, y, c1x, c1y, c2x, c2y, ex, ey],
78
+ color,
79
+ w,
80
+ Math.max(2, w * 3),
81
+ op,
82
+ light,
83
+ );
84
+ // Capture substantial branches so the animation loop can run
85
+ // sap pulses along their bezier curves
86
+ if (w >= 1.4) {
87
+ branchPaths.push([x, y, c1x, c1y, c2x, c2y, ex, ey]);
88
+ }
89
+ if (depth <= 2) {
90
+ const nr = Math.max(0.5, w * 0.4);
91
+ drawGlowNode(sctx, ex, ey, nr, color, op * 0.4, light);
92
+ nodes.push({
93
+ x: ex,
94
+ y: ey,
95
+ r: nr,
96
+ color,
97
+ op: op * 0.4,
98
+ phase: rng.next() * Math.PI * 2,
99
+ });
100
+ }
101
+ const nb =
102
+ depth > 3 ? Math.floor(rng.range(2, 3)) : Math.floor(rng.range(1, 3));
103
+ for (let i = 0; i < nb; i++) {
104
+ const sp = depth > 3 ? rng.range(0.2, 0.6) : rng.range(0.3, 0.9);
105
+ let nc = color;
106
+ if (depth <= 3 && rng.next() < 0.2) nc = C.PURPLE;
107
+ else if (rng.next() < 0.3) nc = nc === C.AMBER ? C.AMBER_LT : C.AMBER;
108
+ branch(
109
+ ex,
110
+ ey,
111
+ angle + rng.range(-sp, sp),
112
+ len * rng.range(0.45, 0.75),
113
+ w * rng.range(0.45, 0.7),
114
+ depth - 1,
115
+ nc,
116
+ op * rng.range(0.5, 0.8),
117
+ );
118
+ }
119
+ }
120
+
121
+ // === MAIN TRUNK — continuous line from bottom to top ===
122
+ const segments = 22;
123
+ let prevX = trunkX + rng.range(-10, 10);
124
+ let prevY = stripH;
125
+ const trunkPoints = [{ x: prevX, y: prevY }];
126
+
127
+ for (let i = 0; i < segments; i++) {
128
+ const t = (i + 1) / segments;
129
+ const nextY = stripH * (1 - t);
130
+ const drift = rng.range(-40, 40);
131
+ const nextX = trunkX + drift + Math.sin(t * Math.PI * 2) * 30;
132
+ trunkPoints.push({ x: nextX, y: nextY });
133
+
134
+ // Draw trunk segment
135
+ const w = 5 * (1 - t * 0.15); // thicker trunk
136
+ const op = 0.55 * (1 - t * 0.1);
137
+ const c = i % 2 === 0 ? C.AMBER : C.AMBER_LT;
138
+ drawGlowBezier(
139
+ sctx,
140
+ [
141
+ prevX,
142
+ prevY,
143
+ prevX + rng.range(-20, 20),
144
+ prevY - (prevY - nextY) * 0.35,
145
+ nextX + rng.range(-15, 15),
146
+ nextY + (prevY - nextY) * 0.3,
147
+ nextX,
148
+ nextY,
149
+ ],
150
+ c,
151
+ w,
152
+ Math.max(3, w * 3.5),
153
+ op,
154
+ light,
155
+ );
156
+
157
+ // Hub node at each segment junction
158
+ const hubR = 3 + (1 - t) * 4;
159
+ drawGlowNode(sctx, nextX, nextY, hubR, C.AMBER_LT, op * 0.8, light);
160
+ nodes.push({
161
+ x: nextX,
162
+ y: nextY,
163
+ r: hubR,
164
+ color: C.AMBER_LT,
165
+ op: op * 0.8,
166
+ phase: i * 0.9,
167
+ });
168
+
169
+ // Branches at each junction — reaching left and right
170
+ const branchCount = Math.floor(rng.range(3, 5));
171
+ for (let b = 0; b < branchCount; b++) {
172
+ const side = rng.next() < 0.5 ? -1 : 1;
173
+ const bAngle =
174
+ side * rng.range(0.2, 1.4) +
175
+ (rng.next() < 0.4 ? Math.PI * 0.5 * side : 0);
176
+ const bLen = rng.range(80, 250);
177
+ const bW = rng.range(1.2, 3);
178
+ const bOp = rng.range(0.18, 0.42);
179
+ const bColor =
180
+ rng.next() < 0.7
181
+ ? rng.next() < 0.5
182
+ ? C.AMBER
183
+ : C.AMBER_LT
184
+ : C.PURPLE;
185
+ branch(
186
+ nextX,
187
+ nextY,
188
+ bAngle,
189
+ bLen,
190
+ bW,
191
+ Math.floor(rng.range(3, 5)),
192
+ bColor,
193
+ bOp,
194
+ );
195
+ }
196
+
197
+ prevX = nextX;
198
+ prevY = nextY;
199
+ }
200
+ // Snapshot the trunk polyline for the animation loop to traverse
201
+ trunkPath = trunkPoints.slice();
202
+
203
+ // Secondary strands alongside trunk
204
+ for (let strand = 0; strand < 3; strand++) {
205
+ const offset = (strand - 1) * 20 + rng.range(-5, 5);
206
+ let spx = trunkX + offset;
207
+ let spy = stripH;
208
+ const strandW = [2, 1.5, 1][strand];
209
+ const strandOp = [0.25, 0.18, 0.12][strand];
210
+ for (let i = 0; i < segments - 2; i++) {
211
+ const t = (i + 1) / segments;
212
+ const ref = trunkPoints[i + 1];
213
+ const nx = ref.x + rng.range(-25, 35);
214
+ const ny = ref.y + rng.range(-30, 30);
215
+ drawGlowBezier(
216
+ sctx,
217
+ [
218
+ spx,
219
+ spy,
220
+ spx + rng.range(-15, 15),
221
+ (spy + ny) * 0.5,
222
+ nx + rng.range(-15, 15),
223
+ (spy + ny) * 0.5 + rng.range(-25, 25),
224
+ nx,
225
+ ny,
226
+ ],
227
+ strand < 2 ? C.AMBER_LT : C.AMBER,
228
+ strandW * (1 - t * 0.4),
229
+ 6,
230
+ strandOp * (1 - t * 0.3),
231
+ light,
232
+ );
233
+ // Extra small branches off secondary strands
234
+ if (rng.next() < 0.4) {
235
+ const a = rng.range(-1.5, 1.5);
236
+ branch(
237
+ nx,
238
+ ny,
239
+ a,
240
+ rng.range(40, 100),
241
+ rng.range(0.5, 1.5),
242
+ 3,
243
+ C.AMBER_LT,
244
+ strandOp * 0.5,
245
+ light,
246
+ );
247
+ }
248
+ spx = nx;
249
+ spy = ny;
250
+ }
251
+ }
252
+ }
253
+
254
+ // Particles — spread across full viewport
255
+ const particles = [];
256
+ const rngP = createRand(99);
257
+ const dustC = [C.AMBER_LT, C.AMBER, C.PURPLE, C.PURPLE_LT, C.GLOW];
258
+ for (let i = 0; i < 200; i++) {
259
+ particles.push({
260
+ x: rngP.range(0, W),
261
+ y: rngP.range(0, H),
262
+ r: rngP.range(0.3, 1.5),
263
+ vx: rngP.range(-0.06, 0.06),
264
+ vy: rngP.range(-0.1, 0.03),
265
+ color: dustC[Math.floor(rngP.next() * dustC.length)],
266
+ alpha: rngP.range(0.03, 0.15),
267
+ phase: rngP.next() * Math.PI * 2,
268
+ });
269
+ }
270
+
271
+ buildStatic();
272
+ window._heroRebuild = function () {
273
+ builtTheme = null;
274
+ buildStatic();
275
+ };
276
+
277
+ // Sap pulses traveling along trunk and branches
278
+ const pulses = [];
279
+ let lastPulseSpawn = 0;
280
+ let lastFlareSpawn = 0;
281
+
282
+ function spawnTrunkPulse() {
283
+ if (trunkPath.length < 2) return;
284
+ pulses.push({
285
+ type: "trunk",
286
+ seg: 0,
287
+ progress: 0,
288
+ speed: 0.8 + Math.random() * 0.55,
289
+ color: Math.random() < 0.6 ? C.AMBER_LT : C.GLOW,
290
+ });
291
+ }
292
+
293
+ function spawnBranchPulse() {
294
+ if (!branchPaths.length) return;
295
+ pulses.push({
296
+ type: "branch",
297
+ pathIdx: Math.floor(Math.random() * branchPaths.length),
298
+ progress: 0,
299
+ speed: 0.9 + Math.random() * 0.75,
300
+ color: Math.random() < 0.7 ? C.AMBER_LT : C.AMBER,
301
+ });
302
+ }
303
+
304
+ function bezierPoint(p, u) {
305
+ const mu = 1 - u;
306
+ const mu2 = mu * mu;
307
+ const u2 = u * u;
308
+ return [
309
+ mu2 * mu * p[0] + 3 * mu2 * u * p[2] + 3 * mu * u2 * p[4] + u2 * u * p[6],
310
+ mu2 * mu * p[1] + 3 * mu2 * u * p[3] + 3 * mu * u2 * p[5] + u2 * u * p[7],
311
+ ];
312
+ }
313
+
314
+ // Released spores — bright motes that drift outward from random visible
315
+ // tree nodes, biased toward the empty side of the canvas so the whole
316
+ // viewport gets ambient motion (not just the right side where the tree lives)
317
+ const spores = [];
318
+ let lastSporeRelease = 0;
319
+
320
+ function releaseSpore(srcY) {
321
+ // Only spawn from nodes near the trunk axis so spores genuinely
322
+ // radiate from the spine, not from arbitrary branch tips
323
+ const trunkRange = 90;
324
+ const candidates = [];
325
+ for (const n of nodes) {
326
+ const sy = n.y - srcY;
327
+ if (sy > -50 && sy < H + 50 && Math.abs(n.x - trunkX) < trunkRange) {
328
+ candidates.push(n);
329
+ }
330
+ }
331
+ if (!candidates.length) return;
332
+ const n = candidates[Math.floor(Math.random() * candidates.length)];
333
+ // Mostly leftward drift (outward into empty space) with some up/down spread
334
+ const angle = Math.PI + (Math.random() - 0.5) * Math.PI * 0.75;
335
+ const speed = 14 + Math.random() * 18;
336
+ spores.push({
337
+ x: n.x,
338
+ y: n.y,
339
+ vx: Math.cos(angle) * speed,
340
+ vy: Math.sin(angle) * speed,
341
+ age: 0,
342
+ maxAge: 5 + Math.random() * 3,
343
+ color: Math.random() < 0.7 ? C.AMBER_LT : C.GLOW,
344
+ r: 0.9 + Math.random() * 0.8,
345
+ });
346
+ }
347
+
348
+ let t = 0,
349
+ lastTime = 0;
350
+ function animate(timestamp) {
351
+ if (document.hidden) {
352
+ requestAnimationFrame(animate);
353
+ return;
354
+ }
355
+ const dt = lastTime ? Math.min((timestamp - lastTime) / 1000, 0.05) : 0.016;
356
+ lastTime = timestamp;
357
+ t += dt;
358
+
359
+ const light = isLightMode();
360
+ const scrollY = window.scrollY || window.pageYOffset;
361
+ const scrollRatio = (scrollY * 0.7) / Math.max(1, docH - H);
362
+
363
+ ctx.clearRect(0, 0, W, H);
364
+
365
+ // Draw the visible slice of the static tree
366
+ // Map scroll position to the strip
367
+ const srcY = scrollRatio * (stripH - H);
368
+ // Source rect in the offscreen canvas (in device pixels)
369
+ ctx.drawImage(
370
+ staticCanvas,
371
+ 0,
372
+ srcY * dpr,
373
+ W * dpr,
374
+ H * dpr, // source
375
+ 0,
376
+ 0,
377
+ W,
378
+ H, // destination
379
+ );
380
+
381
+ // Animated particles (viewport-local)
382
+ particles.forEach((p) => {
383
+ p.x += p.vx;
384
+ p.y += p.vy;
385
+ if (p.x < 0) p.x = W;
386
+ if (p.x > W) p.x = 0;
387
+ if (p.y < 0) p.y = H;
388
+ if (p.y > H) p.y = 0;
389
+ const fl = 0.5 + 0.5 * Math.sin(t * 1.5 + p.phase);
390
+ ctx.fillStyle = rgba(p.color, p.alpha * 2 * fl);
391
+ ctx.beginPath();
392
+ ctx.arc(
393
+ p.x,
394
+ p.y,
395
+ p.r * (0.8 + 0.3 * Math.sin(t + p.phase)),
396
+ 0,
397
+ Math.PI * 2,
398
+ );
399
+ ctx.fill();
400
+ });
401
+
402
+ // Spawn sap pulses — trunk pulses bring life up the spine,
403
+ // branch pulses scatter through the limbs.
404
+ // Tuned for the ai site's kinetic energy: shorter interval, higher
405
+ // probability, occasional triple branch spawns.
406
+ if (timestamp - lastPulseSpawn > 200) {
407
+ lastPulseSpawn = timestamp;
408
+ if (Math.random() < 0.7) spawnTrunkPulse();
409
+ if (Math.random() < 0.95) spawnBranchPulse();
410
+ if (Math.random() < 0.55) spawnBranchPulse();
411
+ if (Math.random() < 0.3) spawnBranchPulse();
412
+ }
413
+
414
+ // Animate + draw pulses (world-space coords, drawn relative to srcY)
415
+ for (let i = pulses.length - 1; i >= 0; i--) {
416
+ const p = pulses[i];
417
+ p.progress += dt * p.speed;
418
+ let px = 0,
419
+ py = 0,
420
+ alive = true;
421
+ if (p.type === "trunk") {
422
+ if (p.progress >= 1) {
423
+ // Often fork onto a branch when arriving at a junction
424
+ if (Math.random() < 0.55) spawnBranchPulse();
425
+ p.seg++;
426
+ p.progress = 0;
427
+ if (p.seg >= trunkPath.length - 1) {
428
+ alive = false;
429
+ }
430
+ }
431
+ if (alive) {
432
+ const a = trunkPath[p.seg];
433
+ const b = trunkPath[p.seg + 1];
434
+ const e = p.progress * p.progress * (3 - 2 * p.progress);
435
+ px = a.x + (b.x - a.x) * e;
436
+ py = a.y + (b.y - a.y) * e;
437
+ }
438
+ } else {
439
+ if (p.progress >= 1) {
440
+ alive = false;
441
+ }
442
+ if (alive) {
443
+ const path = branchPaths[p.pathIdx];
444
+ const pt = bezierPoint(path, p.progress);
445
+ px = pt[0];
446
+ py = pt[1];
447
+ }
448
+ }
449
+ if (!alive) {
450
+ pulses.splice(i, 1);
451
+ continue;
452
+ }
453
+ const screenY = py - srcY;
454
+ if (screenY < -20 || screenY > H + 20) continue;
455
+ const fade = Math.sin(p.progress * Math.PI); // fade in + out at endpoints
456
+ ctx.fillStyle = rgba(p.color, (light ? 0.32 : 0.42) * fade);
457
+ ctx.beginPath();
458
+ ctx.arc(px, screenY, 6, 0, Math.PI * 2);
459
+ ctx.fill();
460
+ ctx.fillStyle = rgba(p.color, (light ? 0.5 : 0.6) * fade);
461
+ ctx.beginPath();
462
+ ctx.arc(px, screenY, 2.4, 0, Math.PI * 2);
463
+ ctx.fill();
464
+ ctx.fillStyle = rgba(light ? C.AMBER_DK : C.CORE, 0.85 * fade);
465
+ ctx.beginPath();
466
+ ctx.arc(px, screenY, 1.2, 0, Math.PI * 2);
467
+ ctx.fill();
468
+ }
469
+
470
+ // Spawn + animate drifting spores released from the tree
471
+ if (timestamp - lastSporeRelease > 200) {
472
+ lastSporeRelease = timestamp;
473
+ releaseSpore(srcY);
474
+ if (Math.random() < 0.85) releaseSpore(srcY);
475
+ if (Math.random() < 0.35) releaseSpore(srcY);
476
+ }
477
+ for (let i = spores.length - 1; i >= 0; i--) {
478
+ const sp = spores[i];
479
+ sp.x += sp.vx * dt;
480
+ sp.y += sp.vy * dt;
481
+ sp.vy += 4 * dt; // gentle gravity
482
+ sp.age += dt;
483
+ if (sp.age > sp.maxAge) {
484
+ spores.splice(i, 1);
485
+ continue;
486
+ }
487
+ const screenY = sp.y - srcY;
488
+ if (screenY < -20 || screenY > H + 20 || sp.x < -20 || sp.x > W + 20)
489
+ continue;
490
+ const lr = sp.age / sp.maxAge;
491
+ const alpha =
492
+ lr < 0.1 ? lr / 0.1 : lr > 0.7 ? Math.max(0, (1 - lr) / 0.3) : 1;
493
+ ctx.fillStyle = rgba(sp.color, alpha * 0.18);
494
+ ctx.beginPath();
495
+ ctx.arc(sp.x, screenY, sp.r * 4, 0, Math.PI * 2);
496
+ ctx.fill();
497
+ ctx.fillStyle = rgba(light ? C.AMBER_DK : sp.color, alpha * 0.85);
498
+ ctx.beginPath();
499
+ ctx.arc(sp.x, screenY, sp.r, 0, Math.PI * 2);
500
+ ctx.fill();
501
+ }
502
+
503
+ // Trigger random flares on visible mid+large nodes
504
+ if (timestamp - lastFlareSpawn > 600 && Math.random() < 0.7) {
505
+ lastFlareSpawn = timestamp;
506
+ const candidates = [];
507
+ for (const n of nodes) {
508
+ const sy = n.y - srcY;
509
+ if (sy > -50 && sy < H + 50 && n.r > 1.5) candidates.push(n);
510
+ }
511
+ if (candidates.length) {
512
+ const n = candidates[Math.floor(Math.random() * candidates.length)];
513
+ n.flare = 1.0;
514
+ }
515
+ }
516
+
517
+ // Pulsing nodes (with optional flare boost) — only visible window
518
+ nodes.forEach((n) => {
519
+ if (n.flare > 0) n.flare = Math.max(0, n.flare - dt * 0.85);
520
+ const screenY = n.y - srcY;
521
+ if (screenY < -100 || screenY > H + 100) return;
522
+ const pulse = 0.6 + 0.4 * Math.sin(t * 1.2 + n.phase);
523
+ const flareBoost = 1 + (n.flare || 0) * 2.5;
524
+ const pr = n.r * (2 + Math.sin(t * 0.7 + n.phase)) * flareBoost;
525
+ ctx.fillStyle = rgba(n.color, n.op * 0.1 * pulse * flareBoost);
526
+ ctx.beginPath();
527
+ ctx.arc(n.x, screenY, pr * 5, 0, Math.PI * 2);
528
+ ctx.fill();
529
+ ctx.fillStyle = rgba(C.GLOW, n.op * 0.12 * pulse * flareBoost);
530
+ ctx.beginPath();
531
+ ctx.arc(n.x, screenY, pr * 2, 0, Math.PI * 2);
532
+ ctx.fill();
533
+ if (n.flare > 0.05) {
534
+ ctx.fillStyle = rgba(C.CORE, n.flare * 0.9);
535
+ ctx.beginPath();
536
+ ctx.arc(n.x, screenY, n.r * 0.6, 0, Math.PI * 2);
537
+ ctx.fill();
538
+ }
539
+ });
540
+
541
+ requestAnimationFrame(animate);
542
+ }
543
+ requestAnimationFrame(animate);
544
+ })();
@@ -0,0 +1,10 @@
1
+ import type { JSX } from "preact";
2
+ import { RizomFrame, type RizomLayoutProps } from "../ui";
3
+
4
+ export const DefaultRizomLayout = ({
5
+ sections,
6
+ }: RizomLayoutProps): JSX.Element => (
7
+ <RizomFrame>
8
+ <main>{sections}</main>
9
+ </RizomFrame>
10
+ );
@@ -0,0 +1,8 @@
1
+ export { DefaultRizomLayout } from "./default-layout";
2
+ export { rizomBaseSite } from "./base-site";
3
+ export {
4
+ RizomRuntimePlugin,
5
+ buildRizomHeadScript,
6
+ rizomRuntimeStaticAssets,
7
+ } from "./plugin";
8
+ export type { RizomRuntimeConfig, RizomThemeProfile } from "./plugin";
@@ -0,0 +1,147 @@
1
+ import { listCanonicalAtprotoLexicons } from "@brains/atproto-contracts";
2
+ import type {
3
+ RizomPluginCapabilities,
4
+ RizomRuntimeConfig,
5
+ RizomSiteShell,
6
+ RizomThemeProfile,
7
+ } from "../contracts";
8
+ import canvasPrelude from "./canvases/prelude.canvas.js" with { type: "text" };
9
+ import treeCanvas from "./canvases/tree.canvas.js" with { type: "text" };
10
+ import constellationCanvas from "./canvases/constellation.canvas.js" with { type: "text" };
11
+ import rootsCanvas from "./canvases/roots.canvas.js" with { type: "text" };
12
+ import bootScript from "./boot/boot.boot.js" with { type: "text" };
13
+
14
+ export type { RizomRuntimeConfig, RizomThemeProfile } from "../contracts";
15
+
16
+ const THEME_PROFILES = new Set<string>(["product", "editorial", "studio"]);
17
+
18
+ function isRizomThemeProfile(value: unknown): value is RizomThemeProfile {
19
+ return typeof value === "string" && THEME_PROFILES.has(value);
20
+ }
21
+
22
+ function parseRuntimeConfig(
23
+ config: Record<string, unknown>,
24
+ ): RizomRuntimeConfig {
25
+ const themeProfile = config["themeProfile"];
26
+ const theme = config["theme"];
27
+
28
+ if (themeProfile !== undefined && !isRizomThemeProfile(themeProfile)) {
29
+ throw new Error(
30
+ `Invalid rizom site themeProfile ${JSON.stringify(themeProfile)}; expected one of: ${[...THEME_PROFILES].join(", ")}`,
31
+ );
32
+ }
33
+ if (theme !== undefined && typeof theme !== "string") {
34
+ throw new Error(
35
+ `Invalid rizom site theme ${JSON.stringify(theme)}; expected a package name string`,
36
+ );
37
+ }
38
+
39
+ return {
40
+ ...(themeProfile !== undefined ? { themeProfile } : {}),
41
+ ...(theme !== undefined ? { theme } : {}),
42
+ };
43
+ }
44
+
45
+ const CANVAS_BY_THEME_PROFILE: Record<RizomThemeProfile, string> = {
46
+ product: "/canvases/tree.canvas.js",
47
+ editorial: "/canvases/roots.canvas.js",
48
+ studio: "/canvases/constellation.canvas.js",
49
+ };
50
+
51
+ function getRizomCanvasPath(
52
+ themeProfile?: RizomThemeProfile,
53
+ ): string | undefined {
54
+ return themeProfile ? CANVAS_BY_THEME_PROFILE[themeProfile] : undefined;
55
+ }
56
+
57
+ export function buildRizomHeadScript(themeProfile?: RizomThemeProfile): string {
58
+ const canvasPath = getRizomCanvasPath(themeProfile);
59
+ const scripts = [`<script src="/boot.js" defer></script>`];
60
+
61
+ if (themeProfile) {
62
+ const themeProfileJson = JSON.stringify(themeProfile);
63
+ scripts.unshift(
64
+ `<script>document.documentElement.setAttribute("data-theme-profile", ${themeProfileJson});</script>`,
65
+ );
66
+ }
67
+
68
+ if (canvasPath) {
69
+ scripts.push(`<script src="/canvases/prelude.canvas.js" defer></script>`);
70
+ scripts.push(`<script src="${canvasPath}" defer></script>`);
71
+ }
72
+
73
+ return scripts.join("");
74
+ }
75
+
76
+ export const RIZOM_ATPROTO_LEXICON_BASE_PATH = "/atproto/lexicons";
77
+
78
+ function formatLexiconJson(lexicon: unknown): string {
79
+ return `${JSON.stringify(lexicon, null, 2)}\n`;
80
+ }
81
+
82
+ export const rizomAtprotoLexiconStaticAssets: Record<string, string> =
83
+ Object.fromEntries(
84
+ listCanonicalAtprotoLexicons().map((lexicon) => [
85
+ `${RIZOM_ATPROTO_LEXICON_BASE_PATH}/${lexicon.id}.json`,
86
+ formatLexiconJson(lexicon),
87
+ ]),
88
+ );
89
+
90
+ export const rizomRuntimeStaticAssets: Record<string, string> = {
91
+ ...rizomAtprotoLexiconStaticAssets,
92
+ "/canvases/prelude.canvas.js": canvasPrelude,
93
+ "/canvases/tree.canvas.js": treeCanvas,
94
+ "/canvases/constellation.canvas.js": constellationCanvas,
95
+ "/canvases/roots.canvas.js": rootsCanvas,
96
+ "/boot.js": bootScript,
97
+ };
98
+
99
+ export class RizomRuntimePlugin {
100
+ public readonly id = "rizom-site";
101
+ public readonly version = "0.1.0";
102
+ public readonly type = "service" as const;
103
+ public readonly packageName: string;
104
+ public readonly description: string;
105
+ public readonly config: RizomRuntimeConfig;
106
+
107
+ constructor(packageName: string, config: Record<string, unknown> = {}) {
108
+ this.packageName = packageName;
109
+ this.description = `${packageName} plugin`;
110
+ this.config = parseRuntimeConfig(config);
111
+ }
112
+
113
+ async register(
114
+ shell: RizomSiteShell,
115
+ _context?: unknown,
116
+ ): Promise<RizomPluginCapabilities> {
117
+ await this.onRegister(shell);
118
+ return { tools: [], resources: [] };
119
+ }
120
+
121
+ protected async onRegister(shell: RizomSiteShell): Promise<void> {
122
+ const themeProfile = this.getThemeProfile();
123
+ const messaging = shell.getMessageBus();
124
+
125
+ messaging.subscribe("system:plugins:ready", async () => {
126
+ await messaging.send({
127
+ type: "plugin:site-builder:head-script:register",
128
+ sender: this.id,
129
+ payload: {
130
+ pluginId: this.id,
131
+ script: buildRizomHeadScript(themeProfile),
132
+ },
133
+ });
134
+ return { success: true };
135
+ });
136
+
137
+ shell
138
+ .getLogger()
139
+ .info(
140
+ `Rizom runtime plugin registered${themeProfile ? ` (theme profile: ${themeProfile})` : ""}`,
141
+ );
142
+ }
143
+
144
+ protected getThemeProfile(): RizomThemeProfile | undefined {
145
+ return this.config.themeProfile;
146
+ }
147
+ }