@justai/cuts 0.100.0 → 0.103.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 +2 -2
- package/src/BackToZone.astro +40 -2
- package/src/Catalogue.astro +19 -1
- package/src/Posts.astro +21 -8
- package/src/Toolbar.astro +4 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justai/cuts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A persona's named parts
|
|
3
|
+
"version": "0.103.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",
|
package/src/BackToZone.astro
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
---
|
|
2
|
-
//
|
|
2
|
+
// queries: back to the zone · the home icon goes to the wrong apex · a game persona links to
|
|
3
|
+
// app.zone · toolbar home icon · pin.games home affordance · BackToZone apex prop
|
|
4
|
+
//
|
|
5
|
+
// THE HOME AFFORDANCE NAMES A DESTINATION, so the destination cannot be assumed. This component was
|
|
6
|
+
// written when there was one storefront and it hardcoded three things about it: the href, the label,
|
|
7
|
+
// and the geometry of the mark. On pin.games all three were wrong at once — a game persona's toolbar
|
|
8
|
+
// offering to take a visitor to the app store — and nothing could see it but a screenshot.
|
|
9
|
+
//
|
|
10
|
+
// `apex` defaults to app.zone, so every existing caller is unchanged and no persona has to be bumped
|
|
11
|
+
// to keep the behaviour it already has. A pin.games citizen passes apex="pin.games".
|
|
12
|
+
//
|
|
13
|
+
// THE TWO MARKS ARE SIBLINGS, DRAWN THE SAME WAY. app.zone is four separated corner brackets around a
|
|
14
|
+
// centre — a zone is the space the corners enclose. pin.games is three separated strokes making a
|
|
15
|
+
// play triangle. The zone has four corners; the play has three edges. Same radii, same palette, same
|
|
16
|
+
// idea of a figure assembled from pieces, which is what keeps them reading as one family at 20px.
|
|
17
|
+
//
|
|
18
|
+
// Home affordance → the apex (the platform / App Store home). Standard on every app:
|
|
3
19
|
// always visible at the top-left of the toolbar. Also tidies the ?ref / ?lang hints
|
|
4
20
|
// from the URL so shared links stay clean.
|
|
5
21
|
//
|
|
@@ -26,9 +42,22 @@
|
|
|
26
42
|
// lighting was never visible at this size, and in one flat ink there is nothing to shade. The
|
|
27
43
|
// geometry here is a second copy of the one in that file, which is authored by hand and generated by
|
|
28
44
|
// nothing — a redraw has to be carried across to this component as well.
|
|
45
|
+
interface Props {
|
|
46
|
+
/** Which storefront "home" means here. Defaults to app.zone — the fleet as it stood. */
|
|
47
|
+
apex?: "app.zone" | "pin.games";
|
|
48
|
+
}
|
|
49
|
+
const { apex = "app.zone" } = Astro.props;
|
|
50
|
+
const href = `https://${apex}`;
|
|
29
51
|
---
|
|
30
52
|
|
|
31
|
-
<a class="home" href=
|
|
53
|
+
<a class="home" href={href} data-home aria-label={apex} title={apex}>
|
|
54
|
+
{apex === "pin.games" ? (
|
|
55
|
+
<svg class="pgmark" viewBox="128 128 768 768" aria-hidden="true" stroke-width="86" stroke-linecap="round">
|
|
56
|
+
<path class="arm a1" d="M330 355 L330 669"/>
|
|
57
|
+
<path class="arm a2" d="M379.3 324.3 L710.7 487.7"/>
|
|
58
|
+
<path class="arm a3" d="M379.3 699.7 L710.7 536.3"/>
|
|
59
|
+
</svg>
|
|
60
|
+
) : (
|
|
32
61
|
<svg class="azmark" viewBox="-8.5 0 928 928" aria-hidden="true">
|
|
33
62
|
<path class="arm a1" d="M360.365 0C372.599 0 382.517 9.91778 382.517 22.152V97.7555C382.517 109.99 372.599 119.907 360.365 119.907H203.398C157.082 119.907 119.537 157.453 119.537 203.769V353.64C119.537 370.244 106.077 383.704 89.4732 383.704H22.152C9.91778 383.704 0 373.786 0 361.552V175.633C0 78.6337 78.6338 0 175.634 0H360.365Z"/>
|
|
34
63
|
<path class="arm a2" d="M550.635 2.37305C538.401 2.37305 528.483 12.2908 528.483 24.525V100.129C528.483 112.363 538.401 122.28 550.635 122.28H707.602C753.918 122.28 791.463 159.826 791.463 206.142V356.013C791.463 372.617 804.923 386.077 821.527 386.077H888.848C901.082 386.077 911 376.159 911 363.925V178.006C911 81.0068 832.366 2.37305 735.366 2.37305H550.635Z"/>
|
|
@@ -42,6 +71,7 @@
|
|
|
42
71
|
<linearGradient id="azHome4" x1="450.555" y1="993.674" x2="6.32921" y2="550.635" gradientUnits="userSpaceOnUse"><stop stop-color="#5285DC"/><stop offset="1" stop-color="#2E62C6"/></linearGradient>
|
|
43
72
|
</defs>
|
|
44
73
|
</svg>
|
|
74
|
+
)}
|
|
45
75
|
</a>
|
|
46
76
|
|
|
47
77
|
<style>
|
|
@@ -62,6 +92,14 @@
|
|
|
62
92
|
.azmark .arm,
|
|
63
93
|
.azmark .ctr { fill: currentColor; }
|
|
64
94
|
|
|
95
|
+
/* The play mark is STROKED where the zone mark is filled, so `currentColor` has to be told twice —
|
|
96
|
+
a stroke that inherits `fill` is an invisible mark, and an invisible mark in a toolbar reads as a
|
|
97
|
+
missing icon rather than a broken rule. */
|
|
98
|
+
.pgmark .arm { fill: none; stroke: currentColor; }
|
|
99
|
+
.home:hover .pgmark .a1, .home:focus-visible .pgmark .a1 { stroke: #3D6FD9; }
|
|
100
|
+
.home:hover .pgmark .a2, .home:focus-visible .pgmark .a2 { stroke: #E8503A; }
|
|
101
|
+
.home:hover .pgmark .a3, .home:focus-visible .pgmark .a3 { stroke: #F5A623; }
|
|
102
|
+
|
|
65
103
|
.home:hover .azmark .a1, .home:focus-visible .azmark .a1 { fill: url(#azHome1); }
|
|
66
104
|
.home:hover .azmark .a2, .home:focus-visible .azmark .a2 { fill: url(#azHome2); }
|
|
67
105
|
.home:hover .azmark .a3, .home:focus-visible .azmark .a3 { fill: url(#azHome3); }
|
package/src/Catalogue.astro
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
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" :
|
|
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
|
-
|
|
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
|
package/src/Toolbar.astro
CHANGED
|
@@ -61,6 +61,8 @@ interface Props {
|
|
|
61
61
|
// destination, and justai.pro is above it (sending the parent "home" to a child platform is
|
|
62
62
|
// the wrong direction). Every other persona keeps it.
|
|
63
63
|
home?: boolean;
|
|
64
|
+
/** Which storefront the home affordance points at. Defaults to app.zone — see BackToZone. */
|
|
65
|
+
apex?: "app.zone" | "pin.games";
|
|
64
66
|
|
|
65
67
|
// Posts is required only where there IS a stream (doctrine 02 — an empty cut shipped to look
|
|
66
68
|
// complete is the buffet). A child persona has none, so `parent` turns this off by default.
|
|
@@ -157,7 +159,7 @@ const {
|
|
|
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.
|
|
160
|
-
const { home = !parent, posts = !parent } = Astro.props;
|
|
162
|
+
const { home = !parent, posts = !parent, apex = "app.zone" } = Astro.props;
|
|
161
163
|
import AuthMount from "./AuthMount.astro";
|
|
162
164
|
---
|
|
163
165
|
|
|
@@ -167,7 +169,7 @@ import AuthMount from "./AuthMount.astro";
|
|
|
167
169
|
<header class="toolbar" data-kernel-title={kernelTitle} data-posts-title={postsTitle}>
|
|
168
170
|
<div class="tb-row">
|
|
169
171
|
<div class="tb-left">
|
|
170
|
-
{home && <BackToZone />}
|
|
172
|
+
{home && <BackToZone apex={apex} />}
|
|
171
173
|
<nav class="tb-nav-group" aria-label={sectionsLabel}>
|
|
172
174
|
{/* POSITION ONE OF A CHILD'S ROW: the persona this one is mounted inside. `/icon.svg` is the
|
|
173
175
|
parent's face here, because a child runs in the parent's origin — the same path, read from
|