@justai/cuts 0.122.0 → 0.125.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 +3 -3
- package/src/Toolbar.astro +121 -19
- package/src/ground.css +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justai/cuts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
4
4
|
"description": "A persona's named parts — 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.
|
|
45
|
-
"@justai/ui": "^0.
|
|
44
|
+
"@justai/core": "^0.32.0",
|
|
45
|
+
"@justai/ui": "^0.21.1",
|
|
46
46
|
"@justai/atrium": "^0.1.9"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/Toolbar.astro
CHANGED
|
@@ -82,6 +82,8 @@ interface Props {
|
|
|
82
82
|
// half's default, which means copying core's `.pname` / `.handle` selectors into a second place
|
|
83
83
|
// that nothing keeps in step. The strip is one line and its vocabulary should be one kind of
|
|
84
84
|
// thing — names or sections, not one of each — so the API says so out loud.
|
|
85
|
+
kernelTitle?: string;
|
|
86
|
+
postsTitle?: string;
|
|
85
87
|
|
|
86
88
|
// queries: toolbar shows a generic person icon · the profile icon is not my persona ·
|
|
87
89
|
// how do I put my own mark in the toolbar · avatar
|
|
@@ -153,7 +155,7 @@ interface Props {
|
|
|
153
155
|
import BackToZone from "./BackToZone.astro";
|
|
154
156
|
const {
|
|
155
157
|
sectionsLabel, profileLabel, kernelLabel, postsLabel,
|
|
156
|
-
files = true, filesLabel = "Files", avatar = "/icon.svg", parent,
|
|
158
|
+
files = true, filesLabel = "Files", kernelTitle, postsTitle, avatar = "/icon.svg", parent,
|
|
157
159
|
} = Astro.props;
|
|
158
160
|
// A child's row answers UP to its parent, not out to the zone, and has no stream of its own — so
|
|
159
161
|
// both default off the moment `parent` is given, rather than each consumer remembering to say so.
|
|
@@ -164,7 +166,8 @@ import AuthMount from "./AuthMount.astro";
|
|
|
164
166
|
<!-- The two titles reach the script as data attributes rather than through `define:vars`, which would
|
|
165
167
|
make this script inline and it has an import. Astro drops an attribute whose value is undefined,
|
|
166
168
|
so a persona that passes neither leaves no trace in the markup at all. -->
|
|
167
|
-
<
|
|
169
|
+
<div class="tb-slot">
|
|
170
|
+
<header class="toolbar" data-kernel-title={kernelTitle} data-posts-title={postsTitle}>
|
|
168
171
|
<div class="tb-row">
|
|
169
172
|
<div class="tb-left">
|
|
170
173
|
{home && <BackToZone apex={apex} />}
|
|
@@ -265,14 +268,73 @@ import AuthMount from "./AuthMount.astro";
|
|
|
265
268
|
<AuthMount />
|
|
266
269
|
</div>
|
|
267
270
|
</div>
|
|
271
|
+
<div class="tb-title" id="tbTitle"><span class="tb-title-text" id="tbTitleText"></span></div>
|
|
268
272
|
</header>
|
|
273
|
+
</div>
|
|
269
274
|
|
|
270
275
|
<style>
|
|
271
276
|
/* ---- app toolbar — sticky to the top of the active region (kernel + posts); it never
|
|
272
277
|
enters the Profile (it lives inside .below). Flat / knife-cut: a left group and a
|
|
273
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; }
|
|
297
|
+
/* queries: the title strip moves the fold · the page drifts after it lands · a band of posts under
|
|
298
|
+
* the game · tb-slot min-height · the slot follows the bar · the strip pushes the page down
|
|
299
|
+
*
|
|
300
|
+
* ── THE BOX IS THE SLOT'S OWN, SO IT TRAVELS WITH THE SLOT ─────────────────────────────────────
|
|
301
|
+
*
|
|
302
|
+
* `position: sticky` never leaves flow: it reserves its full border box, so whatever the bar's LIVE
|
|
303
|
+
* height is at any instant, that is where everything under it sits. The strip opens and closes over
|
|
304
|
+
* 0.35s inside that box — so without a reserved height the shell breathes 26px and the whole
|
|
305
|
+
* document below it breathes with it. That is the drift the founder lost days to.
|
|
306
|
+
*
|
|
307
|
+
* IT LIVED IN `@justai/ui`'s base.css UNTIL 2026-08-06, and measured on pin.games that was the same
|
|
308
|
+
* as not existing: a persona that never imports base.css got the slot's structure from here and its
|
|
309
|
+
* BOX from a file it does not load, so the slot collapsed to the bar and posts moved 26px between
|
|
310
|
+
* the kernel and the profile. The first law again — a component brings its own ground — and the
|
|
311
|
+
* slot is this component's element, so its box is this component's business. The rule that keeps
|
|
312
|
+
* the whole strip safe cannot be the one rule that lives somewhere the strip does not.
|
|
313
|
+
*
|
|
314
|
+
* AND THE RESERVED BAND WEARS THE BAR'S OWN MATERIAL, not the room's. A slot taller than its bar is
|
|
315
|
+
* 26px of SOMETHING under the shelf whenever the strip is shut — the state at the profile, on
|
|
316
|
+
* screen, at rest. Painted in the persona's ground it was a hard black band between a light profile
|
|
317
|
+
* and a light kernel: the founder photographed it within a minute. It is the BAR'S box, so it takes
|
|
318
|
+
* the bar's fill, bleeding by the same `--edge` expression so the two cannot leave slivers. */
|
|
319
|
+
.tb-slot { min-height: var(--shell-top, 78px); }
|
|
320
|
+
.tb-slot::before {
|
|
321
|
+
content: "";
|
|
322
|
+
position: absolute;
|
|
323
|
+
z-index: -1;
|
|
324
|
+
inset-block: 0;
|
|
325
|
+
inset-inline:
|
|
326
|
+
calc(-1 * max(var(--edge, 16px), env(safe-area-inset-left)))
|
|
327
|
+
calc(-1 * max(var(--edge, 16px), env(safe-area-inset-right)));
|
|
328
|
+
background: var(--toolbar-bg, #1e202a);
|
|
329
|
+
}
|
|
330
|
+
/* The shelf's fill has two branches and so must its box: `--toolbar-bg` is one token but its
|
|
331
|
+
FALLBACK differs by scheme, and a band carrying only the dark default is a black strip on a light
|
|
332
|
+
page — which is exactly what the founder photographed a minute after the first attempt. */
|
|
333
|
+
@media (prefers-color-scheme: light) {
|
|
334
|
+
.tb-slot::before { background: var(--toolbar-bg, #fbfcfe); }
|
|
335
|
+
}
|
|
274
336
|
.toolbar {
|
|
275
|
-
position
|
|
337
|
+
/* position, top and z-index live on .tb-slot above — this element is static inside it now. */
|
|
276
338
|
display: flex; flex-direction: column;
|
|
277
339
|
/* full-bleed: the bar (frosted bg + hairline) touches the contained-view edges; the icons stay
|
|
278
340
|
aligned with the body via matching inline padding. --edge is the persona's page inline padding
|
|
@@ -325,23 +387,54 @@ import AuthMount from "./AuthMount.astro";
|
|
|
325
387
|
@media (min-width: 640px) { .toolbar { margin-inline: -28px; padding-inline: 28px; } }
|
|
326
388
|
/* icons row + the scroll-reactive title strip below it */
|
|
327
389
|
.tb-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
328
|
-
/* queries:
|
|
390
|
+
/* queries: --tb-strip · the title strip's height · the toolbar changes height · 26px ·
|
|
391
|
+
* the kernel is 26px short · the game frame does not reach the fold
|
|
392
|
+
*
|
|
393
|
+
* ── THE ONE NUMBER THAT MAKES THIS ROW'S HEIGHT A MOVING TARGET, DECLARED SO IT CAN BE READ ──
|
|
394
|
+
*
|
|
395
|
+
* The strip opens and closes over 0.35s, so `.toolbar`'s height is anywhere in a 26px range at any
|
|
396
|
+
* given moment — and anything that measures the row and feeds a LAYOUT with it is sampling an
|
|
397
|
+
* animation. `--shell-top` did exactly that and the kernel's height came out 26px short whenever
|
|
398
|
+
* the page happened to be measured with the strip open, which is to say whenever somebody opened
|
|
399
|
+
* the persona at #kernel. It reads as a strip of the next section showing above the fold.
|
|
400
|
+
*
|
|
401
|
+
* So the number is a custom property rather than a literal: this rule uses it, and @justai/core's
|
|
402
|
+
* `personaScroll` reads it to compute the row's EXPANDED height without waiting for or racing the
|
|
403
|
+
* transition — `toolbar - strip` is the row, and the row plus this is what the bar will be. One
|
|
404
|
+
* declaration, two readers, and nothing samples a moving value. */
|
|
405
|
+
.toolbar { --tb-strip: 26px; }
|
|
406
|
+
/* queries: the bar grows after load · why does the strip animate on arrival · data-motion ·
|
|
407
|
+
* first paint animation · can this transition wait for the first interaction
|
|
408
|
+
*
|
|
409
|
+
* ── THE TRANSITION IS ALSO A DAMPER, WHICH IS WHY IT CANNOT SIMPLY WAIT ─────────────────────
|
|
410
|
+
*
|
|
411
|
+
* The founder asked the obvious and right question: *"şu geçiş süresi güzel — ama neden bu özellik
|
|
412
|
+
* kullanıcının ilk interaction'ından sonra aktif edilmiyor?"* A persona lands on its kernel, so the
|
|
413
|
+
* strip's correct state on the first frame is open; animating into it grows the bar 26px just after
|
|
414
|
+
* the page appears, which is a glitch nobody asked for.
|
|
329
415
|
*
|
|
330
|
-
*
|
|
416
|
+
* IT WAS TRIED, AND IT BREAKS SOMETHING ELSE. Gated behind a `data-motion` flag set on the first
|
|
417
|
+
* pointer/key/wheel, the strip snaps open during the landing — and that 26px arrives in a SINGLE
|
|
418
|
+
* frame, inside `#below`, while the scroll spy is deciding which cut is active. That is the
|
|
419
|
+
* bistable oscillator `personaScroll` documents at length: the spy measures a page its own output
|
|
420
|
+
* moved. Measured: with the gate in place, `#navProfile` no longer navigates — the page re-asserts
|
|
421
|
+
* the kernel and stays. The 0.35s is not only easing; it is what keeps that step out of one frame.
|
|
331
422
|
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* of those two numbers and was therefore wrong in the other state. That gap is a band of the next
|
|
336
|
-
* section above the fold; on a game persona, whose kernel is an EXACT height, it is on the glass.
|
|
337
|
-
* It also coupled the scroll spy to the layout: the spy decided which cut was current, that opened
|
|
338
|
-
* the strip, the strip moved the page, and the spy read the page it had just moved.
|
|
423
|
+
* SO THE HEIGHT IS NEVER READ INSTEAD. `--shell-top` is computed as `row + --tb-strip` — the row
|
|
424
|
+
* being the bar minus whatever the strip currently is, which is the same number at every frame of
|
|
425
|
+
* the transition. Nothing samples the animation, so nothing depends on when it was sampled.
|
|
339
426
|
*
|
|
340
|
-
* The founder's
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
|
|
427
|
+
* The real fix for the founder's question is a toolbar whose height does not change at all: reserve
|
|
428
|
+
* the strip's space always and fade only its content. That removes the feedback at the source and
|
|
429
|
+
* makes the gate free — and it is a fleet-wide design decision about how the bar looks at the
|
|
430
|
+
* profile, so it is his to take rather than one to slip in. */
|
|
431
|
+
.tb-title { display: flex; align-items: center; height: 0; overflow: hidden; opacity: 0; transition: height 0.35s var(--ease-out), opacity 0.25s var(--ease-out); }
|
|
432
|
+
/* The fallback is not decoration: the token is declared on `.toolbar` and consumed here, which is
|
|
433
|
+
a different selector — so the framework's own token spec reads it as ungrounded and is right to.
|
|
434
|
+
A rule that silently collapses to `height: 0` would close the strip and say nothing. */
|
|
435
|
+
.tb-title.show { height: 26px; height: var(--tb-strip, 26px); opacity: 1; }
|
|
436
|
+
.tb-title-text { font-size: var(--fs-sm); font-weight: 600; letter-spacing: -0.01em; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity 0.16s var(--ease-out); }
|
|
437
|
+
.tb-title-text.fade { opacity: 0; }
|
|
345
438
|
/* nav lives on the left, beside the back affordance — the middle is intentionally empty */
|
|
346
439
|
.tb-left { display: flex; align-items: center; gap: 2px; min-width: 0; }
|
|
347
440
|
.tb-nav-group { display: flex; align-items: center; gap: 2px; }
|
|
@@ -508,6 +601,15 @@ import AuthMount from "./AuthMount.astro";
|
|
|
508
601
|
const ownMark = document.querySelector("#navProfile img");
|
|
509
602
|
if (ownMark) adoptMark({ id: "navProfile" });
|
|
510
603
|
|
|
511
|
-
//
|
|
512
|
-
|
|
604
|
+
// Both or neither — see the props above. When both are set the strip names the SECTIONS; core's
|
|
605
|
+
// default, which names the persona and its handle, is left entirely alone otherwise. Nothing here
|
|
606
|
+
// reproduces that default, which is the point: `.pname` and `.handle` stay core's business.
|
|
607
|
+
const bar = document.querySelector(".toolbar") as HTMLElement | null;
|
|
608
|
+
const kt = bar?.dataset.kernelTitle;
|
|
609
|
+
const pt = bar?.dataset.postsTitle;
|
|
610
|
+
personaScroll(
|
|
611
|
+
kt && pt
|
|
612
|
+
? { titleFor: (id: string) => (id === "kernel" ? kt : id === "posts" ? pt : "") }
|
|
613
|
+
: undefined,
|
|
614
|
+
);
|
|
513
615
|
</script>
|
package/src/ground.css
CHANGED
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
/* What the shell takes above the kernel — see @justai/ui base.css, which measures it. Ground only:
|
|
115
115
|
`:where()` is specificity zero, so a consumer that ships the shell always wins, and a surface that
|
|
116
116
|
ships no shell still lands its cuts clear of a sticky bar instead of under one. */
|
|
117
|
-
:where(:root) { --shell-top:
|
|
117
|
+
:where(:root) { --shell-top: 78px; }
|
|
118
118
|
|
|
119
119
|
:where(html) {
|
|
120
120
|
background: var(--bg);
|