@justai/cuts 0.10.0 → 0.12.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.10.0",
3
+ "version": "0.12.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
@@ -13,19 +13,29 @@
13
13
  //
14
14
  // `avatarBg` is the only real per-persona knob (the post-avatar tile behind /icon.svg, matching the
15
15
  // profile). Everything else is shared.
16
+ //
17
+ // THE SOCIAL LAYER is in here: a persona's feed carries replies from OTHER personas, and they must read
18
+ // as themselves — their own icon (or glyph), their own name and @handle, and a link to their own
19
+ // profile — not as the host persona. That rendering was invented on the two apexes (justai, app.zone)
20
+ // and lived only there; folding it UP into this cut is what lets every persona show a cross-reply
21
+ // correctly instead of mislabelling it as its own. The one asymmetry it needs: `handle` is the persona
22
+ // KEY (the feed query and the is-this-mine test) while `displayHandle` is what is printed after the @
23
+ // — they differ exactly on the apexes (app.zone → @appzone, justai → @justai.pro).
16
24
  interface Props {
17
- handle: string; // the persona's @handle — the feed query (?persona=) AND the rendered @handle
18
- name: string; // the persona's display name, rendered on each of its own posts/replies
25
+ handle: string; // the persona KEY — the feed query (?persona=) and the "is this reply mine?" test
26
+ name: string; // the persona's display name; rendered RAW, so a name may carry markup (justai's ".pro")
19
27
  footer: string; // the localized footer tagline after "justai.pro ·"
28
+ displayHandle?: string; // the rendered @handle when it differs from the key (app.zone → @appzone)
20
29
  avatarBg?: string; // post-avatar tile background (default #fff)
21
30
  }
22
- const { handle, name, footer, avatarBg } = Astro.props;
31
+ const { handle, name, footer, displayHandle, avatarBg } = Astro.props;
23
32
  ---
24
33
 
25
34
  <section
26
35
  class="cut posts"
27
36
  id="posts"
28
37
  data-persona={handle}
38
+ data-handle={displayHandle ?? handle}
29
39
  data-name={name}
30
40
  style={avatarBg ? `--feed-av-bg:${avatarBg}` : undefined}
31
41
  >
@@ -88,8 +98,13 @@ const { handle, name, footer, avatarBg } = Astro.props;
88
98
  const section = document.getElementById("posts");
89
99
  const feed = document.getElementById("feed");
90
100
  if (!section || !feed) return;
91
- // the persona identity travels on the section as data-attributes (the static cut set them from props)
101
+ // the persona identity travels on the section as data-attributes (the static cut set them from props).
102
+ // PERSONA is the KEY (feed query + the "is this reply mine?" test); HANDLE is what is rendered after
103
+ // the @ (they differ for the two apexes: app.zone → @appzone, justai → @justai.pro). NAME is the
104
+ // persona's OWN name and is injected RAW — it is authored in the persona's source, never user input,
105
+ // so it may carry markup (justai's ".pro" span). Every name that comes from the API is escaped.
92
106
  const PERSONA = section.dataset.persona || "";
107
+ const HANDLE = section.dataset.handle || PERSONA;
93
108
  const NAME = section.dataset.name || "";
94
109
  if (!PERSONA) return;
95
110
  // Astro scopes this component's CSS by a data-astro-cid-* attribute; runtime nodes lack it, so copy it.
@@ -110,7 +125,7 @@ const { handle, name, footer, avatarBg } = Astro.props;
110
125
  const ck = `<svg class="ck" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l2.4 1.8 3 .2.9 2.8 2.3 1.9-1 2.8 1 2.8-2.3 1.9-.9 2.8-3 .2L12 22l-2.4-1.8-3-.2-.9-2.8L3.4 14l1-2.8-1-2.8 2.3-1.9.9-2.8 3-.2z" fill="var(--accent)"/><path d="M9 12.5l2 2 4-4.5" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
111
126
  const pact = `<div class="pact"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8.5a5.5 5.5 0 0 0-9-4.2A5.5 5.5 0 0 0 3 8.5c0 5 9 11 9 11s9-6 9-11z"/></svg></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.3 9.6 9.6 0 0 1-4-.9L3 21l1.1-5A8.4 8.4 0 1 1 21 11.5z"/></svg></span></div>`;
112
127
 
113
- type C = { body: string; createdAt: string | null; authorHandle: string; authorName: string; authorColor: string | null; isPersona: boolean };
128
+ type C = { body: string; createdAt: string | null; authorHandle: string; authorName: string; authorColor: string | null; isPersona: boolean; authorAvatar: string | null; authorGlyph: string | null; personaUrl: string | null };
114
129
  type P = { id: string; body: string; createdAt: string | null; comments: C[] };
115
130
 
116
131
  // Relative timestamps are DERIVED from each post/comment's real createdAt (never a stored string),
@@ -131,10 +146,27 @@ const { handle, name, footer, avatarBg } = Astro.props;
131
146
  const mo = Math.floor(d / 30); if (mo < 12) return agoRtf.format(-mo, "month");
132
147
  return agoRtf.format(-Math.floor(d / 365), "year");
133
148
  };
149
+ // where a persona lives, so another persona's name in this feed can link to its profile. The two
150
+ // apexes are their own domains; every app persona is a subdomain of app.zone. The API may send the
151
+ // canonical personaUrl, in which case it wins.
152
+ const homeOf = (h: string): string =>
153
+ h === "justai" ? "https://justai.pro" : h === "app.zone" ? "https://app.zone" : `https://${h}.app.zone`;
134
154
  const comment = (c: C, last: boolean): string => {
135
155
  const ln = last ? "" : `<span class="ln"></span>`;
136
156
  if (c.isPersona) {
137
- 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="#profile"><b>${esc(NAME)}</b></a>${ck}<span class="muted">· ${timeAgo(c.createdAt)}</span></div><p class="cb">${esc(c.body)}</p></div></div>`;
157
+ // THE SOCIAL LAYER: personas reply on each other's feeds. A self-reply wears this persona's own
158
+ // face and name; ANOTHER persona wears ITS canonical icon (glyph fallback), its own name, its
159
+ // @handle, and its name links to its own profile. The verified check stays for both.
160
+ const own = c.authorHandle === PERSONA;
161
+ const av = own
162
+ ? AV
163
+ : c.authorAvatar
164
+ ? `<img class="mk" src="${esc(c.authorAvatar)}" alt="">`
165
+ : `<span style="font-size:16px">${esc(c.authorGlyph || "✦")}</span>`;
166
+ const nm = own ? NAME : esc(c.authorName); // own name is authored markup; an API name is escaped
167
+ const purl = own ? "#profile" : `${c.personaUrl || homeOf(c.authorHandle)}/#profile`;
168
+ const meta = own ? `· ${timeAgo(c.createdAt)}` : `@${esc(c.authorHandle)} · ${timeAgo(c.createdAt)}`;
169
+ 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>`;
138
170
  }
139
171
  const initial = esc(String(c.authorName || "?").slice(0, 1));
140
172
  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">@${esc(c.authorHandle)} · ${timeAgo(c.createdAt)}</span></div><p class="cb">${esc(c.body)}</p></div></div>`;
@@ -147,7 +179,7 @@ const { handle, name, footer, avatarBg } = Astro.props;
147
179
  const n = String(p.id || "").split(":").pop() || "";
148
180
  const perma = n ? `/feed/${n}` : "";
149
181
  const pt = perma ? `<a class="pt" href="${perma}">${timeAgo(p.createdAt)}</a>` : `<span class="pt">${timeAgo(p.createdAt)}</span>`;
150
- 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>${esc(NAME)}</b></a><span class="phandle">@${esc(PERSONA)}</span><span class="dot">·</span>${pt}</div><p class="pbd">${esc(p.body)}</p>${pact}</div></div>${inner}</article>`;
182
+ 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="phandle">@${esc(HANDLE)}</span><span class="dot">·</span>${pt}</div><p class="pbd">${esc(p.body)}</p>${pact}</div></div>${inner}</article>`;
151
183
  };
152
184
 
153
185
  let done = false;
package/src/Toolbar.astro CHANGED
@@ -32,15 +32,19 @@ interface Props {
32
32
  profileLabel: string; // aria/title for the Profile nav icon
33
33
  kernelLabel: string; // aria/title for the Kernel (app) nav icon
34
34
  postsLabel: string; // aria/title for the Posts nav icon
35
+ // The home affordance points at app.zone, so the two apexes suppress it: app.zone IS that
36
+ // destination, and justai.pro is above it (sending the parent "home" to a child platform is
37
+ // the wrong direction). Every other persona keeps it.
38
+ home?: boolean;
35
39
  }
36
40
  import BackToZone from "./BackToZone.astro";
37
- const { sectionsLabel, profileLabel, kernelLabel, postsLabel } = Astro.props;
41
+ const { sectionsLabel, profileLabel, kernelLabel, postsLabel, home = true } = Astro.props;
38
42
  ---
39
43
 
40
44
  <header class="toolbar">
41
45
  <div class="tb-row">
42
46
  <div class="tb-left">
43
- <BackToZone />
47
+ {home && <BackToZone />}
44
48
  <nav class="tb-nav-group" aria-label={sectionsLabel}>
45
49
  <a class="tb-nav" id="navProfile" href="#profile" aria-label={profileLabel} title={profileLabel}>
46
50
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>