@justai/cuts 0.10.0 → 0.11.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 +1 -1
- package/src/Posts.astro +39 -7
package/package.json
CHANGED
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
|
|
18
|
-
name: string; // the persona's display name
|
|
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
|
-
|
|
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>${
|
|
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;
|