@justai/cuts 0.5.0 → 0.5.2

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 +1 -1
  2. package/src/Base.astro +33 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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/Base.astro CHANGED
@@ -185,7 +185,12 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
185
185
  {gaId && (
186
186
  <script is:inline define:vars={{ gaId }}>
187
187
  window.dataLayer = window.dataLayer || [];
188
- function gtag() { dataLayer.push(arguments); }
188
+ // gtag is put on WINDOW, not declared as `function gtag`, and this is load-bearing: `define:vars`
189
+ // wraps this inline script in an IIFE to scope the injected `gaId`, so a `function gtag` here
190
+ // would be trapped in that IIFE — and the consent banner's own inline script (in the body) calls
191
+ // gtag, giving "gtag is not defined" the moment a European visitor clicks Allow/Decline (the
192
+ // choice is never applied, the banner never closes). Global on window, both scripts see it.
193
+ window.gtag = window.gtag || function () { window.dataLayer.push(arguments); };
189
194
  gtag("js", new Date());
190
195
  gtag("consent", "default", { ad_storage: "denied", ad_user_data: "denied", ad_personalization: "denied", analytics_storage: "granted" });
191
196
  // EEA + UK + CH: deny analytics until the visitor consents (the banner below grants it).
@@ -216,25 +221,34 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
216
221
  {/* Consent banner — shown only to European-timezone visitors with no stored choice. The
217
222
  Consent Mode region default already denies analytics for the EEA/UK until "Allow". Gated on
218
223
  gaId with the analytics block above: a persona that runs no analytics has nothing to consent
219
- to, so it renders no banner. */}
224
+ to, so it renders no banner.
225
+
226
+ The <style> below sits INSIDE this {gaId && (...)} expression, so Astro does NOT scope it — a
227
+ style that is a direct static child of a component gets a data-astro-cid hash, but one nested
228
+ in a JSX expression is emitted verbatim, GLOBAL. So every selector here is fleet-wide and MUST
229
+ carry its own namespace. It once used a two-letter `.cc` for the banner container; the feed
230
+ loader emits `<div class="cc">` for its content column, so the global `.cc { display: none }`
231
+ hid the feed's text on EVERY persona. The whole family is `consent-*` now: unmistakably this
232
+ widget's, never anyone else's. A generic global class is a landmine that the shell hides from
233
+ the persona until a feed goes white — see [[the-shell-guarantees-you-cannot-see]]. */}
220
234
  {gaId && (
221
- <div id="cc" class="cc" role="dialog" aria-label={t.chromePrivacy}>
222
- <div class="cc-card">
223
- <p class="cc-text">{t.consentText}</p>
224
- <div class="cc-row">
225
- <button type="button" class="cc-btn" data-consent="denied">{t.consentDecline}</button>
226
- <button type="button" class="cc-btn cc-ok" data-consent="granted">{t.consentAllow}</button>
235
+ <div id="consent" class="consent" role="dialog" aria-label={t.chromePrivacy}>
236
+ <div class="consent-card">
237
+ <p class="consent-text">{t.consentText}</p>
238
+ <div class="consent-row">
239
+ <button type="button" class="consent-btn" data-consent="denied">{t.consentDecline}</button>
240
+ <button type="button" class="consent-btn consent-ok" data-consent="granted">{t.consentAllow}</button>
227
241
  </div>
228
242
  </div>
229
243
  </div>
230
244
  <style>
231
- .cc { display: none; position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 300; justify-content: center; padding: 0 12px; pointer-events: none; }
232
- .cc.show { display: flex; }
233
- .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; }
234
- .cc-text { font-size: var(--fs-xs); color: var(--text); line-height: 1.5; }
235
- .cc-row { display: flex; gap: 8px; justify-content: flex-end; }
236
- .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); }
237
- .cc-btn:active { transform: scale(0.94); }
245
+ .consent { display: none; position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 300; justify-content: center; padding: 0 12px; pointer-events: none; }
246
+ .consent.show { display: flex; }
247
+ .consent-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: consent-up 0.45s var(--ease-spring) both; }
248
+ .consent-text { font-size: var(--fs-xs); color: var(--text); line-height: 1.5; }
249
+ .consent-row { display: flex; gap: 8px; justify-content: flex-end; }
250
+ .consent-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); }
251
+ .consent-btn:active { transform: scale(0.94); }
238
252
  /* The consent button is the one place the fleet genuinely disagreed, and the disagreement is
239
253
  about CONTRAST rather than taste. Seven personas have dark accents and take white text on
240
254
  --accent-press. cleanmylink and howlong have bright ones — mint and amber — where white
@@ -242,9 +256,9 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
242
256
  Two hooks with the majority as the default, so the seven say nothing and the two say
243
257
  exactly what they already verified. This is the styling contract working as designed:
244
258
  colour is the persona's, and a real difference gets a declared hook rather than a fork. */
245
- .cc-ok { background: var(--consent-ok-bg, var(--accent-press)); border-color: var(--consent-ok-bg, var(--accent-press)); color: var(--on-accent, #fff); }
246
- @keyframes cc-up { from { transform: translateY(24px); opacity: 0; } }
247
- @media (prefers-reduced-motion: reduce) { .cc-card { animation: none; } }
259
+ .consent-ok { background: var(--consent-ok-bg, var(--accent-press)); border-color: var(--consent-ok-bg, var(--accent-press)); color: var(--on-accent, #fff); }
260
+ @keyframes consent-up { from { transform: translateY(24px); opacity: 0; } }
261
+ @media (prefers-reduced-motion: reduce) { .consent-card { animation: none; } }
248
262
  </style>
249
263
  <script is:inline>
250
264
  (function () {
@@ -252,7 +266,7 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
252
266
  if (localStorage.getItem("consent")) return;
253
267
  if (!document.getElementById("kernel")) return; // no kernel = not a content page (e.g. 404) — don't ask for consent on a dead end
254
268
  if ((Intl.DateTimeFormat().resolvedOptions().timeZone || "").indexOf("Europe/") !== 0) return;
255
- var el = document.getElementById("cc"); if (!el) return;
269
+ var el = document.getElementById("consent"); if (!el) return;
256
270
  el.classList.add("show");
257
271
  el.addEventListener("click", function (e) {
258
272
  var v = e.target.getAttribute("data-consent"); if (!v) return;