@rizom/site-rizom-ai 0.2.0-alpha.261 → 0.2.0-alpha.263

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rizom/site-rizom-ai",
3
- "version": "0.2.0-alpha.261",
3
+ "version": "0.2.0-alpha.263",
4
4
  "description": "Consolidated Rizom brand site, runtime, layouts, and chrome",
5
5
  "type": "module",
6
6
  "exports": {
package/src/opening.tsx CHANGED
@@ -7,6 +7,13 @@ import {
7
7
  import { ASK_BOX_SCRIPT_PATH } from "@brains/contracts";
8
8
  import { StructuredContentFormatter } from "@brains/content-formatters";
9
9
  import { AskBoxHost } from "@brains/site-atlas";
10
+ import {
11
+ ASK_COLUMN_ATTRIBUTE,
12
+ ASK_DRAWING_ATTRIBUTE,
13
+ ASK_LEADS_ATTRIBUTE,
14
+ ASK_MARK_ATTRIBUTE,
15
+ ASK_ROOM_ATTRIBUTE,
16
+ } from "@brains/contracts";
10
17
  import { createTemplate, type Template } from "@brains/templates";
11
18
  import { z } from "@rizom/site";
12
19
  import { placeNetwork, type PlacedBrain } from "./story/network";
@@ -48,11 +55,19 @@ const replyDelay = (reach: number, after = 0): string =>
48
55
  export function NetworkLayer({
49
56
  brains,
50
57
  kin,
51
- }: ReturnType<typeof placeNetwork>): JSX.Element | null {
58
+ lendable = false,
59
+ }: ReturnType<typeof placeNetwork> & {
60
+ /** The Ask room lends this drawing to a phone's open conversation (the opening's; a chapter's stays put). */
61
+ lendable?: boolean;
62
+ }): JSX.Element | null {
52
63
  if (brains.length === 0) return null;
53
64
  const byId = new Map(brains.map((brain) => [brain.id, brain]));
54
65
  return (
55
- <div className="net-layer" aria-hidden="true">
66
+ <div
67
+ className="net-layer"
68
+ aria-hidden="true"
69
+ {...(lendable ? { [ASK_DRAWING_ATTRIBUTE]: "" } : {})}
70
+ >
56
71
  <svg className="net-svg" viewBox="0 0 100 100">
57
72
  {[14.7, 29.3, 44].map((r) => (
58
73
  <circle key={r} className="net-ring" cx="50" cy="50" r={r} />
@@ -125,6 +140,7 @@ export function NetworkLayer({
125
140
  key={brain.id}
126
141
  className="net-mark"
127
142
  data-brain={brain.id}
143
+ {...{ [ASK_MARK_ATTRIBUTE]: brain.id }}
128
144
  style={{ left: `${brain.x}%`, top: `${brain.y}%` }}
129
145
  >
130
146
  <a href={`/agents/${brain.id}`} aria-label={brain.name}>
@@ -153,45 +169,66 @@ export function Opening(data: OpeningData): JSX.Element {
153
169
  const headingLead = data.headingLead ?? DEFAULT_COPY.headingLead;
154
170
  const lede = data.lede ?? DEFAULT_COPY.lede;
155
171
  return (
156
- <section id="hero" className="chapter chapter--opening" data-title="Top">
157
- {data.kicker && <p className="eyebrow">{data.kicker}</p>}
158
- <h1>
159
- {headingLead}
160
- {data.headingAccent && (
161
- <>
162
- {" "}
163
- <em>{data.headingAccent}</em>
164
- </>
165
- )}
166
- </h1>
167
- <p className="lede">{lede}</p>
168
- {data.askBox ? (
169
- <div className="ask">
170
- <AskBoxHost prefix="opening" placeholder={data.prompt ?? undefined} />
171
- {data.topics.length > 0 && (
172
- <ul className="ask__topics" aria-label="Suggested questions">
173
- {data.topics.map((topic) => (
174
- <li key={topic}>
175
- <button type="button" data-atlas-fill={topic}>
176
- {topic}
177
- </button>
178
- </li>
179
- ))}
180
- </ul>
172
+ // With the box, the opening is an Ask room (@brains/contracts ask-box):
173
+ // the words are its column, which scrolls within the screen beside the
174
+ // drawing on a wide screen; the drawing is lent to a phone's open
175
+ // conversation; the room draws leads from an answer's sources to the dots.
176
+ <section
177
+ id="hero"
178
+ className="chapter chapter--opening"
179
+ data-title="Top"
180
+ {...(data.askBox ? { [ASK_ROOM_ATTRIBUTE]: "" } : {})}
181
+ >
182
+ <div
183
+ className="opening__words"
184
+ {...(data.askBox ? { [ASK_COLUMN_ATTRIBUTE]: "" } : {})}
185
+ >
186
+ {data.kicker && <p className="eyebrow">{data.kicker}</p>}
187
+ <h1>
188
+ {headingLead}
189
+ {data.headingAccent && (
190
+ <>
191
+ {" "}
192
+ <em>{data.headingAccent}</em>
193
+ </>
181
194
  )}
182
- <script src={ASK_BOX_SCRIPT_PATH} defer />
183
- </div>
184
- ) : (
185
- data.ctaHref &&
186
- data.ctaLabel && (
187
- <a className="opening__door" href={data.ctaHref}>
188
- {data.ctaLabel}
189
- </a>
190
- )
191
- )}
192
- <NetworkLayer {...placeNetwork(data)} />
195
+ </h1>
196
+ <p className="lede">{lede}</p>
197
+ {data.askBox ? (
198
+ <div className="ask">
199
+ <AskBoxHost
200
+ prefix="opening"
201
+ placeholder={data.prompt ?? undefined}
202
+ />
203
+ {data.topics.length > 0 && (
204
+ <ul className="ask__topics" aria-label="Suggested questions">
205
+ {data.topics.map((topic) => (
206
+ <li key={topic}>
207
+ <button type="button" data-atlas-fill={topic}>
208
+ {topic}
209
+ </button>
210
+ </li>
211
+ ))}
212
+ </ul>
213
+ )}
214
+ <script src={ASK_BOX_SCRIPT_PATH} defer />
215
+ </div>
216
+ ) : (
217
+ data.ctaHref &&
218
+ data.ctaLabel && (
219
+ <a className="opening__door" href={data.ctaHref}>
220
+ {data.ctaLabel}
221
+ </a>
222
+ )
223
+ )}
224
+ </div>
225
+ <NetworkLayer {...placeNetwork(data)} lendable={data.askBox} />
193
226
  {data.askBox && (
194
- <svg className="net-leads" data-net-leads="" aria-hidden="true" />
227
+ <svg
228
+ className="net-leads"
229
+ {...{ [ASK_LEADS_ATTRIBUTE]: "" }}
230
+ aria-hidden="true"
231
+ />
195
232
  )}
196
233
  </section>
197
234
  );
@@ -15,6 +15,7 @@ import { foundationOrganism } from "../../story/foundation-organism";
15
15
  import { livingOrganism } from "../../story/living-organism";
16
16
  import { workOrganism } from "../../story/work-organism";
17
17
  import { storyRuntimeScript } from "../../story/runtime";
18
+ import { ASK_ROOM_STYLES } from "@brains/site-atlas";
18
19
 
19
20
  export type { RizomRuntimeConfig } from "../contracts";
20
21
 
@@ -61,6 +62,7 @@ export const rizomRuntimeStaticAssets: Record<string, string> = {
61
62
  // The story pages: the page shape, the drawings and the reading thread.
62
63
  "/styles/story.css":
63
64
  storyStyles +
65
+ ASK_ROOM_STYLES +
64
66
  livingOrganism.css() +
65
67
  brainOrganism.css() +
66
68
  workOrganism.css() +
@@ -4,6 +4,7 @@
4
4
  * function is exported for tests and inlined into the shipped script, so
5
5
  * there is one source for the reading line.
6
6
  */
7
+ import { ASK_ROOM_SCRIPT } from "@brains/site-atlas";
7
8
 
8
9
  /** The index of the last chapter whose top has passed the reading line, clamped to the stages a drawing has. */
9
10
  export function currentChapter(
@@ -33,7 +34,9 @@ export function readingLine(
33
34
  return stripBottom === null ? viewportHeight * beside : stripBottom + 1;
34
35
  }
35
36
 
36
- export const storyRuntimeScript: string = `(function () {
37
+ export const storyRuntimeScript: string =
38
+ ASK_ROOM_SCRIPT +
39
+ `(function () {
37
40
  ${currentChapter.toString()}
38
41
  ${readingLine.toString()}
39
42
  function init() {
@@ -109,11 +112,14 @@ export const storyRuntimeScript: string = `(function () {
109
112
  addEventListener("resize", function () { placeRail(); read(); });
110
113
  addEventListener("load", function () { placeRail(); read(); });
111
114
  }
112
- // The homepage's drawing listens to the Ask box: an answer's sources name
113
- // the brains whose published memory they came from (the box's ask:sources
114
- // event), and those brains light while the rest dim. A source without a
115
- // brain is this brain's own and lights the center. Pointing works both
116
- // ways: a listed source lights its brain, a brain flags its listed sources.
115
+ // The homepage's drawings listen to the Ask room: an answer's sources name
116
+ // the brains whose published memory they came from (the room matches them
117
+ // to the opening's dots, keyed by brain, and tells the page), and those
118
+ // brains light in every drawing while the rest dim. A source the room
119
+ // matched to no brain is this brain's own and lights the center. Pointing
120
+ // works both ways: a listed source lights its brain, a brain flags its
121
+ // listed sources. Leads, the lending of the drawing to a phone's open
122
+ // conversation and the tap on a lit dot are the room's.
117
123
  function listen() {
118
124
  // The opening draws the network, and a chapter may draw it again beside
119
125
  // its own words: every drawing answers the same events.
@@ -127,20 +133,6 @@ export const storyRuntimeScript: string = `(function () {
127
133
  var replies = all(".net-reply[data-brain]");
128
134
  var names = all(".net-name[data-brain]");
129
135
  var sourceBrain = {};
130
- function brainOf(source) {
131
- var brain = source && source.brain;
132
- if (!brain) return "";
133
- var host = "";
134
- if (brain.url) { try { host = new URL(brain.url).host; } catch (e) { host = ""; } }
135
- var byHost = host && marks.filter(function (m) { return m.getAttribute("data-brain") === host; })[0];
136
- if (byHost) return host;
137
- var name = String(brain.name || "").toLowerCase();
138
- var byName = marks.filter(function (m) {
139
- var a = m.querySelector("[aria-label]");
140
- return (a ? a.getAttribute("aria-label") : "").toLowerCase() === name;
141
- })[0];
142
- return byName ? byName.getAttribute("data-brain") : "";
143
- }
144
136
  function light(id, on) {
145
137
  [marks, threads, replies, names].forEach(function (list) {
146
138
  list.forEach(function (el) { if (el.getAttribute("data-brain") === id) el.classList.toggle("is-lit", on); });
@@ -149,20 +141,19 @@ export const storyRuntimeScript: string = `(function () {
149
141
  function hot(id, on) {
150
142
  marks.concat(threads).forEach(function (el) { if (el.getAttribute("data-brain") === id) el.classList.toggle("is-hot", on); });
151
143
  }
152
- document.addEventListener("ask:sources", function (event) {
144
+ document.addEventListener("ask:cited", function (event) {
153
145
  var detail = event.detail || {};
154
- var sources = Array.isArray(detail.sources) ? detail.sources : [];
155
146
  var lit = {};
156
- var rizom = false;
157
147
  sourceBrain = {};
158
- sources.forEach(function (source) {
159
- var id = brainOf(source);
160
- if (id) { lit[id] = true; sourceBrain[source.id] = id; } else rizom = true;
148
+ (detail.cited || []).forEach(function (entry) {
149
+ lit[entry.key] = true;
150
+ sourceBrain[entry.source.id] = entry.key;
161
151
  });
162
152
  marks.forEach(function (m) { light(m.getAttribute("data-brain"), !!lit[m.getAttribute("data-brain")]); });
153
+ var rizom = (detail.unmatched || []).length > 0 && (detail.sources || []).length > 0;
163
154
  layers.forEach(function (layer) {
164
155
  layer.classList.toggle("has-replies", Object.keys(lit).length > 0);
165
- layer.classList.toggle("is-rizom", rizom && sources.length > 0);
156
+ layer.classList.toggle("is-rizom", rizom);
166
157
  });
167
158
  });
168
159
  function listed(target) {
@@ -196,104 +187,6 @@ export const storyRuntimeScript: string = `(function () {
196
187
  if (!target || !target.hasAttribute || !target.hasAttribute("data-ask-answer")) return;
197
188
  document.dispatchEvent(new CustomEvent("ask:sources", { detail: { sources: askedSources() } }));
198
189
  }, true);
199
- // The drawing belongs to the page while an answer is open. On desktop a
200
- // dotted lead runs from each listed source to its brain in the opening's
201
- // drawing; on a phone the drawing joins the open conversation as the
202
- // first item of its scroll, and a tap on a lit dot brings its source
203
- // into view. Only the opening's drawing is lent; a chapter's stays put.
204
- var home = layers[0];
205
- var homeMarks = marks.filter(function (m) { return home.contains(m); });
206
- var leadsLayer = document.querySelector("[data-net-leads]");
207
- var narrow = typeof matchMedia === "function" ? matchMedia("(max-width: 60rem)") : { matches: false };
208
- var askHost = document.querySelector("[data-ask-box]");
209
- var SVG = "http://www.w3.org/2000/svg", GAP = 6, GLYPH = 9;
210
- function listedRow(scope, key) {
211
- if (!scope) return null;
212
- var rows = Array.prototype.filter.call(scope.querySelectorAll("[data-ask-source]"), function (row) {
213
- return row.getAttribute("data-ask-source") === key;
214
- });
215
- return rows.length ? rows[rows.length - 1] : null;
216
- }
217
- function drawLeads() {
218
- if (!leadsLayer) return;
219
- while (leadsLayer.firstChild) leadsLayer.removeChild(leadsLayer.firstChild);
220
- if (narrow.matches || !askHost) return;
221
- // The layer's own box is the frame: coordinates are its, whatever positions it.
222
- var frame = leadsLayer.getBoundingClientRect();
223
- leadsLayer.setAttribute("viewBox", "0 0 " + frame.width + " " + frame.height);
224
- Object.keys(sourceBrain).forEach(function (key) {
225
- var mark = homeMarks.filter(function (m) { return m.getAttribute("data-brain") === sourceBrain[key]; })[0];
226
- var row = listedRow(askHost, key);
227
- if (!mark || !row) return;
228
- var from = row.getBoundingClientRect();
229
- if (!from.height) return;
230
- var to = mark.getBoundingClientRect();
231
- var x1 = from.right + GAP - frame.left;
232
- var y1 = from.top + from.height / 2 - frame.top;
233
- var toward = to.left + to.width / 2 - frame.left;
234
- var x2 = toward > x1 ? toward - GLYPH : toward + GLYPH;
235
- var y2 = to.top + to.height / 2 - frame.top;
236
- var bend = Math.max(40, Math.abs(x2 - x1) / 2);
237
- var lead = document.createElementNS(SVG, "path");
238
- lead.setAttribute("data-lead", key);
239
- lead.setAttribute("d", "M" + x1 + " " + y1 + " C" + (x1 + bend) + " " + y1 + " " + (x2 - bend) + " " + y2 + " " + x2 + " " + y2);
240
- leadsLayer.appendChild(lead);
241
- });
242
- }
243
- var leadsPending = false;
244
- function scheduleLeads() {
245
- if (leadsPending || !leadsLayer) return;
246
- leadsPending = true;
247
- requestAnimationFrame(function () { leadsPending = false; drawLeads(); });
248
- }
249
- if (leadsLayer) {
250
- addEventListener("resize", scheduleLeads);
251
- // Scrolling the page or the conversation moves the listed sources.
252
- document.addEventListener("scroll", scheduleLeads, true);
253
- document.addEventListener("toggle", scheduleLeads, true);
254
- if (askHost && typeof ResizeObserver === "function") new ResizeObserver(scheduleLeads).observe(askHost);
255
- }
256
- document.addEventListener("ask:sources", function () { drawLeads(); });
257
- function sheetOpen() { return !!askHost && askHost.hasAttribute("data-ask-sheet"); }
258
- var slot = null;
259
- function lend() {
260
- var dock = askHost.querySelector("[data-ask-dock]");
261
- if (!home || !dock || home.parentElement === dock) return;
262
- slot = document.createElement("div");
263
- slot.className = "net-slot";
264
- slot.setAttribute("aria-hidden", "true");
265
- home.parentNode.insertBefore(slot, home);
266
- dock.appendChild(home);
267
- }
268
- function giveBack() {
269
- if (!slot) return;
270
- slot.parentNode.insertBefore(home, slot);
271
- slot.parentNode.removeChild(slot);
272
- slot = null;
273
- }
274
- if (askHost && home && typeof MutationObserver === "function") {
275
- // The box mounts, and so its dock appears, after the sheet has opened.
276
- new MutationObserver(function () { if (sheetOpen()) lend(); else giveBack(); })
277
- .observe(askHost, { attributes: true, attributeFilter: ["data-ask-sheet"], childList: true, subtree: true });
278
- }
279
- document.addEventListener("click", function (event) {
280
- if (!sheetOpen() || !home) return;
281
- var mark = event.target && event.target.closest ? event.target.closest(".net-mark[data-brain]") : null;
282
- if (!mark || !home.contains(mark)) return;
283
- var brain = mark.getAttribute("data-brain");
284
- var key = Object.keys(sourceBrain).filter(function (k) { return sourceBrain[k] === brain; })[0];
285
- var area = askHost.querySelector(".brain-box-scroll");
286
- var row = key ? listedRow(area, key) : null;
287
- if (!row || !area) return;
288
- event.preventDefault();
289
- var at = row.getBoundingClientRect(), box = area.getBoundingClientRect();
290
- var top = area.scrollTop + at.top - box.top - area.clientHeight / 2 + at.height / 2;
291
- if (area.scrollTo) area.scrollTo({ top: top, behavior: "smooth" }); else area.scrollTop = top;
292
- row.removeAttribute("data-ask-flash");
293
- void row.offsetWidth;
294
- row.setAttribute("data-ask-flash", "");
295
- setTimeout(function () { row.removeAttribute("data-ask-flash"); }, 1500);
296
- }, true);
297
190
  document.addEventListener("mouseover", function (e) { point(e, true); });
298
191
  document.addEventListener("mouseout", function (e) { point(e, false); });
299
192
  document.addEventListener("focusin", function (e) { point(e, true); });
package/src/story.css CHANGED
@@ -784,6 +784,45 @@
784
784
  width: min(100%, calc(100svh - var(--bar-height, 4.6rem) - 2rem));
785
785
  aspect-ratio: 1;
786
786
  }
787
+ /* On a wide screen the opening's drawing keeps its place beside the words
788
+ while the opening is read, an answer included, and leaves with the opening;
789
+ it fades once the reading has moved the figure on, as on a phone. In flow
790
+ with no height, so the words do not move; its width is the room right of
791
+ the words, as the absolute layer's was. */
792
+ @media not all and (max-width: 60rem) {
793
+ /* The words are the room's column (the kit scrolls it within the screen);
794
+ the bar and the opening's 4rem of padding above and below stay clear, so
795
+ the composer is on the first screen. */
796
+ .chapter--opening .opening__words {
797
+ --ask-column-clear: calc(8rem + var(--bar-height, 4.6rem));
798
+ }
799
+ :where(.chapter--opening) > .net-layer {
800
+ position: sticky;
801
+ order: -1;
802
+ top: calc(
803
+ var(--bar-height, 4.6rem) + (100svh - var(--bar-height, 4.6rem)) / 2
804
+ );
805
+ height: 0;
806
+ margin-left: calc(var(--words) + var(--words-gap));
807
+ width: calc(
808
+ 100cqi - max(0px, (100cqi - 80rem) / 2) - clamp(1rem, 5vw, 5rem) -
809
+ var(--words) - var(--words-gap) - clamp(1rem, 6vw, 7rem)
810
+ );
811
+ transition: opacity 0.6s ease;
812
+ }
813
+ :where(.chapter--opening) > .net-layer > * {
814
+ translate: 0 -50%;
815
+ }
816
+ .story:has(.figure:not([data-stage="0"]))
817
+ :where(.chapter--opening)
818
+ > .net-layer,
819
+ .story:has(.figure:not([data-stage="0"])) .net-leads {
820
+ opacity: 0;
821
+ }
822
+ .net-leads {
823
+ transition: opacity 0.6s ease;
824
+ }
825
+ }
787
826
  .net-svg {
788
827
  overflow: visible;
789
828
  }
@@ -1416,46 +1455,21 @@
1416
1455
  opacity: 0;
1417
1456
  transition: opacity 0.6s ease;
1418
1457
  }
1419
- /* The drawing belongs to the page while an answer is open. On desktop a
1420
- dotted lead runs from each listed source to its brain (drawn by the
1421
- runtime); on a phone the opening's drawing joins the open conversation as
1422
- the first item of its scroll, a slot holding its place meanwhile, and a
1423
- tapped dot flashes its source. */
1458
+ /* The Ask room's lead layer (the room script draws into it; the kit styles
1459
+ the leads): leads join words to the drawing across the page, so the
1460
+ viewport is their frame. On a phone the opening's drawing joins the open
1461
+ conversation as the first item of its scroll, a slot holding its place. */
1424
1462
  .net-leads {
1425
- position: absolute;
1463
+ position: fixed;
1426
1464
  inset: 0;
1427
1465
  z-index: 2;
1428
- width: 100%;
1429
- height: 100%;
1430
- /* The chapter is the words' width; a lead runs past it to the drawing. */
1466
+ width: 100vw;
1467
+ height: 100vh;
1431
1468
  overflow: visible;
1432
1469
  pointer-events: none;
1433
1470
  }
1434
- .net-leads path {
1435
- fill: none;
1436
- stroke: var(--color-accent);
1437
- stroke-width: 1.4;
1438
- stroke-linecap: round;
1439
- stroke-dasharray: 0.1 6;
1440
- opacity: 0.85;
1441
- }
1442
- .opening__ask [data-ask-source][data-ask-flash] {
1443
- animation: ask-flash 1.5s ease-out;
1444
- }
1445
- @keyframes ask-flash {
1446
- 0%,
1447
- 40% {
1448
- background: rgb(from var(--color-accent) r g b / 0.14);
1449
- }
1450
- 100% {
1451
- background: transparent;
1452
- }
1453
- }
1454
1471
  @media (max-width: 60rem) {
1455
- .net-leads {
1456
- display: none;
1457
- }
1458
- .net-slot {
1472
+ [data-ask-slot] {
1459
1473
  height: var(--strip-h);
1460
1474
  }
1461
1475
  .opening__ask[data-ask-sheet] [data-ask-dock]:has(.net-layer) {
@@ -1473,14 +1487,6 @@
1473
1487
  border-bottom: 1px solid var(--color-border);
1474
1488
  }
1475
1489
  }
1476
- @media (prefers-reduced-motion: reduce) {
1477
- .net-leads {
1478
- display: none;
1479
- }
1480
- .opening__ask [data-ask-source][data-ask-flash] {
1481
- animation: none;
1482
- }
1483
- }
1484
1490
  /* The chapter's drawing is the network at rest: no pulse travels it and no
1485
1491
  brain blinks on its own; only the open question's brains answer. */
1486
1492
  .asked .net-pulse,