@justai/cuts 0.1.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.
Files changed (2) hide show
  1. package/package.json +12 -0
  2. package/src/Base.astro +203 -0
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@justai/cuts",
3
+ "version": "0.1.0",
4
+ "description": "A persona's named parts — the page shell that holds them, and the cuts themselves.",
5
+ "license": "UNLICENSED",
6
+ "repository": { "type": "git", "url": "git+https://github.com/justai-pro/web.git", "directory": "packages/cuts" },
7
+ "type": "module",
8
+ "files": ["src"],
9
+ "exports": { "./Base.astro": "./src/Base.astro" },
10
+ "peerDependencies": { "astro": ">=5", "@justai/ui": ">=0.2.0" },
11
+ "publishConfig": { "access": "public" }
12
+ }
package/src/Base.astro ADDED
@@ -0,0 +1,203 @@
1
+ ---
2
+ /**
3
+ * <Base> — the persona-page shell.
4
+ *
5
+ * Measured before it was written: across the nine app.zone tool personas this file was 154 lines,
6
+ * of which 132 were IDENTICAL. The 22 that differed were all identity — the host, the theme colour,
7
+ * the favicon, the analytics property — plus two real ones, both preserved here as contract rather
8
+ * than flattened: --on-accent (cleanmylink and howlong have light accents where white text fails
9
+ * contrast) and a head slot (time self-hosts Geist).
10
+ *
11
+ * WHAT IT DELIBERATELY DOES NOT OWN.
12
+ *
13
+ * The locale list. `locales` and `defaultLocale` are PROPS, read from the consuming repo's
14
+ * src/i18n/index.ts, and this package ships neither. A framework that carried its own list would be
15
+ * a second declaration site for the one thing this ecosystem has already been bitten by twice —
16
+ * an inert astro.config block that drifted to twelve while a thirteenth locale shipped, and a
17
+ * literal array in this very script that nothing checked. There is one home for that list and it is
18
+ * not here.
19
+ *
20
+ * The dictionaries. Same reason, stronger: the machinery is shared, the CONTENT is the persona's.
21
+ */
22
+ import { dir as dirOf } from "@justai/ui/locale";
23
+
24
+ interface Props {
25
+ lang: string;
26
+ t: Record<string, string>;
27
+ /** The persona's own origin, e.g. https://tabata.app.zone — canonical, hreflang and OG all read it. */
28
+ site: string;
29
+ /** From the consuming repo's src/i18n — never from this package. */
30
+ locales: readonly string[];
31
+ defaultLocale: string;
32
+ /** A data: URI. The persona's mark; the one asset a shell cannot supply. */
33
+ favicon: string;
34
+ /** Defaults are the fleet majority, measured. Override when the persona genuinely differs. */
35
+ themeDark?: string;
36
+ themeLight?: string;
37
+ /** app.zone's property. justai.pro is its own apex and passes its own. */
38
+ gaId?: string;
39
+ }
40
+
41
+ const {
42
+ lang, t, site, locales, defaultLocale, favicon,
43
+ themeDark = "#000000",
44
+ themeLight = "#f2f2f7",
45
+ gaId = "G-13L3BP6XCW",
46
+ } = Astro.props;
47
+
48
+ const d = dirOf(lang);
49
+ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
50
+ ---
51
+
52
+ <!doctype html>
53
+ <html lang={lang} dir={d}>
54
+ <head>
55
+ <meta charset="utf-8" />
56
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
57
+ <link rel="preconnect" href="https://api.justai.pro" crossorigin />
58
+ <!-- detail: reveal only after we've positioned at the Kernel — no scroll flash.
59
+ Gated behind .js so it's always visible if JS is off. -->
60
+ <!-- justai auth: capture the SSO token here, synchronously, BEFORE the scroll-spy rewrites the hash
61
+ to #kernel — otherwise it clobbers #auth_token before the deferred sdk.js can read it. -->
62
+ <script is:inline>(function(){try{var h=location.hash||"";var m=h.match(/(?:^#|&)auth_token=([^&]+)/);if(m){localStorage.setItem("justai_token",decodeURIComponent(m[1]))}else if(/(?:^#|&)auth_signout=1\b/.test(h)){localStorage.removeItem("justai_token")}else{return}history.replaceState(null,"",location.pathname+location.search)}catch(e){}})();</script>
63
+ <script is:inline>document.documentElement.classList.add("js")</script>
64
+ <style is:inline>html.js body{opacity:0}html.js body.ready{opacity:1}</style>
65
+ <!-- The locale list is INJECTED, never typed. It used to be a literal array here — a second
66
+ declaration beside src/i18n/index.ts, kept in agreement by hand, in the one place where
67
+ being wrong is invisible: a locale missing from this list simply never auto-routes, and
68
+ nothing anywhere goes red. The same shape in astro.config had already drifted (twelve
69
+ listed while /it/ was live), which is why this one is wired to the real list instead of
70
+ being checked against it. -->
71
+ <script is:inline define:vars={{ supported: locales, def: defaultLocale }}>
72
+ // Auto-route to the visitor's language: a saved choice wins; otherwise the browser
73
+ // language on first visit. Only on the default (en) root — explicit /xx/ links are
74
+ // respected, and crawlers + hreflang still index every locale.
75
+ (function () {
76
+ try {
77
+ var path = location.pathname;
78
+ if (path !== "/" && path !== "/index.html") return;
79
+ var params = new URLSearchParams(location.search);
80
+ // Priority: ?lang hint from app.zone (saved as the choice) > saved pref > browser.
81
+ var hint = params.get("lang");
82
+ var target;
83
+ if (hint && supported.indexOf(hint) !== -1) {
84
+ target = hint;
85
+ localStorage.setItem("lang", hint);
86
+ } else {
87
+ target = localStorage.getItem("lang");
88
+ if (!target) {
89
+ var langs = navigator.languages || [navigator.language || def];
90
+ for (var i = 0; i < langs.length; i++) {
91
+ var c = (langs[i] || "").toLowerCase().split("-")[0];
92
+ if (supported.indexOf(c) !== -1) { target = c; break; }
93
+ }
94
+ }
95
+ }
96
+ if (target && target !== def && supported.indexOf(target) !== -1) {
97
+ location.replace("/" + target + "/" + location.search); // preserve ?ref
98
+ }
99
+ } catch (e) {}
100
+ })();
101
+ </script>
102
+ <title>{t.metaTitle}</title>
103
+ <meta name="description" content={t.metaDescription} />
104
+ <meta property="og:title" content={t.metaTitle} />
105
+ <meta property="og:description" content={t.metaDescription} />
106
+ <meta property="og:type" content="website" />
107
+ <meta property="og:image" content={new URL("/og.jpg", Astro.site)} />
108
+ <meta property="og:image:type" content="image/jpeg" />
109
+ <meta property="og:image:width" content="1200" />
110
+ <meta property="og:image:height" content="630" />
111
+ <meta property="og:url" content={new URL(Astro.url.pathname, Astro.site)} />
112
+ <meta property="og:site_name" content={new URL(site).host} />
113
+ <meta name="twitter:card" content="summary_large_image" />
114
+ <meta name="twitter:image" content={new URL("/og.jpg", Astro.site)} />
115
+ <meta name="theme-color" content={themeDark} media="(prefers-color-scheme: dark)" />
116
+ <meta name="theme-color" content={themeLight} media="(prefers-color-scheme: light)" />
117
+ <link rel="canonical" href={`${site}${localePath(lang)}`} />
118
+ {locales.map((l) => (
119
+ <link rel="alternate" hreflang={l} href={`${site}${localePath(l)}`} />
120
+ ))}
121
+ <link rel="alternate" hreflang="x-default" href={`${site}/`} />
122
+ <link rel="icon" href="/favicon.ico" sizes="32x32" />
123
+ <link
124
+ rel="icon"
125
+ href={favicon}
126
+ />
127
+ <!-- PWA: Workbox service worker (precache + offline) + installable manifest -->
128
+ <link rel="manifest" href="/manifest.webmanifest" />
129
+ <link rel="apple-touch-icon" href="/icon-192.png" />
130
+ <script is:inline>
131
+ if ("serviceWorker" in navigator) addEventListener("load", () => navigator.serviceWorker.register("/sw.js"));
132
+ </script>
133
+ <!-- Google Analytics 4 — anonymous (ad signals + personalization OFF), lazy-loaded after idle.
134
+ analytics_storage granted: direct user counting (cookieless modeling needs high traffic to
135
+ surface, so it shows nothing at low/launch volume).
136
+
137
+ gaId comes through define:vars because this script runs in the BROWSER. An is:inline block
138
+ has no access to component scope, so a bare `gaId` here would be undefined at runtime and
139
+ every persona would report to a property that does not exist — silently, with no failed
140
+ request and no console error, which is the only kind of analytics bug nobody finds. -->
141
+ <script is:inline define:vars={{ gaId }}>
142
+ window.dataLayer = window.dataLayer || [];
143
+ function gtag() { dataLayer.push(arguments); }
144
+ gtag("js", new Date());
145
+ gtag("consent", "default", { ad_storage: "denied", ad_user_data: "denied", ad_personalization: "denied", analytics_storage: "granted" });
146
+ // EEA + UK + CH: deny analytics until the visitor consents (the banner below grants it).
147
+ gtag("consent", "default", { region: ["AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE","IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE","IS","LI","NO","GB","CH"], ad_storage: "denied", ad_user_data: "denied", ad_personalization: "denied", analytics_storage: "denied" });
148
+ try { var cc0 = localStorage.getItem("consent"); if (cc0) gtag("consent", "update", { analytics_storage: cc0 }); } catch (e) {}
149
+ gtag("config", gaId, { cookie_expires: 60 * 60 * 24 * 60, /* two months — matches the GA data retention; the cookie must not outlive the data it labels (foundations/15) */ allow_google_signals: false, allow_ad_personalization_signals: false });
150
+ addEventListener("load", function () {
151
+ (window.requestIdleCallback || function (f) { setTimeout(f, 1200); })(function () {
152
+ var s = document.createElement("script");
153
+ s.async = true;
154
+ s.src = "https://www.googletagmanager.com/gtag/js?id=" + gaId;
155
+ document.head.appendChild(s);
156
+ });
157
+ });
158
+ </script>
159
+ </head>
160
+ <body>
161
+ <slot />
162
+ <!-- Consent banner — shown only to European-timezone visitors with no stored choice. The
163
+ Consent Mode region default already denies analytics for the EEA/UK until "Allow". -->
164
+ <div id="cc" class="cc" role="dialog" aria-label={t.chromePrivacy}>
165
+ <div class="cc-card">
166
+ <p class="cc-text">{t.consentText}</p>
167
+ <div class="cc-row">
168
+ <button type="button" class="cc-btn" data-consent="denied">{t.consentDecline}</button>
169
+ <button type="button" class="cc-btn cc-ok" data-consent="granted">{t.consentAllow}</button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ <style>
174
+ .cc { display: none; position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 300; justify-content: center; padding: 0 12px; pointer-events: none; }
175
+ .cc.show { display: flex; }
176
+ .cc-card { pointer-events: auto; width: 100%; max-width: 460px; margin-block-end: max(12px, env(safe-area-inset-bottom)); background: color-mix(in srgb, var(--bg) 93%, transparent); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: 0 14px 40px -20px rgba(0, 0, 0, 0.2); backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5); padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; animation: cc-up 0.45s var(--ease-spring) both; }
177
+ .cc-text { font-size: var(--fs-xs); color: var(--text); line-height: 1.5; }
178
+ .cc-row { display: flex; gap: 8px; justify-content: flex-end; }
179
+ .cc-btn { font: inherit; font-size: var(--fs-xs); font-weight: 600; padding: 8px 16px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface-2); color: var(--text); cursor: pointer; transition: transform 0.2s var(--ease-spring); }
180
+ .cc-btn:active { transform: scale(0.94); }
181
+ .cc-ok { background: var(--accent-press); border-color: var(--accent-press); color: #fff; }
182
+ @keyframes cc-up { from { transform: translateY(24px); opacity: 0; } }
183
+ @media (prefers-reduced-motion: reduce) { .cc-card { animation: none; } }
184
+ </style>
185
+ <script is:inline>
186
+ (function () {
187
+ try {
188
+ if (localStorage.getItem("consent")) return;
189
+ if (!document.getElementById("kernel")) return; // no kernel = not a content page (e.g. 404) — don't ask for consent on a dead end
190
+ if ((Intl.DateTimeFormat().resolvedOptions().timeZone || "").indexOf("Europe/") !== 0) return;
191
+ var el = document.getElementById("cc"); if (!el) return;
192
+ el.classList.add("show");
193
+ el.addEventListener("click", function (e) {
194
+ var v = e.target.getAttribute("data-consent"); if (!v) return;
195
+ gtag("consent", "update", { analytics_storage: v });
196
+ try { localStorage.setItem("consent", v); } catch (_) {}
197
+ el.classList.remove("show");
198
+ });
199
+ } catch (_) {}
200
+ })();
201
+ </script>
202
+ </body>
203
+ </html>