@justai/cuts 0.141.0 → 0.143.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.141.0",
4
- "description": "A persona's named parts \u2014 the page shell that holds them, and the cuts themselves.",
3
+ "version": "0.143.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",
@@ -43,7 +43,24 @@ interface Props {
43
43
  /** BCP-47 code of the page — the language the registry is asked for. */
44
44
  lang: string;
45
45
  /** Which citizens this apex lists. The whole difference between two storefronts. */
46
- kind: string;
46
+ /**
47
+ * queries: Catalogue kind prop · data-kind on the grid · what kind does a mixed household pass ·
48
+ * vestigial prop · why is kind optional now
49
+ *
50
+ * VESTIGIAL, AND KEPT ONLY SO THE TWO EXISTING APEXES STILL COMPILE.
51
+ *
52
+ * It fed one thing: `data-kind` on the grid, which the runtime append read to ask the registry
53
+ * `?kind=`. That question moved to `?apex=` when a household arrived whose citizens are not all one
54
+ * species, and the attribute lost its last reader in the same change. (`data-kind` on `<html>` is a
55
+ * DIFFERENT attribute, written by Base and read by @justai/ui for a game kernel's geometry — that
56
+ * one is alive.)
57
+ *
58
+ * Optional rather than deleted because `app.zone` and `pin.games` still pass it, and a required prop
59
+ * removed is a build error in two repos for no gain. It goes when they next build. A mixed household
60
+ * has no answer to it at all, which is the clearest possible statement that it was the wrong
61
+ * question here.
62
+ */
63
+ kind?: string;
47
64
  /** The apex's own host, used for the referral parameter on every card. */
48
65
  apexHost: string;
49
66
  /** Layer one. Empty is valid and correct for an apex with nothing seeded yet. */
@@ -85,7 +102,7 @@ interface Props {
85
102
  };
86
103
  }
87
104
  const {
88
- lang, kind, apexHost,
105
+ lang, apexHost,
89
106
  seed = [], order = [], taglines = {}, catLabels = {}, entries = {},
90
107
  locales, strings,
91
108
  } = Astro.props;
@@ -211,7 +228,7 @@ const isNew = (added: string): boolean => { const t0 = Date.parse(added || "");
211
228
  {/* taglines ride the same channel as newLabel and catMap: a data attribute on the grid. The
212
229
  runtime append builds its cards in plain strings and cannot reach this component's scope, so
213
230
  anything it needs has to arrive this way. Keyed by host, which is what the API calls `home`. */}
214
- <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)}>
231
+ <section class="grid" id="grid" data-apex={apexHost} data-new-label={strings.new} data-cat-map={JSON.stringify(catLabels)} data-order={JSON.stringify(order)} data-taglines={JSON.stringify(taglines)}>
215
232
  {seed.map((app) => <CatalogueCard app={app} name={appName(app)} desc={appDesc(app)} isNew={isNew(app.added)} newLabel={strings.new} />)}
216
233
  </section>
217
234
  {/* queries: runtime cards are unstyled · the card has no CSS · an apex with no seed · scope cid ·
@@ -90,7 +90,10 @@ const icon = `${app.url}/icon.svg`;
90
90
  width: 60px;
91
91
  height: 60px;
92
92
  border-radius: var(--radius-md);
93
- object-fit: cover;
93
+ /* contain: this is a persona's mark, not a photograph. A square tile is unaffected — the two
94
+ declarations agree on square art — and a mark taller than its box keeps its ends instead of
95
+ having them cropped. See the note on .pa .mk in Posts.astro. */
96
+ object-fit: contain;
94
97
  box-shadow: 0 8px 20px -12px rgba(0, 0, 0, 0.35);
95
98
  }
96
99
 
@@ -269,8 +269,13 @@ const chrome = face === "page";
269
269
  .pp[data-face="sheet"] .app { max-width: none; margin-inline: 0; border: 0; background: none;
270
270
  backdrop-filter: none; -webkit-backdrop-filter: none; }
271
271
 
272
- /* a mark fills its box; the picture inside it covers */
273
- .pp .mk { display: block; width: 100%; height: 100%; object-fit: cover; }
272
+ /* A MARK FILLS ITS BOX AND LOSES NOTHING DOING IT. This said `cover`, which fills by scaling to the
273
+ larger ratio and cropping the overflow off both edges. Square art renders identically either way —
274
+ which is why it survived on every child page — but pin.games' mark is 700.19 x 767.26, so `cover`
275
+ took 4.8% off its top and 4.8% off its bottom, and those are the blue bracket and the red one.
276
+ Same change, same reason, as .pa .mk in Posts.astro. The feed service carries a fork of this
277
+ stylesheet and had lost the rule entirely; it is `contain` there now too. */
278
+ .pp .mk { display: block; width: 100%; height: 100%; object-fit: contain; }
274
279
 
275
280
  /* Motion is never forced on somebody who asked it not to be — and the biggest effects, the ones a
276
281
  surface is proudest of, are exactly what that setting exists to suppress. */
package/src/Posts.astro CHANGED
@@ -268,7 +268,19 @@ const vars = [
268
268
  }
269
269
  .rav { flex: none; width: 30px; height: 30px; border-radius: var(--radius-sm); display: grid; place-items: center; font-size: 15px; background: var(--feed-av-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
270
270
  .pa :global(svg), .rav :global(svg) { display: block; }
271
- .pa .mk, .rav .mk { width: 100%; height: 100%; object-fit: cover; display: block; }
271
+ /* queries: the avatar is cropped · blue and red pieces cut off · pin.games mark clipped top and
272
+ bottom · a mark loses its tips in the feed · cover vs contain for a persona icon
273
+ CONTAIN, NOT COVER — THE SLOT IS FORGIVING, THE ASSET NEED NOT BE PERFECT. `cover` fills the box
274
+ by scaling to the LARGER ratio and cropping the overflow, split evenly between the two edges. For
275
+ a square drawing in a square slot the two are the same declaration, which is why this stood for
276
+ twenty-six personas without a complaint: every fleet icon.svg is square. pin.games' mark is not —
277
+ its box is 700.19 x 767.26, so `cover` scaled it by width, overflowed the height by 9.6%, and took
278
+ 4.8% off the top and 4.8% off the bottom. What lives at the top and bottom of that drawing is the
279
+ blue bracket and the red one, which is exactly the pair the founder reported as cut. On a 76px
280
+ profile avatar that is 3.6px gone from each end.
281
+ Squaring the asset is the other half of this and is being done too, but it is not a substitute:
282
+ an asset can be redrawn wrong tomorrow, while a slot that never crops cannot crop anyone. */
283
+ .pa .mk, .rav .mk { width: 100%; height: 100%; object-fit: contain; display: block; }
272
284
  /* a dark tile must invert in light mode, not stay dark — personas whose avatar ground is a gradient
273
285
  pass avatarBgLight; the rest fall back to their single background. */
274
286
  @media (prefers-color-scheme: light) { :global(html:not([data-theme="dark"])) .pa, :global(html:not([data-theme="dark"])) .rav { background: var(--feed-av-bg-light, var(--feed-av-bg, #fff)); } }
package/src/Profile.astro CHANGED
@@ -127,7 +127,10 @@ const vars = [
127
127
  @media (prefers-reduced-motion: reduce) { .avatar { transition: none; } }
128
128
  /* the avatar content is slotted (persona-owned) → style it globally */
129
129
  .avatar :global(svg) { width: 100%; height: 100%; display: block; }
130
- .avatar :global(img) { width: 100%; height: 100%; display: block; object-fit: cover; }
130
+ /* contain, not cover — see the note on .pa .mk in Posts.astro. A square icon renders identically
131
+ under either; a mark whose box is taller than wide loses its top and its bottom under `cover`,
132
+ which is what cropped pin.games' blue and red brackets on this very avatar. */
133
+ .avatar :global(img) { width: 100%; height: 100%; display: block; object-fit: contain; }
131
134
  @media (prefers-color-scheme: light) {
132
135
  :global(html:not([data-theme="dark"])) .avatar { border-color: #fff; background: var(--avatar-bg-light, var(--avatar-bg, #fff)); }
133
136
  }
@@ -144,6 +144,33 @@
144
144
  // So there is no fallback now. If we were not told where a persona lives, its name is rendered
145
145
  // WITHOUT a link: a name that goes nowhere is a small disappointment, and a name that goes
146
146
  // somewhere wrong is a broken ecosystem seen by a stranger.
147
+ // queries: apex logo has a tile in the feed · app.zone tiled on justai's page · which personas sign
148
+ // bare · is this persona an apex · favicon.svg vs icon.svg for a commenter
149
+ //
150
+ // AN APEX SIGNS BARE; EVERYONE ELSE WEARS THEIR TILE. A persona signing its OWN posts already gets
151
+ // this through `avatarSrc`/`avatarBare`, which is how each apex hands itself the bare pair. Another
152
+ // persona replying did not: it was drawn from the registry's `avatar` — /icon.svg, the tile — so on
153
+ // an apex's own page the two sibling apexes appeared in tiles among that page's children, which is
154
+ // exactly the sibling claim the bare mark exists to deny (app-zone/.justai/expert/how-an-app-zone-
155
+ // icon-is-lit.md: the platform mark is inset where app marks are raised).
156
+ //
157
+ // APEX-NESS IS READ, NEVER DECLARED. A persona is an apex when its home is a BARE registrable domain
158
+ // — app.zone, pin.games, justai.pro — and a child's home always carries a label in front of it:
159
+ // qr.app.zone, sudoku.pin.games. One dot versus two. The same test the icon builder applies
160
+ // (builder/icon/build.py, apex_handles) and the feed service applies (feed Feed.astro, isApex), on
161
+ // purpose: three places may not disagree about who is an apex. Read from `home` and not from
162
+ // `personaKind`, because `platform` answers a different question — auth is a platform persona and is
163
+ // not an apex.
164
+ //
165
+ // /favicon.svg and /mark.svg are the platform standard, generated from the same /icon.svg by
166
+ // @justai/astro-config/mark, so an apex has this pair by construction and no name is hardcoded here.
167
+ // The registered avatar stays /icon.svg for everyone: the tile IS right in the catalogue, and only a
168
+ // feed asks an apex for its bare mark.
169
+ const apexHome = (url: string | null | undefined): string | null => {
170
+ if (!url) return null;
171
+ const h = String(url).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
172
+ return (h.match(/\./g) || []).length === 1 ? h : null;
173
+ };
147
174
  const comment = (c: C, last: boolean): string => {
148
175
  const ln = last ? "" : `<span class="ln"></span>`;
149
176
  if (c.isPersona) {
@@ -151,11 +178,15 @@
151
178
  // face and name; ANOTHER persona wears ITS canonical icon (glyph fallback), its own name, its
152
179
  // @handle, and its name links to its own profile. The verified check stays for both.
153
180
  const own = c.authorHandle === PERSONA;
181
+ // A replying apex signs bare from ITS OWN home, in the scheme pair, exactly as it does on its page.
182
+ const apex = own ? null : apexHome(c.personaUrl);
154
183
  const av = own
155
184
  ? AV
156
- : c.authorAvatar
157
- ? `<img class="mk" src="${esc(c.authorAvatar)}" alt="">`
158
- : `<span style="font-size:16px">${esc(c.authorGlyph || "✦")}</span>`;
185
+ : apex
186
+ ? `<img class="mk bare av-light" src="https://${esc(apex)}/favicon.svg" alt=""><img class="mk bare av-dark" src="https://${esc(apex)}/mark.svg" alt="">`
187
+ : c.authorAvatar
188
+ ? `<img class="mk" src="${esc(c.authorAvatar)}" alt="">`
189
+ : `<span style="font-size:16px">${esc(c.authorGlyph || "✦")}</span>`;
159
190
  const nm = own ? NAME : esc(c.authorName); // own name is authored markup; an API name is escaped
160
191
  const purl = own ? "#profile" : c.personaUrl ? `${c.personaUrl}/#profile` : null;
161
192
  const at = own ? PERSONA : c.authorHandle;