@justai/cuts 0.131.1 → 0.133.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/Posts.astro +144 -7
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.133.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/Posts.astro
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { tagline } from "./tagline.js";
|
|
2
|
+
import { tagline, TAGLINE } from "./tagline.js";
|
|
3
3
|
// ░░ POSTS cut — a persona's feed, revealed by scrolling past the Kernel. ░░
|
|
4
4
|
//
|
|
5
5
|
// The feed's markup, its styling, AND its runtime loader were byte-near-identical across every persona
|
|
@@ -85,7 +85,50 @@ 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
|
+
//
|
|
106
|
+
// ── `Astro.currentLocale` IS UNDEFINED HERE, ON EVERY PERSONA IN THIS FLEET ─────────────────────
|
|
107
|
+
//
|
|
108
|
+
// It was written as the fallback and it has never once fired. The fleet routes languages through a
|
|
109
|
+
// `[lang]` PARAMETER, not Astro's i18n config, so `currentLocale` is undefined in every persona that
|
|
110
|
+
// has one — `Sheet.astro` says exactly this, in this same package, and the two comments contradicted
|
|
111
|
+
// each other with the wrong one load-bearing.
|
|
112
|
+
//
|
|
113
|
+
// MEASURED 2026-08-21, and it is not theoretical: `time.app.zone/tr/` and `tabata.app.zone/tr/` were
|
|
114
|
+
// closing their feeds with *"made to matter"* — the ENGLISH house line, on a Turkish page — because
|
|
115
|
+
// neither passes `lang`. Fifteen of the twenty-five personas that render this cut are in that state.
|
|
116
|
+
//
|
|
117
|
+
// So there are two answers, one per surface, and neither is "ask fifteen repos to remember":
|
|
118
|
+
// the footer → rendered by the server, so it is RE-LABELLED at runtime from `<html lang>`, which
|
|
119
|
+
// is the pattern this package already uses for the words a cut owns
|
|
120
|
+
// the feed → fetched by the loader, which runs at runtime and can simply read `<html lang>`
|
|
121
|
+
// `lang` still wins wherever a persona states it: a stated fact beats an inferred one.
|
|
122
|
+
const feedLang = lang ?? "en";
|
|
123
|
+
const foot = footer ?? tagline(feedLang);
|
|
124
|
+
// Only a STATED language rides on the section. An inferred "en" here would look identical to a
|
|
125
|
+
// persona that really is English, and the loader could no longer tell the difference.
|
|
126
|
+
const statedLang = lang ?? undefined;
|
|
127
|
+
// The house line is re-labelled at runtime ONLY when the persona did not state its language and did
|
|
128
|
+
// not override the sentence. A consumer's word always wins — the same rule `data-chrome` keeps.
|
|
129
|
+
const RELABEL = !lang && !footer
|
|
130
|
+
? `(function(){try{var T=${JSON.stringify(TAGLINE)},l=(document.documentElement.lang||"en").toLowerCase().split("-")[0],e=document.querySelector("[data-house-line]");if(e&&T[l])e.textContent=T[l];}catch(_){}})();`
|
|
131
|
+
: "";
|
|
89
132
|
const vars = [
|
|
90
133
|
avatarBg && `--feed-av-bg:${avatarBg}`,
|
|
91
134
|
avatarBgLight && `--feed-av-bg-light:${avatarBgLight}`,
|
|
@@ -97,6 +140,7 @@ const vars = [
|
|
|
97
140
|
id="posts"
|
|
98
141
|
data-persona={handle}
|
|
99
142
|
data-name={name}
|
|
143
|
+
data-lang={statedLang}
|
|
100
144
|
data-av={avatarSrc ?? "/icon.svg"}
|
|
101
145
|
data-av-dark={avatarSrcDark}
|
|
102
146
|
data-av-bare={avatarBare ? "" : undefined}
|
|
@@ -105,7 +149,7 @@ const vars = [
|
|
|
105
149
|
<!-- filled at runtime by the loader below (lazy, on Posts-cut intersection) — single source: the console -->
|
|
106
150
|
<div id="feed"></div>
|
|
107
151
|
<footer class="z-foot">
|
|
108
|
-
<a href="https://justai.pro">justai.pro</a> · {foot}
|
|
152
|
+
<a href="https://justai.pro">justai.pro</a> · <span data-house-line={!lang && !footer ? "" : undefined}>{foot}</span>
|
|
109
153
|
{/* the change-your-mind door: hidden until the shell's consent script labels + reveals it
|
|
110
154
|
(Base owns the behavior and the localized label; this cut only owns the placement). On a
|
|
111
155
|
page with no analytics — and so no consent banner — it simply never appears. */}
|
|
@@ -113,6 +157,8 @@ const vars = [
|
|
|
113
157
|
</footer>
|
|
114
158
|
</section>
|
|
115
159
|
|
|
160
|
+
{RELABEL && <script is:inline set:html={RELABEL}></script>}
|
|
161
|
+
|
|
116
162
|
<style>
|
|
117
163
|
/* ---- POSTS cut — a conversation: ONE line descends from the post's avatar and
|
|
118
164
|
connects every comment & reply (they're all replies to the post). ---- */
|
|
@@ -271,6 +317,12 @@ const vars = [
|
|
|
271
317
|
a.pt { text-decoration: none; }
|
|
272
318
|
a.pt:hover { text-decoration: underline; }
|
|
273
319
|
.pbd { font-size: var(--fs-sm); line-height: 1.5; margin-block-start: 4px; }
|
|
320
|
+
/* The translation notice — dim, small, directly under the words it is about, and carrying the ONE
|
|
321
|
+
link back to what the persona actually wrote, so the link is the only lit thing in the row.
|
|
322
|
+
Matches the feed permalink page exactly: the two surfaces render one conversation. */
|
|
323
|
+
.tnote { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; margin-block-start: 6px; font-size: var(--fs-2xs); color: var(--text-dim); }
|
|
324
|
+
.tnote a { color: var(--confirm-accent, var(--accent)); text-decoration: none; }
|
|
325
|
+
.tnote a:hover { text-decoration: underline; }
|
|
274
326
|
.pact { display: flex; gap: 22px; margin-block-start: 12px; color: var(--text-dim); }
|
|
275
327
|
.pact svg { width: 17px; height: 17px; }
|
|
276
328
|
.pact span { cursor: pointer; transition: color 0.2s; }
|
|
@@ -308,13 +360,37 @@ const vars = [
|
|
|
308
360
|
// so it may carry markup (justai's ".pro" span). Every name that comes from the API is escaped.
|
|
309
361
|
const PERSONA = section.dataset.persona || "";
|
|
310
362
|
const NAME = section.dataset.name || "";
|
|
363
|
+
// THE PAGE'S LANGUAGE. `data-lang` is present only when the persona STATED it; otherwise the
|
|
364
|
+
// document's own `lang` is read, which is the one place every persona in this fleet agrees —
|
|
365
|
+
// `Astro.currentLocale` is undefined under `[lang]` routing and cannot be the fallback it looks
|
|
366
|
+
// like. Not localStorage either: on a localized home the language is in the URL, settled before
|
|
367
|
+
// this ran, and a link shared in Turkish must open in Turkish for whoever follows it.
|
|
368
|
+
const LANG = (section.dataset.lang || document.documentElement.lang || "en").toLowerCase().split("-")[0];
|
|
311
369
|
if (!PERSONA) return;
|
|
312
370
|
// Astro scopes this component's CSS by a data-astro-cid-* attribute; runtime nodes lack it, so copy it.
|
|
313
371
|
const scopeAttr = section.getAttributeNames().find((n) => n.startsWith("data-astro-cid-")) || null;
|
|
314
372
|
|
|
315
373
|
// Twitter-style: clicking anywhere on a post (except a real link) opens its feed page.
|
|
374
|
+
// The "show original" link is handled first and swallowed — it is a control, not a destination.
|
|
316
375
|
feed.addEventListener("click", (e) => {
|
|
317
376
|
const t = e.target as HTMLElement;
|
|
377
|
+
const tog = t.closest("[data-toggle-original]") as HTMLElement | null;
|
|
378
|
+
if (tog) {
|
|
379
|
+
const art = tog.closest(".post");
|
|
380
|
+
const body = art?.querySelector(".pbd") as HTMLElement | null;
|
|
381
|
+
const state = art?.querySelector("[data-tstate]") as HTMLElement | null;
|
|
382
|
+
// Only take over the click once everything it needs is in hand. If the markup ever changes
|
|
383
|
+
// shape the link stays a link and still reaches the original — a control that degrades to
|
|
384
|
+
// working beats one that silently does nothing.
|
|
385
|
+
if (!body || !state) return;
|
|
386
|
+
e.preventDefault();
|
|
387
|
+
const showing = tog.textContent === tog.dataset.showTranslation;
|
|
388
|
+
body.textContent = showing ? tog.dataset.translation! : tog.dataset.original!;
|
|
389
|
+
body.setAttribute("lang", (showing ? tog.dataset.lang : tog.dataset.originalLang) || "");
|
|
390
|
+
tog.textContent = showing ? tog.dataset.showOriginal! : tog.dataset.showTranslation!;
|
|
391
|
+
state.textContent = showing ? tog.dataset.translated! : tog.dataset.originalLabel!;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
318
394
|
if (t.closest("a")) return;
|
|
319
395
|
const art = t.closest(".post[data-href]") as HTMLElement | null;
|
|
320
396
|
if (art?.dataset.href) location.assign(art.dataset.href);
|
|
@@ -339,7 +415,40 @@ const vars = [
|
|
|
339
415
|
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
416
|
|
|
341
417
|
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
|
-
|
|
418
|
+
// `lang` is what `body` IS; `translated` says a machine made it. Both are optional so this cut
|
|
419
|
+
// still renders against a console that predates them — the fleet installs its packages on its own
|
|
420
|
+
// schedule, and a persona that has not been bumped must not lose its feed.
|
|
421
|
+
type P = { id: string; body: string; lang?: string; originalLang?: string; originalBody?: string | null; translated?: boolean; createdAt: string | null; comments: C[] };
|
|
422
|
+
|
|
423
|
+
// queries: translated automatically · show original · machine translation notice on the home feed
|
|
424
|
+
//
|
|
425
|
+
// THE SAME LINE THE PERMALINK PAGE SHOWS, because they render one conversation and must not
|
|
426
|
+
// disagree about it. The original is canonical, marked, and one tap away — that pairing IS the
|
|
427
|
+
// doctrine (dna/entities/localization.md), not a courtesy.
|
|
428
|
+
//
|
|
429
|
+
// Thirteen strings rather than a lookup through the persona's own dictionary: this cut ships to
|
|
430
|
+
// every persona and cannot reach into any of their locale files. ~400 bytes, said once, for the
|
|
431
|
+
// whole fleet — the alternative is the same sentence copied into thirteen files times thirty
|
|
432
|
+
// repos, which is the shape `tagline.js` already exists to prevent.
|
|
433
|
+
const TRANSLATED: Record<string, string> = {
|
|
434
|
+
en: "Translated automatically", tr: "Otomatik olarak çevrildi", es: "Traducido automáticamente",
|
|
435
|
+
de: "Automatisch übersetzt", fr: "Traduit automatiquement", pt: "Traduzido automaticamente",
|
|
436
|
+
it: "Tradotto automaticamente", nl: "Automatisch vertaald", ru: "Переведено автоматически",
|
|
437
|
+
ja: "自動翻訳", zh: "自动翻译", ar: "تُرجم آليًا", hi: "स्वचालित रूप से अनूदित",
|
|
438
|
+
};
|
|
439
|
+
const SHOW_ORIGINAL: Record<string, string> = {
|
|
440
|
+
en: "Show original", tr: "Orijinali göster", es: "Ver original", de: "Original anzeigen",
|
|
441
|
+
fr: "Afficher l'original", pt: "Ver original", it: "Mostra originale", nl: "Origineel weergeven",
|
|
442
|
+
ru: "Показать оригинал", ja: "原文を表示", zh: "显示原文", ar: "عرض النص الأصلي", hi: "मूल दिखाएँ",
|
|
443
|
+
};
|
|
444
|
+
// The way BACK. It is a toggle, not a door: swapping in place keeps the reader on the feed they
|
|
445
|
+
// were reading, in the language they set — the alternative walks them to the permalink and hands
|
|
446
|
+
// them the whole page in English for the sake of one paragraph.
|
|
447
|
+
const SHOW_TRANSLATION: Record<string, string> = {
|
|
448
|
+
en: "Show translation", tr: "Çeviriyi göster", es: "Ver traducción", de: "Übersetzung anzeigen",
|
|
449
|
+
fr: "Afficher la traduction", pt: "Ver tradução", it: "Mostra traduzione", nl: "Vertaling weergeven",
|
|
450
|
+
ru: "Показать перевод", ja: "翻訳を表示", zh: "显示翻译", ar: "عرض الترجمة", hi: "अनुवाद दिखाएँ",
|
|
451
|
+
};
|
|
343
452
|
|
|
344
453
|
// Relative timestamps are DERIVED from each post/comment's real createdAt (never a stored string),
|
|
345
454
|
// localized to the visitor's language and recomputed at read time — so they age honestly.
|
|
@@ -405,9 +514,34 @@ const vars = [
|
|
|
405
514
|
// Twitter-style permalink: the timestamp links to the post's feed page, and the whole post is
|
|
406
515
|
// clickable (the delegated handler above). n = the post id's last segment ("post:qr:7" → 7).
|
|
407
516
|
const n = String(p.id || "").split(":").pop() || "";
|
|
408
|
-
|
|
517
|
+
// queries: clicking a post loses my language · the permalink is English · navigation language
|
|
518
|
+
//
|
|
519
|
+
// EVERY NAVIGATION STAYS IN THE READER'S LANGUAGE — the founder's rule, and it is stronger than
|
|
520
|
+
// "link to the translation when there is one". Set a persona to Italian and EVERY page of it is
|
|
521
|
+
// Italian; a post with no Italian gives the original words with Italian chrome around them, and
|
|
522
|
+
// `/feed/{n}/it` renders exactly that rather than 404ing.
|
|
523
|
+
//
|
|
524
|
+
// The one URL that carries no segment is the post read in the language it was written in: there
|
|
525
|
+
// is no "translation into the original", so the permalink is already that page.
|
|
526
|
+
const orig = p.originalLang || "en";
|
|
527
|
+
const tr = !!p.translated && !!p.originalBody;
|
|
528
|
+
const base = n ? `/feed/${n}` : "";
|
|
529
|
+
const perma = base && LANG !== orig ? `${base}/${LANG}` : base;
|
|
409
530
|
const pt = perma ? `<a class="pt" href="${perma}">${timeAgo(p.createdAt)}</a>` : `<span class="pt">${timeAgo(p.createdAt)}</span>`;
|
|
410
|
-
|
|
531
|
+
// The way back, in the language of the person reading it. A real <a> so the delegated
|
|
532
|
+
// click-anywhere handler above lets it through — it returns early on any link.
|
|
533
|
+
// Both texts ride in the markup so the swap is instant and needs no second request. The link
|
|
534
|
+
// keeps a real href — without scripts it still reaches the original's own page.
|
|
535
|
+
const note = tr
|
|
536
|
+
? `<p class="tnote" dir="auto"><span data-tstate>${esc(TRANSLATED[LANG] ?? TRANSLATED.en)}</span><span class="dot">·</span>` +
|
|
537
|
+
`<a href="${base}" data-toggle-original data-original="${esc(p.originalBody)}" data-translation="${esc(p.body)}"` +
|
|
538
|
+
` data-show-original="${esc(SHOW_ORIGINAL[LANG] ?? SHOW_ORIGINAL.en)}" data-show-translation="${esc(SHOW_TRANSLATION[LANG] ?? SHOW_TRANSLATION.en)}"` +
|
|
539
|
+
` data-translated="${esc(TRANSLATED[LANG] ?? TRANSLATED.en)}" data-original-label="${esc(orig.toUpperCase())} · ${esc(NAME).replace(/<[^>]*>/g, "")}"` +
|
|
540
|
+
` data-original-lang="${esc(orig)}" data-lang="${esc(p.lang || LANG)}">${esc(SHOW_ORIGINAL[LANG] ?? SHOW_ORIGINAL.en)}</a></p>`
|
|
541
|
+
: "";
|
|
542
|
+
// `dir="auto"` on the body: an Arabic post inside an LTR page — and the reverse — must lay
|
|
543
|
+
// itself out from its own first strong character, not from the page around it.
|
|
544
|
+
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
545
|
};
|
|
412
546
|
|
|
413
547
|
let done = false;
|
|
@@ -415,7 +549,10 @@ const vars = [
|
|
|
415
549
|
if (done) return;
|
|
416
550
|
done = true;
|
|
417
551
|
try {
|
|
418
|
-
|
|
552
|
+
// `lang` rides in the query because it IS the content here — api.justai.pro is CDN'd with the
|
|
553
|
+
// query string in the cache key, so this is one more entry rather than one more origin hit.
|
|
554
|
+
const res = await fetch(
|
|
555
|
+
"https://api.justai.pro/api/feed?persona=" + encodeURIComponent(PERSONA) + "&lang=" + encodeURIComponent(LANG));
|
|
419
556
|
if (!res.ok) return;
|
|
420
557
|
const data = await res.json();
|
|
421
558
|
feed.innerHTML = ((data.posts as P[]) || []).map(post).join("");
|