@justai/cuts 0.76.0 → 0.78.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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/Toolbar.astro +129 -35
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.76.0",
4
- "description": "A persona's named parts \u2014 the page shell that holds them, and the cuts themselves.",
3
+ "version": "0.78.0",
4
+ "description": "A persona's named parts — the page shell that holds them, and the cuts themselves.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@justai/core": "^0.10.0",
40
- "@justai/ui": "^0.4.0",
40
+ "@justai/ui": "^0",
41
41
  "@justai/atrium": "^0.1.9"
42
42
  }
43
43
  }
package/src/Toolbar.astro CHANGED
@@ -8,7 +8,6 @@
8
8
  //
9
9
  // <Toolbar sectionsLabel={t.chromeSections} profileLabel={t.chromeProfile}
10
10
  // kernelLabel={t.chromeApp} postsLabel={t.chromePosts}>
11
- // <svg slot="kernelIcon" .../>
12
11
  // <LangPicker slot="langPicker" current={lang} locales={locales} ... />
13
12
  // </Toolbar>
14
13
  //
@@ -16,11 +15,23 @@
16
15
  // data-coupled ones are slots):
17
16
  // • BackToZone — takes no props → internalised here (the persona no longer imports it).
18
17
  // • the three nav aria-labels — resolved strings (like every other cut: Posts, LangPicker) → props.
19
- // • kernelIcon — the ONE per-persona nav glyph (⚡ / clock / globe …) → required slot.
20
- // • profileIcon — OPTIONAL. The persona's own mark, in place of the generic bust. The Profile cut is
21
- // WHO, so its icon may say *whose* profile this is rather than that a profile exists; the face is the
22
- // persona's, so it comes from the persona. Omit it and the bust stays — adopting this is each
23
- // persona's own decision, taken when its mark exists, and nothing moves in the fleet without it.
18
+ // • THE FOUR POSITIONS SAY FOUR DIFFERENT KINDS OF THING, and that is the whole design of the row:
19
+ // home is where you came FROM, the mark is WHO, the bolt is WHAT IT DOES, the bubble is what it
20
+ // SAYS. Neither of the middle two is a per-persona drawing any more, and the reason is not
21
+ // tidiness — an icon that repeats the identity spends the row's only distinction on something the
22
+ // row has already said two places to its left, and then neither icon means anything.
23
+ // • kernelIcon — GONE. It was a per-persona glyph (⚡ / clock / globe …) in a slot with NO fallback,
24
+ // which is the worst shape a slot can have: omit it and the persona shipped a live, invisible tap
25
+ // target. It is always the bolt now, drawn here.
26
+ // The kernel is the only section whose CONTENT differs between personas, which is exactly why its
27
+ // LABEL must not — otherwise there is nothing stable to navigate by, and somebody moving from qr
28
+ // to lendcam has to re-learn the row. A bolt is a state ("something runs here"), not a picture of
29
+ // a subject; a clock is true for one persona and a lie in the next.
30
+ // • profileIcon — OPTIONAL, and the DEFAULT is now the persona's own face rather than a generic
31
+ // bust: `avatar` (default `/icon.svg`) is the ecosystem's one-face path, the same one persona.yml
32
+ // declares and the registry stores. The bust is gone rather than demoted — a fallback that renders
33
+ // something plausible is how thirteen of fifteen personas wore nobody's face for months with
34
+ // nothing red anywhere. Pass the slot when an inline SVG is wanted instead of an <img>.
24
35
  // Note what a mark cannot do: an <img> ignores `currentColor`, so `.active`'s accent never reaches
25
36
  // it. It is not replaced with anything: the title strip already closes on this cut and opens off
26
37
  // it, and a mark is an identity rather than a UI state. Slot presence adds `.tb-mark`, which the
@@ -67,9 +78,24 @@ interface Props {
67
78
  // thing — names or sections, not one of each — so the API says so out loud.
68
79
  kernelTitle?: string;
69
80
  postsTitle?: string;
81
+
82
+ // queries: toolbar shows a generic person icon · the profile icon is not my persona ·
83
+ // how do I put my own mark in the toolbar · avatar
84
+ //
85
+ // THE PERSONA'S OWN FACE, BY DEFAULT RATHER THAN BY DECISION.
86
+ //
87
+ // This used to be a slot with a generic bust behind it, so a persona showed its own mark only if
88
+ // somebody remembered to pass one — and thirteen of fifteen never did. But the shell already knows
89
+ // where a persona's face is: ONE icon, everywhere, at `/icon.svg`. `persona.yml` declares it, the
90
+ // registry stores it, the catalog draws it. There was nothing to decide; there was only something
91
+ // to look up.
92
+ //
93
+ // The `profileIcon` slot still wins when a persona passes one — an inline SVG can take
94
+ // `currentColor` where an <img> cannot, which is why qr passes its mark rather than pointing at it.
95
+ avatar?: string;
70
96
  }
71
97
  import BackToZone from "./BackToZone.astro";
72
- const { sectionsLabel, profileLabel, kernelLabel, postsLabel, home = true, files = true, filesLabel = "Files", kernelTitle, postsTitle } = Astro.props;
98
+ const { sectionsLabel, profileLabel, kernelLabel, postsLabel, home = true, files = true, filesLabel = "Files", kernelTitle, postsTitle, avatar = "/icon.svg" } = Astro.props;
73
99
  import AuthMount from "./AuthMount.astro";
74
100
  ---
75
101
 
@@ -81,31 +107,35 @@ import AuthMount from "./AuthMount.astro";
81
107
  <div class="tb-left">
82
108
  {home && <BackToZone />}
83
109
  <nav class="tb-nav-group" aria-label={sectionsLabel}>
84
- {/* The Profile cut is WHO, so a persona may put its OWN FACE here instead of the generic bust —
85
- the icon then says whose profile this is rather than that a profile exists. Passed as a slot
86
- for the same reason `kernelIcon` is: the face belongs to the persona, not to the shell.
110
+ {/* THE PROFILE ICON IS THE PERSONA'S OWN FACE. Always — the founder's rule, and app.zone and
111
+ qr had already settled it while thirteen siblings still showed a generic bust.
87
112
 
88
- The fallback matters as much as the slot. A persona that passes nothing keeps the bust it has
89
- today, so adopting this is a decision each persona makes when its own mark is ready — nothing
90
- in the fleet moves on the day this ships.
113
+ The bust is gone rather than demoted. A fallback that renders something plausible is how a
114
+ persona ships wearing nobody's face for months: nothing is broken, nothing is red, and the
115
+ only reader who notices is somebody comparing two personas side by side. `/icon.svg` is
116
+ not a guess — it is the ecosystem's one-face rule, the same path `persona.yml` declares
117
+ and the registry stores.
91
118
 
92
- `tb-mark` is set from slot PRESENCE, not from a prop somebody could forget to keep in step. It
93
- exists so the sizing rule can find the image; it deliberately does NOT restyle the mark. */}
94
- <a
95
- class:list={["tb-nav", { "tb-mark": Astro.slots.has("profileIcon") }]}
96
- id="navProfile"
97
- href="#profile"
98
- aria-label={profileLabel}
99
- title={profileLabel}
100
- >
101
- {Astro.slots.has("profileIcon") ? (
102
- <slot name="profileIcon" />
103
- ) : (
104
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
105
- )}
119
+ `tb-mark` is set whichever way the face arrives, because it is what the sizing rule looks
120
+ for: a mark runs to the edge of its viewBox and a glyph does not, so they need different
121
+ boxes to put the same ink on screen. */}
122
+ <a class="tb-nav tb-mark" id="navProfile" href="#profile" aria-label={profileLabel} title={profileLabel}>
123
+ {Astro.slots.has("profileIcon") ? <slot name="profileIcon" /> : <img src={avatar} alt="" width="19" height="19" loading="lazy" decoding="async" />}
106
124
  </a>
125
+ {/* THE KERNEL IS ALWAYS THE BOLT. The founder's rule, and it is the shell's to draw rather
126
+ than the persona's to remember.
127
+
128
+ It was a bare `<slot name="kernelIcon" />` with NO fallback, which is the worst shape a
129
+ slot can have: a persona that passed nothing shipped a live, invisible tap target where
130
+ every sibling showed a mark — no error, no warning, and visible only beside another
131
+ persona. Fourteen of fifteen passed their own drawing instead, so the row said something
132
+ different in every repo about the same thing.
133
+
134
+ The bolt says WHAT THE SECTION IS — the persona's one job, its live surface — and the
135
+ profile icon two places to the left already says WHO. An icon that repeats the identity
136
+ spends the row's only distinction on a thing the row has already said. */}
107
137
  <a class="tb-nav" id="navKernel" href="#kernel" aria-label={kernelLabel} title={kernelLabel}>
108
- <slot name="kernelIcon" />
138
+ <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>
109
139
  </a>
110
140
  <a class="tb-nav" id="navPosts" href="#posts" aria-label={postsLabel} title={postsLabel}>
111
141
  <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>
@@ -163,14 +193,47 @@ import AuthMount from "./AuthMount.astro";
163
193
  margin-inline: calc(-1 * max(var(--edge, 16px), env(safe-area-inset-left))) calc(-1 * max(var(--edge, 16px), env(safe-area-inset-right)));
164
194
  padding-block: max(8px, env(safe-area-inset-top)) 8px;
165
195
  padding-inline: max(var(--edge, 16px), env(safe-area-inset-left)) max(var(--edge, 16px), env(safe-area-inset-right));
166
- background: color-mix(in srgb, var(--bg) 70%, transparent);
167
- backdrop-filter: blur(22px) saturate(1.5);
168
- -webkit-backdrop-filter: blur(22px) saturate(1.5);
169
- /* a very faint hairline always; it firms up once content scrolls under the bar */
170
- border-block-end: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
171
- transition: border-color 0.3s var(--ease-out);
196
+
197
+ /* ── THE BAR IS A SHELF, NOT A TINT ──────────────────────────────────────────────────────
198
+ *
199
+ * queries: the toolbar reads as a black strip · why is the bar darker in dark than in light ·
200
+ * the bar looks flat against the column · toolbar material · --toolbar-bg
201
+ *
202
+ * It used to derive its fill from `--bg` — `color-mix(--bg 70%, transparent)` under a blur.
203
+ * One declaration, but it meant two different things: `--bg` is a near-white in light and #000
204
+ * in dark, so the same rule read "lighten a near-white a little" on one ground and "pull a
205
+ * lifted surface to pure black" on the other. Measured against the column it sits on: **-6 in
206
+ * light, -17 in dark.** That asymmetry was a by-product of the token, never a decision.
207
+ *
208
+ * So it takes the CARD's material instead, and the light comes from above like everything else:
209
+ * an opaque fill with a shadow cast downward, and the page sliding under it. The fill is opaque
210
+ * ON PURPOSE — the blur was inert in light anyway, because the column is 92% opaque and there is
211
+ * nothing behind it to refract.
212
+ *
213
+ * WHY IT IS HERE NOW. app.zone settled this, qr copied it verbatim — same two hex values, same
214
+ * two shadows — and BOTH left a comment saying it belongs in cuts. Two consumers is the
215
+ * promotion trigger, and the note the second one wrote is the specification:
216
+ *
217
+ * "the right end state is cuts reading `var(--toolbar-bg, <its current expression>)`,
218
+ * after which these four declarations delete themselves and the tokens alone do the work."
219
+ *
220
+ * The token remains the seam: a persona whose ground is genuinely different sets `--toolbar-bg`
221
+ * and `--toolbar-shadow` and this rule steps aside. */
222
+ background: var(--toolbar-bg, #1e202a);
223
+ box-shadow: var(--toolbar-shadow, 0 2px 12px -2px rgba(0, 0, 0, 0.55));
224
+ /* No hairline: it was the only thing separating the bar from the column, and the shadow does
225
+ that now. A line under an OPAQUE shelf reads as a seam rather than as a separation. */
226
+ border-block-end: none;
227
+ }
228
+ @media (prefers-color-scheme: light) {
229
+ .toolbar {
230
+ background: var(--toolbar-bg, #fbfcfe);
231
+ box-shadow: var(--toolbar-shadow, 0 1px 2px rgba(16, 24, 40, 0.06), 0 8px 20px -8px rgba(16, 24, 40, 0.16));
232
+ }
172
233
  }
173
- .toolbar.stuck { border-block-end-color: var(--border); }
234
+ /* `.stuck` is still set by the script below and still means "content is under the bar" — it simply
235
+ has nothing to paint now that the shelf is opaque and always shadowed. Kept because the class is
236
+ part of this cut's contract and a consumer may hang its own behaviour on it. */
174
237
  @media (min-width: 768px) { .toolbar { margin-inline: -28px; padding-inline: 28px; } }
175
238
  /* icons row + the scroll-reactive title strip below it */
176
239
  .tb-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
@@ -219,6 +282,37 @@ import AuthMount from "./AuthMount.astro";
219
282
  shrink the identity. */
220
283
  .tb-nav.tb-mark :global(img) { width: 19px; height: 19px; }
221
284
  .tb-nav :global(svg) { width: 19px; height: 19px; }
285
+
286
+ /* ── EQUAL INK, TWO BOXES ────────────────────────────────────────────────────────────────────
287
+ *
288
+ * queries: the toolbar icons are not the same size · the mark looks bigger than the glyphs ·
289
+ * the row is not level · 25.33px
290
+ *
291
+ * The paragraph above ends by naming the way out — *"grow the GLYPHS, not the identity"* — and
292
+ * this is that, arriving from the two repos that measured it.
293
+ *
294
+ * "19px" means two different things in this row. A Lucide glyph is drawn in a 24 viewBox with its
295
+ * ink between 3 and 21, so a 19px box puts **14.25px** of ink on screen; a mark runs to the edge
296
+ * of its own viewBox and puts **18.65px**. The mark therefore reads about a third larger, which is
297
+ * exactly what it did — and while the mark was still coloured, colour carried part of that
298
+ * difference. Now that it is monochrome at rest, the size gap is the only thing left and it shows.
299
+ *
300
+ * 19 × 24/18 = 25.33px is the box at which a glyph's INK measures 19px, the same 19px the mark
301
+ * occupies. `:not(.tb-mark)` is not a special case — it is the same rule read the other way: a
302
+ * glyph carries padding inside its viewBox and a mark does not, so equal ink needs unequal boxes.
303
+ *
304
+ * app.zone settled it, qr arrived at the identical number, and qr's own comment says why it is
305
+ * here: *"otherwise every persona has to remember to carry it, which is precisely the thing the
306
+ * shared shell exists to prevent."* */
307
+ .tb-nav:not(.tb-mark) :global(svg) { width: 25.33px; height: 25.33px; }
308
+
309
+ /* THE BUBBLE HANGS. Its silhouette's centre measures 1.05px below the line the other three sit on
310
+ (39.05 against 38.0) because the tail is part of the shape and pulls the box down with it — the
311
+ glyph is centred while the thing you SEE is not. A pixel is inside the noise on most rows; on
312
+ four icons at one size sharing one baseline it is the only thing left that is not level.
313
+ Nudged rather than redrawn, and `transform` on the svg never collides with the press feedback:
314
+ `.tb-nav:active`'s scale lives on the anchor. Both repos carry this verbatim. */
315
+ #navPosts :global(svg) { transform: translateY(-1px); }
222
316
  </style>
223
317
 
224
318
  <script>