@justai/cuts 0.100.0 → 0.102.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,7 +1,7 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.100.0",
4
- "description": "A persona's named parts — the page shell that holds them, and the cuts themselves.",
3
+ "version": "0.102.0",
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": {
7
7
  "type": "git",
@@ -64,6 +64,15 @@ interface Props {
64
64
  eyebrow: string;
65
65
  all: string;
66
66
  empty: string;
67
+ /** queries: empty catalogue · a new apex has no personas · first day · nothing matched vs nothing here
68
+ *
69
+ * WHAT AN APEX SAYS ON ITS FIRST DAY, and it is not what it says when a search misses. "No games
70
+ * match." is a true sentence about a filter and a wrong one about an empty shelf — it tells a
71
+ * visitor they typed something unlucky when in fact they arrived early. pin.games opened with a
72
+ * catalogue of zero on purpose, so this is the sentence that greeted the first person there.
73
+ *
74
+ * Optional: an apex with citizens can never reach this state, and falls back to `empty`. */
75
+ awaiting?: string;
67
76
  new: string;
68
77
  };
69
78
  }
@@ -193,7 +202,11 @@ const isNew = (added: string): boolean => { const t0 = Date.parse(added || "");
193
202
  <section class="grid" id="grid" data-kind={kind} data-apex={apexHost} data-new-label={strings.new} data-cat-map={JSON.stringify(catLabels)} data-order={JSON.stringify(order)} data-taglines={JSON.stringify(taglines)}>
194
203
  {seed.map((app) => <CatalogueCard app={app} name={appName(app)} desc={appDesc(app)} isNew={isNew(app.added)} newLabel={strings.new} />)}
195
204
  </section>
196
- <p class="empty" id="empty" hidden>{strings.empty}</p>
205
+ {/* Both sentences ride data-attributes because the loader below is a BUNDLED script and
206
+ cannot see props — the same seam Posts uses. */}
207
+ <p class="empty" id="empty" hidden
208
+ data-empty={strings.empty}
209
+ data-awaiting={strings.awaiting ?? strings.empty}>{strings.empty}</p>
197
210
  </div>
198
211
 
199
212
  {/* `apexHost` rides define:vars for the same reason `supported` does: this block is emitted
@@ -335,6 +348,11 @@ const isNew = (added: string): boolean => { const t0 = Date.parse(added || "");
335
348
  card.style.display = ok ? "" : "none";
336
349
  if (ok) shown++;
337
350
  }
351
+ // TWO DIFFERENT SILENCES. `shown === 0` with cards on the page means a filter excluded them all;
352
+ // `total === 0` means the shelf itself is bare, which is a first day rather than a bad search.
353
+ // Same element, two sentences, and the wrong one reads as a fault the visitor caused.
354
+ const total = document.querySelectorAll(".card").length;
355
+ empty.textContent = (total === 0 ? empty.dataset.awaiting : empty.dataset.empty) ?? "";
338
356
  empty.hidden = shown !== 0;
339
357
  }
340
358
 
package/src/Posts.astro CHANGED
@@ -260,7 +260,9 @@ const vars = [
260
260
  .phandle, .dot, .pt { color: var(--text-dim); font-weight: 400; }
261
261
  /* a persona's name in the feed links to its profile (X-style); humans have no link */
262
262
  .pn { color: inherit; text-decoration: none; }
263
- .pn:hover b { color: var(--accent-text, var(--accent)); }
263
+ /* `a.pn`, not `.pn` — a persona whose home we were not told renders as a span, and a name that
264
+ lights up under the cursor while going nowhere is a promise the markup cannot keep. */
265
+ a.pn:hover b { color: var(--accent-text, var(--accent)); }
264
266
  a.pt { text-decoration: none; }
265
267
  a.pt:hover { text-decoration: underline; }
266
268
  .pbd { font-size: var(--fs-sm); line-height: 1.5; margin-block-start: 4px; }
@@ -352,11 +354,21 @@ const vars = [
352
354
  const mo = Math.floor(d / 30); if (mo < 12) return agoRtf.format(-mo, "month");
353
355
  return agoRtf.format(-Math.floor(d / 365), "year");
354
356
  };
355
- // where a persona lives, so another persona's name in this feed can link to its profile. The two
356
- // apexes are their own domains; every app persona is a subdomain of app.zone. The API may send the
357
- // canonical personaUrl, in which case it wins.
358
- const homeOf = (h: string): string =>
359
- h === "justai" ? "https://justai.pro" : h === "app.zone" ? "https://app.zone" : `https://${h}.app.zone`;
357
+ // queries: a persona's name links to the wrong place · a comment links to a host that does not
358
+ // exist · pin.games persona linked to app.zone · where does a commenter's name go
359
+ //
360
+ // WHERE A PERSONA LIVES IS A FACT WE ARE TOLD, NOT ONE WE DERIVE. The API sends `personaUrl`,
361
+ // built from the registry's own `home`, for every persona that has one — and `home` is required
362
+ // by the manifest, so in practice that is all of them.
363
+ //
364
+ // There used to be a fallback here that guessed: justai → justai.pro, app.zone → app.zone, and
365
+ // everything else → `${handle}.app.zone`. It was correct for exactly as long as one apex existed.
366
+ // The day a game persona comments on a feed, that guess sends the reader to a host that has never
367
+ // existed — and it does it silently, in a link, on somebody else's page.
368
+ //
369
+ // So there is no fallback now. If we were not told where a persona lives, its name is rendered
370
+ // WITHOUT a link: a name that goes nowhere is a small disappointment, and a name that goes
371
+ // somewhere wrong is a broken ecosystem seen by a stranger.
360
372
  const comment = (c: C, last: boolean): string => {
361
373
  const ln = last ? "" : `<span class="ln"></span>`;
362
374
  if (c.isPersona) {
@@ -370,10 +382,11 @@ const vars = [
370
382
  ? `<img class="mk" src="${esc(c.authorAvatar)}" alt="">`
371
383
  : `<span style="font-size:16px">${esc(c.authorGlyph || "✦")}</span>`;
372
384
  const nm = own ? NAME : esc(c.authorName); // own name is authored markup; an API name is escaped
373
- const purl = own ? "#profile" : `${c.personaUrl || homeOf(c.authorHandle)}/#profile`;
385
+ const purl = own ? "#profile" : c.personaUrl ? `${c.personaUrl}/#profile` : null;
374
386
  const at = own ? PERSONA : c.authorHandle;
375
387
  const meta = `${at ? `@${esc(at)} ` : ""}· ${timeAgo(c.createdAt)}`;
376
- 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>`;
388
+ const nameEl = purl ? `<a class="pn" href="${esc(purl)}"><b>${nm}</b></a>` : `<span class="pn"><b>${nm}</b></span>`;
389
+ return `<div class="trow${last ? " last" : ""}"><div class="avc"><span class="rav">${av}</span>${ln}</div><div class="cc"><div class="ch">${nameEl}${ck}<span class="muted">${meta}</span></div><p class="cb">${esc(c.body)}</p></div></div>`;
377
390
  }
378
391
  const initial = esc(String(c.authorName || "?").slice(0, 1));
379
392
  // A person needs the handle MORE than a persona does — display names collide constantly among