@justai/cuts 0.118.0 → 0.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.118.0",
3
+ "version": "0.120.0",
4
4
  "description": "A persona's named parts \u2014 the page shell that holds them, and the cuts themselves.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -41,8 +41,8 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@justai/core": "^0.26.0",
45
- "@justai/ui": "^0.16.0",
44
+ "@justai/core": "^0.27.0",
45
+ "@justai/ui": "^0.18.0",
46
46
  "@justai/atrium": "^0.1.9"
47
47
  }
48
48
  }
package/src/Base.astro CHANGED
@@ -88,12 +88,31 @@ interface Props {
88
88
 
89
89
  const {
90
90
  lang, t, site, locales, defaultLocale, favicon,
91
- themeDark = "#000000",
92
- themeLight = "#f2f2f7",
91
+ themeDark,
92
+ themeLight,
93
93
  gaId = "G-13L3BP6XCW",
94
94
  kind = "app",
95
95
  } = Astro.props;
96
96
 
97
+ // queries: theme-color · a grey band under the game on iPhone · Safari's bottom bar colour ·
98
+ // the browser chrome does not match the game · game persona theme colour
99
+ //
100
+ // ── THE BROWSER'S OWN BAR IS PAINTED BY THIS PAGE, AND A GAME PERSONA MUST SAY SO ─────────────
101
+ //
102
+ // iOS Safari colours its toolbars from `theme-color`. On a phone in light mode that default is
103
+ // #f2f2f7 — so a game persona, whose kernel is a dark room filling the whole screen, ended in a pale
104
+ // grey shelf where the browser's bar is. The founder photographed it four times and we spent an hour
105
+ // on the layout, because it looks exactly like the page leaking: his own device eventually proved
106
+ // the container was exact (toolbar 78 + kernel 632.3 = innerHeight 710) and the band was Safari,
107
+ // wearing the colour we handed it.
108
+ //
109
+ // So a GAME persona's chrome is the game's ground, in both schemes: the room has no seam at the
110
+ // bottom of the screen because there is nothing there that is not the room. A persona that wants
111
+ // otherwise passes its own — this is a default, not a law.
112
+ const GAME_GROUND = "#080b12";
113
+ const dark = themeDark ?? (kind === "game" ? GAME_GROUND : "#000000");
114
+ const light = themeLight ?? (kind === "game" ? GAME_GROUND : "#f2f2f7");
115
+
97
116
  const d = dirOf(lang);
98
117
  const consent = consentCopy(lang);
99
118
  const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
@@ -183,8 +202,8 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
183
202
  <meta property="og:site_name" content={new URL(site).host} />
184
203
  <meta name="twitter:card" content="summary_large_image" />
185
204
  <meta name="twitter:image" content={new URL("/og.jpg", Astro.site)} />
186
- <meta name="theme-color" content={themeDark} media="(prefers-color-scheme: dark)" />
187
- <meta name="theme-color" content={themeLight} media="(prefers-color-scheme: light)" />
205
+ <meta name="theme-color" content={dark} media="(prefers-color-scheme: dark)" />
206
+ <meta name="theme-color" content={light} media="(prefers-color-scheme: light)" />
188
207
  <link rel="canonical" href={`${site}${localePath(lang)}`} />
189
208
  {locales.map((l) => (
190
209
  <link rel="alternate" hreflang={l} href={`${site}${localePath(l)}`} />
package/src/Posts.astro CHANGED
@@ -118,7 +118,7 @@ const vars = [
118
118
  connects every comment & reply (they're all replies to the post). ---- */
119
119
  /* the cut owns its own landing offset (was `#kernel, #posts { scroll-margin-top }` in each persona —
120
120
  which orphaned once #posts moved here and carried this component's scope cid instead of the persona's). */
121
- .posts { border-block-start: 1px solid var(--border); scroll-margin-top: 82px; }
121
+ .posts { border-block-start: 1px solid var(--border); scroll-margin-top: 82px; scroll-margin-top: var(--shell-top, 82px); }
122
122
  /* A post is hoverable because it is a link to its own page, and the hover used to say so with a grey wash
123
123
  — `--text` at 3.5% — which reads as dimming rather than as lifting. It now uses the card material, so
124
124
  hovering a post looks like the same gesture as hovering a catalogue card.
package/src/Toolbar.astro CHANGED
@@ -166,116 +166,136 @@ import AuthMount from "./AuthMount.astro";
166
166
  <!-- The two titles reach the script as data attributes rather than through `define:vars`, which would
167
167
  make this script inline and it has an import. Astro drops an attribute whose value is undefined,
168
168
  so a persona that passes neither leaves no trace in the markup at all. -->
169
- <header class="toolbar" data-kernel-title={kernelTitle} data-posts-title={postsTitle}>
170
- <div class="tb-row">
171
- <div class="tb-left">
172
- {home && <BackToZone apex={apex} />}
173
- <nav class="tb-nav-group" aria-label={sectionsLabel}>
174
- {/* POSITION ONE OF A CHILD'S ROW: the persona this one is mounted inside. `/icon.svg` is the
175
- parent's face here, because a child runs in the parent's origin — the same path, read from
176
- a different address. `#kernel` rather than `/` so the way back lands on the parent's work
177
- and not on its biography, which is where a person who came from there was standing. */}
178
- {parent && (
179
- <a class="tb-nav tb-mark" id="navParent" href={parent.href ?? "/#kernel"} aria-label={parent.label ?? "Home"} title={parent.label ?? "Home"}>
180
- <img src={parent.icon ?? "/icon.svg"} alt="" width="20" height="20" loading="lazy" decoding="async" />
169
+ <div class="tb-slot">
170
+ <header class="toolbar" data-kernel-title={kernelTitle} data-posts-title={postsTitle}>
171
+ <div class="tb-row">
172
+ <div class="tb-left">
173
+ {home && <BackToZone apex={apex} />}
174
+ <nav class="tb-nav-group" aria-label={sectionsLabel}>
175
+ {/* POSITION ONE OF A CHILD'S ROW: the persona this one is mounted inside. `/icon.svg` is the
176
+ parent's face here, because a child runs in the parent's origin — the same path, read from
177
+ a different address. `#kernel` rather than `/` so the way back lands on the parent's work
178
+ and not on its biography, which is where a person who came from there was standing. */}
179
+ {parent && (
180
+ <a class="tb-nav tb-mark" id="navParent" href={parent.href ?? "/#kernel"} aria-label={parent.label ?? "Home"} title={parent.label ?? "Home"}>
181
+ <img src={parent.icon ?? "/icon.svg"} alt="" width="20" height="20" loading="lazy" decoding="async" />
182
+ </a>
183
+ )}
184
+ {/* THE PROFILE ICON IS THE PERSONA'S OWN FACE. Always — the founder's rule, and app.zone and
185
+ qr had already settled it while thirteen siblings still showed a generic bust.
186
+
187
+ The bust is gone rather than demoted. A fallback that renders something plausible is how a
188
+ persona ships wearing nobody's face for months: nothing is broken, nothing is red, and the
189
+ only reader who notices is somebody comparing two personas side by side. `/icon.svg` is
190
+ not a guess — it is the ecosystem's one-face rule, the same path `persona.yml` declares
191
+ and the registry stores.
192
+
193
+ `tb-mark` is set whichever way the face arrives, because it is what the sizing rule looks
194
+ for: a mark runs to the edge of its viewBox and a glyph does not, so they need different
195
+ boxes to put the same ink on screen. */}
196
+ {/* `tb-mark` says "a solid MARK is in this box" — it is what the sizing rule looks for, and a
197
+ mark runs to the edge of its viewBox where a stroke glyph sits inside 75% of one. A
198
+ persona's face is always a mark, so a persona always carries the class.
199
+
200
+ A CHILD IS THE OTHER CASE, and it was measured rather than assumed: files passes a stroke
201
+ folder, and with `tb-mark` on it, it rendered at 19px beside a 25.33px bolt — one size
202
+ smaller, which is exactly the defect the 25.33 rule exists to remove. So in child mode the
203
+ slotted icon takes the GLYPH box and the row reads level. A child that draws a solid mark
204
+ instead should say so in its own CSS; every child in the fleet draws a glyph today. */}
205
+ <a class:list={["tb-nav", { "tb-mark": !parent }]} id="navProfile" href="#profile" aria-label={profileLabel} title={profileLabel}>
206
+ {/* A CHILD MUST PASS ITS OWN MARK. The default `/icon.svg` is the face of whoever owns this
207
+ origin — right for a persona, and its PARENT for a child, which would put the same
208
+ picture in two places and say nothing in either. So when `parent` is given there is no
209
+ fallback: a child that passes nothing renders an empty box, which is a defect somebody
210
+ sees, rather than a duplicate face, which is a defect nobody reports. */}
211
+ {Astro.slots.has("profileIcon")
212
+ ? <slot name="profileIcon" />
213
+ : (!parent && <img src={avatar} alt="" width="20" height="20" loading="lazy" decoding="async" />)}
181
214
  </a>
182
- )}
183
- {/* THE PROFILE ICON IS THE PERSONA'S OWN FACE. Always — the founder's rule, and app.zone and
184
- qr had already settled it while thirteen siblings still showed a generic bust.
185
-
186
- The bust is gone rather than demoted. A fallback that renders something plausible is how a
187
- persona ships wearing nobody's face for months: nothing is broken, nothing is red, and the
188
- only reader who notices is somebody comparing two personas side by side. `/icon.svg` is
189
- not a guess — it is the ecosystem's one-face rule, the same path `persona.yml` declares
190
- and the registry stores.
191
-
192
- `tb-mark` is set whichever way the face arrives, because it is what the sizing rule looks
193
- for: a mark runs to the edge of its viewBox and a glyph does not, so they need different
194
- boxes to put the same ink on screen. */}
195
- {/* `tb-mark` says "a solid MARK is in this box" — it is what the sizing rule looks for, and a
196
- mark runs to the edge of its viewBox where a stroke glyph sits inside 75% of one. A
197
- persona's face is always a mark, so a persona always carries the class.
198
-
199
- A CHILD IS THE OTHER CASE, and it was measured rather than assumed: files passes a stroke
200
- folder, and with `tb-mark` on it, it rendered at 19px beside a 25.33px bolt — one size
201
- smaller, which is exactly the defect the 25.33 rule exists to remove. So in child mode the
202
- slotted icon takes the GLYPH box and the row reads level. A child that draws a solid mark
203
- instead should say so in its own CSS; every child in the fleet draws a glyph today. */}
204
- <a class:list={["tb-nav", { "tb-mark": !parent }]} id="navProfile" href="#profile" aria-label={profileLabel} title={profileLabel}>
205
- {/* A CHILD MUST PASS ITS OWN MARK. The default `/icon.svg` is the face of whoever owns this
206
- origin — right for a persona, and its PARENT for a child, which would put the same
207
- picture in two places and say nothing in either. So when `parent` is given there is no
208
- fallback: a child that passes nothing renders an empty box, which is a defect somebody
209
- sees, rather than a duplicate face, which is a defect nobody reports. */}
210
- {Astro.slots.has("profileIcon")
211
- ? <slot name="profileIcon" />
212
- : (!parent && <img src={avatar} alt="" width="20" height="20" loading="lazy" decoding="async" />)}
213
- </a>
214
- {/* THE KERNEL IS ALWAYS THE BOLT. The founder's rule, and it is the shell's to draw rather
215
- than the persona's to remember.
216
-
217
- It was a bare `<slot name="kernelIcon" />` with NO fallback, which is the worst shape a
218
- slot can have: a persona that passed nothing shipped a live, invisible tap target where
219
- every sibling showed a mark — no error, no warning, and visible only beside another
220
- persona. Fourteen of fifteen passed their own drawing instead, so the row said something
221
- different in every repo about the same thing.
222
-
223
- The bolt says WHAT THE SECTION IS — the persona's one job, its live surface — and the
224
- profile icon two places to the left already says WHO. An icon that repeats the identity
225
- spends the row's only distinction on a thing the row has already said. */}
226
- <a class="tb-nav" id="navKernel" href="#kernel" aria-label={kernelLabel} title={kernelLabel}>
227
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.9 3L5.7 13.35h5.4L10.2 21l8.1-10.8h-5.4l.9-7.2z"/></svg>
228
- </a>
229
- {posts && (
230
- <a class="tb-nav" id="navPosts" href="#posts" aria-label={postsLabel} title={postsLabel}>
231
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.3 9.6 9.6 0 0 1-4-.9L3 21l1.1-5A8.4 8.4 0 1 1 21 11.5z"/></svg>
215
+ {/* THE KERNEL IS ALWAYS THE BOLT. The founder's rule, and it is the shell's to draw rather
216
+ than the persona's to remember.
217
+
218
+ It was a bare `<slot name="kernelIcon" />` with NO fallback, which is the worst shape a
219
+ slot can have: a persona that passed nothing shipped a live, invisible tap target where
220
+ every sibling showed a mark — no error, no warning, and visible only beside another
221
+ persona. Fourteen of fifteen passed their own drawing instead, so the row said something
222
+ different in every repo about the same thing.
223
+
224
+ The bolt says WHAT THE SECTION IS — the persona's one job, its live surface — and the
225
+ profile icon two places to the left already says WHO. An icon that repeats the identity
226
+ spends the row's only distinction on a thing the row has already said. */}
227
+ <a class="tb-nav" id="navKernel" href="#kernel" aria-label={kernelLabel} title={kernelLabel}>
228
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.9 3L5.7 13.35h5.4L10.2 21l8.1-10.8h-5.4l.9-7.2z"/></svg>
229
+ </a>
230
+ {posts && (
231
+ <a class="tb-nav" id="navPosts" href="#posts" aria-label={postsLabel} title={postsLabel}>
232
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.3 9.6 9.6 0 0 1-4-.9L3 21l1.1-5A8.4 8.4 0 1 1 21 11.5z"/></svg>
233
+ </a>
234
+ )}
235
+ <slot name="navExtra" />
236
+ </nav>
237
+ {files && (
238
+ // queries: files icon in the toolbar · how do I reach my files from a persona · the folder
239
+ // in the toolbar · why is files not in the section nav
240
+ //
241
+ // THE PERSONA'S FILES, ONE TAP FROM ANYWHERE IN IT.
242
+ //
243
+ // Until this, the only way into `<persona>/files` was to be asked for a file — the room
244
+ // where your things live could be entered only by a persona's invitation, which is exactly
245
+ // backwards for a room that is yours.
246
+ //
247
+ // OUTSIDE the sections nav, and deliberately: profile, kernel and posts are parts of THIS
248
+ // page and their links are fragments. `/files` is a different document — a place, not a
249
+ // section — and putting a navigation to elsewhere inside `<nav aria-label="sections">`
250
+ // would tell a screen reader it is one of them. Same reasoning the `leftExtra` slot below
251
+ // already carries, which is why this sits beside it rather than in the group above.
252
+ //
253
+ // ON BY DEFAULT, because the edge mounts `/files` on EVERY persona host: a persona that had
254
+ // to opt in would be a persona whose files exist and cannot be found. One bump here reaches
255
+ // the whole fleet, which is the only shape that survives a thousand of them.
256
+ <a class="tb-nav tb-files" id="navFiles" href="/files" aria-label={filesLabel} title={filesLabel}>
257
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5A1.5 1.5 0 0 1 4.5 6h4L11 8.5h8.5A1.5 1.5 0 0 1 21 10v7a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 17z"/></svg>
232
258
  </a>
233
259
  )}
234
- <slot name="navExtra" />
235
- </nav>
236
- {files && (
237
- // queries: files icon in the toolbar · how do I reach my files from a persona · the folder
238
- // in the toolbar · why is files not in the section nav
239
- //
240
- // THE PERSONA'S FILES, ONE TAP FROM ANYWHERE IN IT.
241
- //
242
- // Until this, the only way into `<persona>/files` was to be asked for a file — the room
243
- // where your things live could be entered only by a persona's invitation, which is exactly
244
- // backwards for a room that is yours.
245
- //
246
- // OUTSIDE the sections nav, and deliberately: profile, kernel and posts are parts of THIS
247
- // page and their links are fragments. `/files` is a different document — a place, not a
248
- // section — and putting a navigation to elsewhere inside `<nav aria-label="sections">`
249
- // would tell a screen reader it is one of them. Same reasoning the `leftExtra` slot below
250
- // already carries, which is why this sits beside it rather than in the group above.
251
- //
252
- // ON BY DEFAULT, because the edge mounts `/files` on EVERY persona host: a persona that had
253
- // to opt in would be a persona whose files exist and cannot be found. One bump here reaches
254
- // the whole fleet, which is the only shape that survives a thousand of them.
255
- <a class="tb-nav tb-files" id="navFiles" href="/files" aria-label={filesLabel} title={filesLabel}>
256
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5A1.5 1.5 0 0 1 4.5 6h4L11 8.5h8.5A1.5 1.5 0 0 1 21 10v7a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 17z"/></svg>
257
- </a>
258
- )}
259
- <!-- beside the section nav, but deliberately OUTSIDE it: a link that is not a section of this
260
- page (justai.pro carries app.zone's mark here). Keeping it out of <nav aria-label=sections>
261
- is the a11y half of the reason it is its own slot. -->
262
- <slot name="leftExtra" />
263
- </div>
264
- <div class="tb-right">
265
- <slot name="toolbarRight" />
266
- <slot name="langPicker" />
267
- <AuthMount />
260
+ <!-- beside the section nav, but deliberately OUTSIDE it: a link that is not a section of this
261
+ page (justai.pro carries app.zone's mark here). Keeping it out of <nav aria-label=sections>
262
+ is the a11y half of the reason it is its own slot. -->
263
+ <slot name="leftExtra" />
264
+ </div>
265
+ <div class="tb-right">
266
+ <slot name="toolbarRight" />
267
+ <slot name="langPicker" />
268
+ <AuthMount />
269
+ </div>
268
270
  </div>
269
- </div>
270
- <div class="tb-title" id="tbTitle"><span class="tb-title-text" id="tbTitleText"></span></div>
271
- </header>
271
+ <div class="tb-title" id="tbTitle"><span class="tb-title-text" id="tbTitleText"></span></div>
272
+ </header>
273
+ </div>
272
274
 
273
275
  <style>
274
276
  /* ---- app toolbar — sticky to the top of the active region (kernel + posts); it never
275
277
  enters the Profile (it lives inside .below). Flat / knife-cut: a left group and a
276
278
  right group, no rounded pills. ---- */
279
+ /* queries: tb-slot · why is the toolbar wrapped · the bar changes height · the kernel is 26px
280
+ * short · a band of the feed at the bottom · the toolbar reserves a box · sticky flow
281
+ *
282
+ * ── THE SLOT IS WHAT FLOW SEES; THE BAR IS WHAT THE EYE SEES ──────────────────────────────
283
+ *
284
+ * `position: sticky` NEVER LEAVES FLOW. It reserves its full border box and paints inside it — so
285
+ * the bar's LIVE height positioned everything under it (52 with the title strip shut, 78 open)
286
+ * while `--shell-top` was deliberately frozen at the open height so that nothing would sample the
287
+ * 0.35s animation. The HEIGHT stopped sampling it. THE POSITION NEVER DID. Two numbers that must
288
+ * be equal, written a hundred lines apart in two packages, and their difference is a band of the
289
+ * next section above the fold: measured on the live persona with the scroll untouched, the kernel
290
+ * sat 52px below `#below` instead of 78 and 25.81px of the feed was on screen.
291
+ *
292
+ * The wrapper separates them. The SLOT is sticky and its box is `--shell-top`; the BAR is static
293
+ * inside it and may be any height it likes. Nothing above the kernel changes size any more, so
294
+ * there is nothing for an engine to compensate for — which is also why only a device ever showed
295
+ * it: Blink scroll-anchors the step away and WebKit has no scroll anchoring. */
296
+ .tb-slot { position: sticky; top: 0; z-index: 40; display: flow-root; }
277
297
  .toolbar {
278
- position: sticky; top: 0; z-index: 40;
298
+ /* position, top and z-index live on .tb-slot above — this element is static inside it now. */
279
299
  display: flex; flex-direction: column;
280
300
  /* full-bleed: the bar (frosted bg + hairline) touches the contained-view edges; the icons stay
281
301
  aligned with the body via matching inline padding. --edge is the persona's page inline padding