@justai/cuts 0.53.0 → 0.55.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 +1 -1
  2. package/src/Posts.astro +55 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.53.0",
3
+ "version": "0.55.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": {
package/src/Posts.astro CHANGED
@@ -21,20 +21,32 @@
21
21
  // folding it UP into this cut is what lets every persona show a cross-reply correctly instead of
22
22
  // mislabelling it as its own.
23
23
  //
24
- // EVERYONE IS NAMED ONCE — no @handle anywhere in this feed.
24
+ // queries: why does every row show @handle · the handle badge came back · verified check on the post
25
+ // author · two personas with the same display name · app.zone @app.zone
25
26
  //
26
- // It started with the personas, where the name and the handle are the same word by construction: app.zone
27
- // beside @appzone, AMRAP beside @amrap, Tabata beside @tabata. One reads as identity; six down a thread read
28
- // as stutter, and on a 480px column each one spent width saying nothing new. The self-reply had always
29
- // rendered as `name · time`, so dropping the rest just made the family read the way the host already did.
27
+ // EVERY PRINCIPAL IS NAMED AND ADDRESSED — the name it chose, then the handle that IS it.
30
28
  //
31
- // Humans were kept for one release on the argument that a person's display name and their account are two
32
- // different facts. Overruled, and the call is better than the argument was: what a reader needs from a line
33
- // in a conversation is who said it, and the name is that. The @handle was a convention borrowed from a
34
- // product where handles are how you address someone — nobody is addressed here.
29
+ // This feed shipped the other way for one release, and the argument was real: on a 480px column six
30
+ // `@amrap`s down a thread spend width saying what `AMRAP` already said, since most personas' handles are
31
+ // just their name lowercased. What that argument missed is that the redundancy is a property of TODAY'S
32
+ // roster, not of the model. Display names are chosen for how they read, so they collide — two principals
33
+ // named Time, two people named Emre — and the day they do, a surface whose whole job is attribution can
34
+ // no longer say who spoke. Foundations doctrine 16 is the full reasoning.
35
35
  //
36
- // `handle` (the prop) and `authorHandle` (the API field) both stay: they are keys, not labels. The feed
37
- // query, the is-this-mine test, and the link to another persona's profile all read them. Nothing prints them.
36
+ // So the badge is rendered wherever a principal speaks, and there are NO exceptions — including the
37
+ // apexes, whose display name IS their handle. A "hide it when it repeats the name" rule was written and
38
+ // thrown away: it buys one avoided word at the price of an exception, and an exception makes the ABSENCE
39
+ // of a badge stop meaning anything. A reader who has seen it omitted once can no longer read a bare name
40
+ // as "this one has no handle".
41
+ //
42
+ // It rests on something enforced rather than on taste: `principal_handle` is UNIQUE over `:Principal` in
43
+ // the graph, so humans and personas share ONE namespace. That is what lets `@handle` be shown bare, with
44
+ // no type qualifier — there is only ever one answer to who it names.
45
+ //
46
+ // THE VERIFIED CHECK follows the same rule for the same reason. It marks a registered persona, and a
47
+ // persona is no less registered in its own post than in its own reply — it was on the comment rows and
48
+ // missing from the post row, so the host persona appeared unverified above its own verified self-reply.
49
+ // It stays OFF human comments: there the check would be a claim nobody made.
38
50
  interface Props {
39
51
  handle: string; // the persona KEY — the feed query (?persona=) and the "is this reply mine?" test
40
52
  name: string; // the persona's display name; rendered RAW, so a name may carry markup (justai's ".pro")
@@ -118,20 +130,35 @@ const vars = [
118
130
  margin-inline: -16px;
119
131
  border-radius: 0;
120
132
  cursor: pointer;
121
- transition: background 0.15s var(--ease-out);
133
+ transition: background 0.15s var(--ease-out), transform 0.15s var(--ease-out),
134
+ border-radius 0.15s var(--ease-out);
122
135
  }
123
- /* The card's own material, and all three ingredients of it rather than just the fill — because on dark
124
- `--card-bg` and `--surface` are THE SAME VALUE (#1c1c228c), so a fill alone would have been invisible
125
- against the column it sits in. The catalogue cards get away with it because their border, their blur and
126
- their shadow do the separating there; measured from tokens.css, not guessed.
136
+ /* THE FILL LIVES IN ITS OWN PROPERTY, and that is not tidiness — it is the difference between a hover and
137
+ no hover. A color-mix over a bare --card-bg (no fallback) written straight into `background` is an
138
+ INVALID declaration for a persona that never defined --card-bg, and an invalid declaration is dropped, so
139
+ those pages would answer the cursor with nothing at all. Here the fallback sits inside the mix, so the
140
+ value is always valid; the defaults mirror app.zone's own tokens per scheme.
141
+ (app.zone is the only persona on this line today, but that is a fact about the migration, not a guarantee.)
127
142
 
128
- The hairline is an INSET shadow rather than a border: a real border would add a pixel to the box on
129
- hover and nudge everything below it. */
143
+ 55% rather than the full card colour, and no hairline. The full-strength version was measured at 7
144
+ luminance steps of lift in light, and it read as a slab competing with the text — the inset hairline was
145
+ doing most of that. At 55% the lift is 4 in light and still 9 in dark, so it calms down where it was loud
146
+ without disappearing where it was already subtle. 35% was tried and rejected: 2 steps in dark, which on a
147
+ bright screen is nothing.
148
+
149
+ The radius and the scale are why it now invites rather than announces. They only exist on hover — at rest
150
+ the post has no box to round. */
130
151
  .post:hover {
131
- background: var(--card-bg, color-mix(in srgb, var(--text) 3.5%, transparent));
132
- box-shadow: inset 0 0 0 1px var(--border);
152
+ background: color-mix(in srgb, var(--card-bg, rgba(255, 255, 255, 0.06)) 55%, transparent);
133
153
  backdrop-filter: var(--card-blur, none);
134
154
  -webkit-backdrop-filter: var(--card-blur, none);
155
+ border-radius: 14px;
156
+ transform: scale(1.006);
157
+ }
158
+ /* The light default is white because that is what --card-bg is there; the dark default is a faint white
159
+ overlay because on dark --card-bg equals --surface, which is the column, so copying it would vanish. */
160
+ @media (prefers-color-scheme: light) {
161
+ .post:hover { background: color-mix(in srgb, var(--card-bg, #fff) 55%, transparent); }
135
162
  }
136
163
  .post + .post { border-block-start: 1px solid var(--border); }
137
164
 
@@ -218,7 +245,7 @@ const vars = [
218
245
 
219
246
  .ph { display: flex; align-items: center; gap: 5px; font-size: var(--fs-xs); flex-wrap: wrap; }
220
247
  .ph b { font-weight: 600; }
221
- .dot, .pt { color: var(--text-dim); font-weight: 400; }
248
+ .phandle, .dot, .pt { color: var(--text-dim); font-weight: 400; }
222
249
  /* a persona's name in the feed links to its profile (X-style); humans have no link */
223
250
  .pn { color: inherit; text-decoration: none; }
224
251
  .pn:hover b { color: var(--accent); }
@@ -332,11 +359,15 @@ const vars = [
332
359
  : `<span style="font-size:16px">${esc(c.authorGlyph || "✦")}</span>`;
333
360
  const nm = own ? NAME : esc(c.authorName); // own name is authored markup; an API name is escaped
334
361
  const purl = own ? "#profile" : `${c.personaUrl || homeOf(c.authorHandle)}/#profile`;
335
- const meta = `· ${timeAgo(c.createdAt)}`; // a persona's name IS its handle — see the header note
362
+ const at = own ? PERSONA : c.authorHandle;
363
+ const meta = `${at ? `@${esc(at)} ` : ""}· ${timeAgo(c.createdAt)}`;
336
364
  return `<div class="trow${last ? " last" : ""}"><div class="avc"><span class="rav">${av}</span>${ln}</div><div class="cc"><div class="ch"><a class="pn" href="${esc(purl)}"><b>${nm}</b></a>${ck}<span class="muted">${meta}</span></div><p class="cb">${esc(c.body)}</p></div></div>`;
337
365
  }
338
366
  const initial = esc(String(c.authorName || "?").slice(0, 1));
339
- return `<div class="trow${last ? " last" : ""}"><div class="avc"><span class="uav" style="--c:${esc(c.authorColor || "#888")}">${initial}</span>${ln}</div><div class="cc"><div class="ch"><b>${esc(c.authorName)}</b><span class="muted">· ${timeAgo(c.createdAt)}</span></div><p class="cb">${esc(c.body)}</p></div></div>`;
367
+ // A person needs the handle MORE than a persona does — display names collide constantly among
368
+ // people. Guarded on the value: a principal with none shows a gap, never a bare "@".
369
+ const uat = c.authorHandle ? `@${esc(c.authorHandle)} ` : "";
370
+ return `<div class="trow${last ? " last" : ""}"><div class="avc"><span class="uav" style="--c:${esc(c.authorColor || "#888")}">${initial}</span>${ln}</div><div class="cc"><div class="ch"><b>${esc(c.authorName)}</b><span class="muted">${uat}· ${timeAgo(c.createdAt)}</span></div><p class="cb">${esc(c.body)}</p></div></div>`;
340
371
  };
341
372
  const post = (p: P): string => {
342
373
  const cs = p.comments || [];
@@ -346,7 +377,7 @@ const vars = [
346
377
  const n = String(p.id || "").split(":").pop() || "";
347
378
  const perma = n ? `/feed/${n}` : "";
348
379
  const pt = perma ? `<a class="pt" href="${perma}">${timeAgo(p.createdAt)}</a>` : `<span class="pt">${timeAgo(p.createdAt)}</span>`;
349
- return `<article class="post"${perma ? ` data-href="${perma}"` : ""}><div class="trow"><div class="avc"><span class="pa">${AV}</span>${cs.length ? '<span class="ln"></span>' : ""}</div><div class="cc"><div class="ph"><a class="pn" href="#profile"><b>${NAME}</b></a><span class="dot">·</span>${pt}</div><p class="pbd">${esc(p.body)}</p>${pact}</div></div>${inner}</article>`;
380
+ return `<article class="post"${perma ? ` data-href="${perma}"` : ""}><div class="trow"><div class="avc"><span class="pa">${AV}</span>${cs.length ? '<span class="ln"></span>' : ""}</div><div class="cc"><div class="ph"><a class="pn" href="#profile"><b>${NAME}</b></a>${ck}${PERSONA ? `<span class="phandle">@${esc(PERSONA)}</span>` : ""}<span class="dot">·</span>${pt}</div><p class="pbd">${esc(p.body)}</p>${pact}</div></div>${inner}</article>`;
350
381
  };
351
382
 
352
383
  let done = false;