@justai/cuts 0.131.0 → 0.132.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 +3 -3
- package/src/Posts.astro +112 -5
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.132.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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@justai/core": "^0.
|
|
44
|
+
"@justai/core": "^0.35.0",
|
|
45
45
|
"@justai/ui": "^0.22.0",
|
|
46
46
|
"@justai/atrium": "^0.1.9"
|
|
47
47
|
}
|
package/src/Posts.astro
CHANGED
|
@@ -85,7 +85,25 @@ const { handle, name, lang, footer, avatarBg, avatarBgLight, avatarSrc, avatarBa
|
|
|
85
85
|
// The override wins where it is given; otherwise the family's own line, in this page's language.
|
|
86
86
|
// `Astro.currentLocale` is the fallback for a caller that passes neither, so an older persona that
|
|
87
87
|
// has not been bumped yet still closes its feed in the right language rather than in English.
|
|
88
|
-
|
|
88
|
+
// queries: posts cut language · English posts on a Turkish page · translated feed on the home page ·
|
|
89
|
+
// why does the feed not follow the page language · show original on a persona's home
|
|
90
|
+
//
|
|
91
|
+
// ── THE PAGE'S LANGUAGE, AND NOW THE FEED'S TOO ────────────────────────────────────────────────
|
|
92
|
+
//
|
|
93
|
+
// One value, two jobs. It already picked the house tagline; it now also asks the console for the feed
|
|
94
|
+
// in that language. They must be the same value or the footer and the posts above it disagree about
|
|
95
|
+
// what page this is.
|
|
96
|
+
//
|
|
97
|
+
// This was a real defect and builder's own finishing checklist names it — *"a post in English on a
|
|
98
|
+
// Turkish page"*. Measured on qr before the fix: `<html lang="tr">`, the bio Turkish, the timestamp
|
|
99
|
+
// reading *"3 hf. önce"*, and every post underneath in English, linking to the English permalink.
|
|
100
|
+
//
|
|
101
|
+
// The PAGE decides, not localStorage. On a localized persona home the language is in the URL
|
|
102
|
+
// (`/tr/`), so it is settled before any script runs — and the same rule the feed permalink pages
|
|
103
|
+
// learned the hard way: a page whose language came from its address must not be second-guessed by
|
|
104
|
+
// what the visitor last picked somewhere else.
|
|
105
|
+
const feedLang = lang ?? Astro.currentLocale ?? "en";
|
|
106
|
+
const foot = footer ?? tagline(feedLang);
|
|
89
107
|
const vars = [
|
|
90
108
|
avatarBg && `--feed-av-bg:${avatarBg}`,
|
|
91
109
|
avatarBgLight && `--feed-av-bg-light:${avatarBgLight}`,
|
|
@@ -97,6 +115,7 @@ const vars = [
|
|
|
97
115
|
id="posts"
|
|
98
116
|
data-persona={handle}
|
|
99
117
|
data-name={name}
|
|
118
|
+
data-lang={feedLang}
|
|
100
119
|
data-av={avatarSrc ?? "/icon.svg"}
|
|
101
120
|
data-av-dark={avatarSrcDark}
|
|
102
121
|
data-av-bare={avatarBare ? "" : undefined}
|
|
@@ -271,6 +290,12 @@ const vars = [
|
|
|
271
290
|
a.pt { text-decoration: none; }
|
|
272
291
|
a.pt:hover { text-decoration: underline; }
|
|
273
292
|
.pbd { font-size: var(--fs-sm); line-height: 1.5; margin-block-start: 4px; }
|
|
293
|
+
/* The translation notice — dim, small, directly under the words it is about, and carrying the ONE
|
|
294
|
+
link back to what the persona actually wrote, so the link is the only lit thing in the row.
|
|
295
|
+
Matches the feed permalink page exactly: the two surfaces render one conversation. */
|
|
296
|
+
.tnote { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; margin-block-start: 6px; font-size: var(--fs-2xs); color: var(--text-dim); }
|
|
297
|
+
.tnote a { color: var(--confirm-accent, var(--accent)); text-decoration: none; }
|
|
298
|
+
.tnote a:hover { text-decoration: underline; }
|
|
274
299
|
.pact { display: flex; gap: 22px; margin-block-start: 12px; color: var(--text-dim); }
|
|
275
300
|
.pact svg { width: 17px; height: 17px; }
|
|
276
301
|
.pact span { cursor: pointer; transition: color 0.2s; }
|
|
@@ -308,13 +333,34 @@ const vars = [
|
|
|
308
333
|
// so it may carry markup (justai's ".pro" span). Every name that comes from the API is escaped.
|
|
309
334
|
const PERSONA = section.dataset.persona || "";
|
|
310
335
|
const NAME = section.dataset.name || "";
|
|
336
|
+
// THE PAGE'S LANGUAGE, set by the cut above from the same value that picked the footer. Not
|
|
337
|
+
// localStorage: on a localized home the language is in the URL and was settled before this ran.
|
|
338
|
+
const LANG = section.dataset.lang || "en";
|
|
311
339
|
if (!PERSONA) return;
|
|
312
340
|
// Astro scopes this component's CSS by a data-astro-cid-* attribute; runtime nodes lack it, so copy it.
|
|
313
341
|
const scopeAttr = section.getAttributeNames().find((n) => n.startsWith("data-astro-cid-")) || null;
|
|
314
342
|
|
|
315
343
|
// Twitter-style: clicking anywhere on a post (except a real link) opens its feed page.
|
|
344
|
+
// The "show original" link is handled first and swallowed — it is a control, not a destination.
|
|
316
345
|
feed.addEventListener("click", (e) => {
|
|
317
346
|
const t = e.target as HTMLElement;
|
|
347
|
+
const tog = t.closest("[data-toggle-original]") as HTMLElement | null;
|
|
348
|
+
if (tog) {
|
|
349
|
+
const art = tog.closest(".post");
|
|
350
|
+
const body = art?.querySelector(".pbd") as HTMLElement | null;
|
|
351
|
+
const state = art?.querySelector("[data-tstate]") as HTMLElement | null;
|
|
352
|
+
// Only take over the click once everything it needs is in hand. If the markup ever changes
|
|
353
|
+
// shape the link stays a link and still reaches the original — a control that degrades to
|
|
354
|
+
// working beats one that silently does nothing.
|
|
355
|
+
if (!body || !state) return;
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
const showing = tog.textContent === tog.dataset.showTranslation;
|
|
358
|
+
body.textContent = showing ? tog.dataset.translation! : tog.dataset.original!;
|
|
359
|
+
body.setAttribute("lang", (showing ? tog.dataset.lang : tog.dataset.originalLang) || "");
|
|
360
|
+
tog.textContent = showing ? tog.dataset.showOriginal! : tog.dataset.showTranslation!;
|
|
361
|
+
state.textContent = showing ? tog.dataset.translated! : tog.dataset.originalLabel!;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
318
364
|
if (t.closest("a")) return;
|
|
319
365
|
const art = t.closest(".post[data-href]") as HTMLElement | null;
|
|
320
366
|
if (art?.dataset.href) location.assign(art.dataset.href);
|
|
@@ -339,7 +385,40 @@ const vars = [
|
|
|
339
385
|
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>`;
|
|
340
386
|
|
|
341
387
|
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 };
|
|
342
|
-
|
|
388
|
+
// `lang` is what `body` IS; `translated` says a machine made it. Both are optional so this cut
|
|
389
|
+
// still renders against a console that predates them — the fleet installs its packages on its own
|
|
390
|
+
// schedule, and a persona that has not been bumped must not lose its feed.
|
|
391
|
+
type P = { id: string; body: string; lang?: string; originalLang?: string; originalBody?: string | null; translated?: boolean; createdAt: string | null; comments: C[] };
|
|
392
|
+
|
|
393
|
+
// queries: translated automatically · show original · machine translation notice on the home feed
|
|
394
|
+
//
|
|
395
|
+
// THE SAME LINE THE PERMALINK PAGE SHOWS, because they render one conversation and must not
|
|
396
|
+
// disagree about it. The original is canonical, marked, and one tap away — that pairing IS the
|
|
397
|
+
// doctrine (dna/entities/localization.md), not a courtesy.
|
|
398
|
+
//
|
|
399
|
+
// Thirteen strings rather than a lookup through the persona's own dictionary: this cut ships to
|
|
400
|
+
// every persona and cannot reach into any of their locale files. ~400 bytes, said once, for the
|
|
401
|
+
// whole fleet — the alternative is the same sentence copied into thirteen files times thirty
|
|
402
|
+
// repos, which is the shape `tagline.js` already exists to prevent.
|
|
403
|
+
const TRANSLATED: Record<string, string> = {
|
|
404
|
+
en: "Translated automatically", tr: "Otomatik olarak çevrildi", es: "Traducido automáticamente",
|
|
405
|
+
de: "Automatisch übersetzt", fr: "Traduit automatiquement", pt: "Traduzido automaticamente",
|
|
406
|
+
it: "Tradotto automaticamente", nl: "Automatisch vertaald", ru: "Переведено автоматически",
|
|
407
|
+
ja: "自動翻訳", zh: "自动翻译", ar: "تُرجم آليًا", hi: "स्वचालित रूप से अनूदित",
|
|
408
|
+
};
|
|
409
|
+
const SHOW_ORIGINAL: Record<string, string> = {
|
|
410
|
+
en: "Show original", tr: "Orijinali göster", es: "Ver original", de: "Original anzeigen",
|
|
411
|
+
fr: "Afficher l'original", pt: "Ver original", it: "Mostra originale", nl: "Origineel weergeven",
|
|
412
|
+
ru: "Показать оригинал", ja: "原文を表示", zh: "显示原文", ar: "عرض النص الأصلي", hi: "मूल दिखाएँ",
|
|
413
|
+
};
|
|
414
|
+
// The way BACK. It is a toggle, not a door: swapping in place keeps the reader on the feed they
|
|
415
|
+
// were reading, in the language they set — the alternative walks them to the permalink and hands
|
|
416
|
+
// them the whole page in English for the sake of one paragraph.
|
|
417
|
+
const SHOW_TRANSLATION: Record<string, string> = {
|
|
418
|
+
en: "Show translation", tr: "Çeviriyi göster", es: "Ver traducción", de: "Übersetzung anzeigen",
|
|
419
|
+
fr: "Afficher la traduction", pt: "Ver tradução", it: "Mostra traduzione", nl: "Vertaling weergeven",
|
|
420
|
+
ru: "Показать перевод", ja: "翻訳を表示", zh: "显示翻译", ar: "عرض الترجمة", hi: "अनुवाद दिखाएँ",
|
|
421
|
+
};
|
|
343
422
|
|
|
344
423
|
// Relative timestamps are DERIVED from each post/comment's real createdAt (never a stored string),
|
|
345
424
|
// localized to the visitor's language and recomputed at read time — so they age honestly.
|
|
@@ -405,9 +484,34 @@ const vars = [
|
|
|
405
484
|
// Twitter-style permalink: the timestamp links to the post's feed page, and the whole post is
|
|
406
485
|
// clickable (the delegated handler above). n = the post id's last segment ("post:qr:7" → 7).
|
|
407
486
|
const n = String(p.id || "").split(":").pop() || "";
|
|
408
|
-
|
|
487
|
+
// queries: clicking a post loses my language · the permalink is English · navigation language
|
|
488
|
+
//
|
|
489
|
+
// EVERY NAVIGATION STAYS IN THE READER'S LANGUAGE — the founder's rule, and it is stronger than
|
|
490
|
+
// "link to the translation when there is one". Set a persona to Italian and EVERY page of it is
|
|
491
|
+
// Italian; a post with no Italian gives the original words with Italian chrome around them, and
|
|
492
|
+
// `/feed/{n}/it` renders exactly that rather than 404ing.
|
|
493
|
+
//
|
|
494
|
+
// The one URL that carries no segment is the post read in the language it was written in: there
|
|
495
|
+
// is no "translation into the original", so the permalink is already that page.
|
|
496
|
+
const orig = p.originalLang || "en";
|
|
497
|
+
const tr = !!p.translated && !!p.originalBody;
|
|
498
|
+
const base = n ? `/feed/${n}` : "";
|
|
499
|
+
const perma = base && LANG !== orig ? `${base}/${LANG}` : base;
|
|
409
500
|
const pt = perma ? `<a class="pt" href="${perma}">${timeAgo(p.createdAt)}</a>` : `<span class="pt">${timeAgo(p.createdAt)}</span>`;
|
|
410
|
-
|
|
501
|
+
// The way back, in the language of the person reading it. A real <a> so the delegated
|
|
502
|
+
// click-anywhere handler above lets it through — it returns early on any link.
|
|
503
|
+
// Both texts ride in the markup so the swap is instant and needs no second request. The link
|
|
504
|
+
// keeps a real href — without scripts it still reaches the original's own page.
|
|
505
|
+
const note = tr
|
|
506
|
+
? `<p class="tnote" dir="auto"><span data-tstate>${esc(TRANSLATED[LANG] ?? TRANSLATED.en)}</span><span class="dot">·</span>` +
|
|
507
|
+
`<a href="${base}" data-toggle-original data-original="${esc(p.originalBody)}" data-translation="${esc(p.body)}"` +
|
|
508
|
+
` data-show-original="${esc(SHOW_ORIGINAL[LANG] ?? SHOW_ORIGINAL.en)}" data-show-translation="${esc(SHOW_TRANSLATION[LANG] ?? SHOW_TRANSLATION.en)}"` +
|
|
509
|
+
` data-translated="${esc(TRANSLATED[LANG] ?? TRANSLATED.en)}" data-original-label="${esc(orig.toUpperCase())} · ${esc(NAME).replace(/<[^>]*>/g, "")}"` +
|
|
510
|
+
` data-original-lang="${esc(orig)}" data-lang="${esc(p.lang || LANG)}">${esc(SHOW_ORIGINAL[LANG] ?? SHOW_ORIGINAL.en)}</a></p>`
|
|
511
|
+
: "";
|
|
512
|
+
// `dir="auto"` on the body: an Arabic post inside an LTR page — and the reverse — must lay
|
|
513
|
+
// itself out from its own first strong character, not from the page around it.
|
|
514
|
+
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>${ck}${PERSONA ? `<span class="phandle">@${esc(PERSONA)}</span>` : ""}<span class="dot">·</span>${pt}</div><p class="pbd" dir="auto"${tr ? ` lang="${esc(p.lang)}"` : ""}>${esc(p.body)}</p>${note}${pact}</div></div>${inner}</article>`;
|
|
411
515
|
};
|
|
412
516
|
|
|
413
517
|
let done = false;
|
|
@@ -415,7 +519,10 @@ const vars = [
|
|
|
415
519
|
if (done) return;
|
|
416
520
|
done = true;
|
|
417
521
|
try {
|
|
418
|
-
|
|
522
|
+
// `lang` rides in the query because it IS the content here — api.justai.pro is CDN'd with the
|
|
523
|
+
// query string in the cache key, so this is one more entry rather than one more origin hit.
|
|
524
|
+
const res = await fetch(
|
|
525
|
+
"https://api.justai.pro/api/feed?persona=" + encodeURIComponent(PERSONA) + "&lang=" + encodeURIComponent(LANG));
|
|
419
526
|
if (!res.ok) return;
|
|
420
527
|
const data = await res.json();
|
|
421
528
|
feed.innerHTML = ((data.posts as P[]) || []).map(post).join("");
|