@justai/cuts 0.129.7 → 0.130.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.129.7",
3
+ "version": "0.130.1",
4
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": {
@@ -129,7 +129,7 @@ const href = `https://${apex}`;
129
129
  Dark is the un-queried default because the fleet's tokens are dark-first. */
130
130
  .home:hover .azmark .ctr, .home:focus-visible .azmark .ctr { fill: #F5F1E8; }
131
131
  @media (prefers-color-scheme: light) {
132
- .home:hover .azmark .ctr, .home:focus-visible .azmark .ctr { fill: #2B2724; }
132
+ :global(html:not([data-theme="dark"])) .home:hover .azmark .ctr, :global(html:not([data-theme="dark"])) .home:focus-visible .azmark .ctr { fill: #2B2724; }
133
133
  }
134
134
  </style>
135
135
 
package/src/Base.astro CHANGED
@@ -54,8 +54,21 @@ interface Props {
54
54
  * rather than narrowed to a shape only this package would recognise.
55
55
  */
56
56
  t: Record<string, unknown>;
57
- /** The persona's own origin, e.g. https://tabata.app.zone — canonical, hreflang and OG all read it. */
58
- site: string;
57
+ /**
58
+ * DEPRECATED — declare the origin in astro.config.mjs (`site:`) and stop passing this.
59
+ *
60
+ * The origin now comes from `Astro.site`, which every persona already has to declare for the
61
+ * build. Passing it here as well is the second declaration site this docstring warns about
62
+ * twenty lines above, and it went wrong exactly the way the locale list did: compress-image was
63
+ * copied from cleanmylink and shipped with cleanmylink's origin in this prop while its
64
+ * astro.config was correct. For a day, canonical, og:site_name and all fourteen hreflang hrefs
65
+ * named another product — while og:url and og:image, three lines apart in the same head, read
66
+ * `Astro.site` and were right. One head, two truths, and nothing said so.
67
+ *
68
+ * Still accepted so no persona has to change on the same day this ships; it is now checked
69
+ * rather than trusted, and a build where the two disagree fails.
70
+ */
71
+ site?: string;
59
72
  /** From the consuming repo's src/i18n — never from this package. */
60
73
  locales: readonly string[];
61
74
  defaultLocale: string;
@@ -87,13 +100,37 @@ interface Props {
87
100
  }
88
101
 
89
102
  const {
90
- lang, t, site, locales, defaultLocale, favicon,
103
+ lang, t, site: siteProp, locales, defaultLocale, favicon,
91
104
  themeDark,
92
105
  themeLight,
93
106
  gaId = "G-13L3BP6XCW",
94
107
  kind = "app",
95
108
  } = Astro.props;
96
109
 
110
+ // ── ONE HOME FOR THE ORIGIN ───────────────────────────────────────────────────────────────────
111
+ // queries: canonical yanlış · başka personanın alan adı · site prop · hreflang yanlış domain
112
+ //
113
+ // A wrong origin renders perfectly. That is the whole difficulty: nothing is red, the page is
114
+ // valid, and the only symptom is search engines being told this page belongs to someone else.
115
+ // The failure has to be moved to a place where it CANNOT be quiet, and a static build is the one
116
+ // place available — so a disagreement is an error, not a warning. A warning in a build log is the
117
+ // same silence with more steps.
118
+ const site = Astro.site?.origin ?? siteProp;
119
+ if (!site) {
120
+ throw new Error(
121
+ "[@justai/cuts] No origin. Declare it once, in astro.config.mjs:\n" +
122
+ ' export default personaConfig({ site: "https://<persona>.app.zone", ... })',
123
+ );
124
+ }
125
+ if (siteProp && Astro.site && new URL(siteProp).origin !== Astro.site.origin) {
126
+ throw new Error(
127
+ "[@justai/cuts] Two origins, and they disagree:\n" +
128
+ ` astro.config.mjs ${Astro.site.origin}\n` +
129
+ ` <Base site=…> ${new URL(siteProp).origin}\n` +
130
+ "astro.config wins — it is what the sitemap, the build and og:url already use. Drop the prop.",
131
+ );
132
+ }
133
+
97
134
  // queries: theme-color · a grey band under the game on iPhone · Safari's bottom bar colour ·
98
135
  // the browser chrome does not match the game · game persona theme colour
99
136
  //
package/src/Posts.astro CHANGED
@@ -172,7 +172,7 @@ const vars = [
172
172
  /* The light default is white because that is what --card-bg is there; the dark default is a faint white
173
173
  overlay because on dark --card-bg equals --surface, which is the column, so copying it would vanish. */
174
174
  @media (prefers-color-scheme: light) {
175
- .post:hover { background: color-mix(in srgb, var(--card-bg, #fff) 55%, transparent); }
175
+ :global(html:not([data-theme="dark"])) .post:hover { background: color-mix(in srgb, var(--card-bg, #fff) 55%, transparent); }
176
176
  }
177
177
  .post + .post { border-block-start: 1px solid var(--border); }
178
178
 
@@ -225,7 +225,7 @@ const vars = [
225
225
  .pa .mk, .rav .mk { width: 100%; height: 100%; object-fit: cover; display: block; }
226
226
  /* a dark tile must invert in light mode, not stay dark — personas whose avatar ground is a gradient
227
227
  pass avatarBgLight; the rest fall back to their single background. */
228
- @media (prefers-color-scheme: light) { .pa, .rav { background: var(--feed-av-bg-light, var(--feed-av-bg, #fff)); } }
228
+ @media (prefers-color-scheme: light) { :global(html:not([data-theme="dark"])) .pa, :global(html:not([data-theme="dark"])) .rav { background: var(--feed-av-bg-light, var(--feed-av-bg, #fff)); } }
229
229
 
230
230
  /* A bare mark stands on the feed's own ground, so the tile and its hairline have to go — and they have to
231
231
  go from the CONTAINER, which is why this reads upward from the image with :has(). Scoping it to the image
@@ -256,6 +256,9 @@ const vars = [
256
256
  .pa .mk.av-light, .rav .mk.av-light { display: none; }
257
257
  .pa .mk.av-dark, .rav .mk.av-dark { display: block; }
258
258
  }
259
+ /* The dark fork's data-theme twin: a light-system page forced dark must swap the marks too. */
260
+ :global(html[data-theme="dark"]) .pa .mk.av-light, :global(html[data-theme="dark"]) .rav .mk.av-light { display: none; }
261
+ :global(html[data-theme="dark"]) .pa .mk.av-dark, :global(html[data-theme="dark"]) .rav .mk.av-dark { display: block; }
259
262
 
260
263
  .ph { display: flex; align-items: center; gap: 5px; font-size: var(--fs-xs); flex-wrap: wrap; }
261
264
  .ph b { font-weight: 600; }
package/src/Preroll.astro CHANGED
@@ -364,34 +364,22 @@ const safeId = "game-" + (id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/
364
364
  * container, and layout containment makes it the containing block for FIXED descendants too — a
365
365
  * fixed overlay written inside it would be clipped to the frame it is meant to surround. */
366
366
  html[data-stage="playing"] { overflow: hidden; }
367
- /* The kernel that holds a preroll rides above the room PERMANENTLY, not only while playing: the
368
- * raise used to follow data-stage, so pausing dropped the kernel in one frame while the dim took
369
- * half a second to fade - and every pause put a brief wave of darkness across the game itself.
370
- * Static costs nothing (the room is transparent outside playing) and cannot mistime. */
371
- .cut.kernel:has(#pr) { position: relative; z-index: 35; }
372
- /* ── THE ROOM IS DARK DOT PAPER, NOT A BLACK FILM ───────────────────────────────────────────
373
- * queries: theater dim flat colour · the dots disappear in the dark · blender dark theme ·
374
- * dot paper grid in theater mode
367
+ /* queries: theater mode · where did the dim layer go · dark theme while playing
375
368
  *
376
- * The founder, looking at the first dim: *"dot paper grid'de dot kendini belli etmeyince bu
377
- * sefer duz bir renk gibi algilaniyor"* — and pointing at the fix: 3dviewer's player, whose
378
- * stage is Blender's own Theme Space grey, read out of a running Blender. A flat translucent
379
- * black erased the ground's identity; a dark room should still be THE SAME PAPER with the
380
- * lights down, the way Blender's viewport keeps its grid in the dark.
369
+ * ── THE THEATER IS THE PERSONA'S OWN DARK THEME, NOT A LAYER ────────────────────────────────
381
370
  *
382
- * So the layer carries its own lattice — the door's 24px dot grid, light-on-dark — over a
383
- * neutral Blender-family dark. The opacity is high on purpose: the dots belong to THIS layer,
384
- * and a translucent lattice over a busy page is noise, while a nearly-opaque one is a surface.
385
- * The chrome beneath (the toolbar steps under at z 20) stays legible as shapes in a dark room. */
386
- .pr-room {
387
- position: fixed; inset: 0; z-index: 30;
388
- background:
389
- radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px) 0 0 / 24px 24px,
390
- #262626;
391
- opacity: 0; pointer-events: none;
392
- transition: opacity 0.5s var(--ease-out, ease-out);
393
- }
394
- html[data-stage="playing"] .pr-room { opacity: 0.86; }
371
+ * Three custom dims were built and struck here in one day — a black film, a z-order dance, dark
372
+ * dot paper — before the founder pointed at what was already true: *"justai ecosisteminde zaten
373
+ * tum personalarin light/dark temalari vardi ve kusursuz calisiyorlardi. senin yapman gereken tek
374
+ * sey vardi: theme switch, simple."* Every persona already ships a complete dark theme - the
375
+ * ground, the dots, the toolbar, the cards, all of it designed and measured - and a second
376
+ * darkness laid over the first is two systems doing one job.
377
+ *
378
+ * So the stage sets data-theme="dark" on <html> while playing and removes it on pause, and the
379
+ * guard the fleet's light forks now carry (time's wall wrote the idiom first: data-theme wins
380
+ * over the media query) does everything the dim layer ever tried to. A dark-system visitor sees
381
+ * no change at all; a light-system visitor gets the persona's own night, and their auto theme
382
+ * back the moment they pause. */
395
383
  </style>
396
384
 
397
385
  <script>
@@ -502,16 +490,25 @@ const safeId = "game-" + (id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/
502
490
  let declareProtocol: (v: number) => void = () => {};
503
491
  const declared: Promise<number> = new Promise((r) => { declareProtocol = r; });
504
492
  if (bus) {
505
- // The room's lights, hung on <body> — see the global style block for why not in here.
506
- const room = document.createElement("div");
507
- room.className = "pr-room";
508
- room.setAttribute("aria-hidden", "true");
509
- document.body.appendChild(room);
493
+ // Whatever theme the page carried before the stage took it — restored on pause, so a manual
494
+ // data-theme set by anything else survives a round trip through a game.
495
+ let priorTheme: string | undefined;
510
496
 
511
497
  const setStage = (mode: string) => {
512
498
  const playing = mode === "playing";
513
- if (playing) document.documentElement.dataset.stage = "playing";
514
- else delete document.documentElement.dataset.stage;
499
+ if (playing) {
500
+ document.documentElement.dataset.stage = "playing";
501
+ // The theater: the persona's own dark theme, switched on. See the style block above.
502
+ if (document.documentElement.dataset.theme !== "dark") {
503
+ priorTheme = document.documentElement.dataset.theme;
504
+ document.documentElement.dataset.theme = "dark";
505
+ }
506
+ } else {
507
+ delete document.documentElement.dataset.stage;
508
+ if (priorTheme === undefined) delete document.documentElement.dataset.theme;
509
+ else document.documentElement.dataset.theme = priorTheme;
510
+ priorTheme = undefined;
511
+ }
515
512
  // The other cuts go INERT rather than display:none — the page must not reflow, and the
516
513
  // player's scroll position has to survive a pause. inert takes them out of tab order and
517
514
  // the accessibility tree both; a screen reader must not keep walking a page the player
package/src/Profile.astro CHANGED
@@ -129,7 +129,7 @@ const vars = [
129
129
  .avatar :global(svg) { width: 100%; height: 100%; display: block; }
130
130
  .avatar :global(img) { width: 100%; height: 100%; display: block; object-fit: cover; }
131
131
  @media (prefers-color-scheme: light) {
132
- .avatar { border-color: #fff; background: var(--avatar-bg-light, var(--avatar-bg, #fff)); }
132
+ :global(html:not([data-theme="dark"])) .avatar { border-color: #fff; background: var(--avatar-bg-light, var(--avatar-bg, #fff)); }
133
133
  }
134
134
  .pname { font-size: var(--fs-lg); font-weight: 700; letter-spacing: -0.02em; margin-block-start: 12px; }
135
135
  .handle { color: var(--text-dim); font-size: var(--fs-xs); margin-block-start: 2px; }
package/src/Toolbar.astro CHANGED
@@ -344,7 +344,8 @@ import AuthMount from "./AuthMount.astro";
344
344
  FALLBACK differs by scheme, and a band carrying only the dark default is a black strip on a light
345
345
  page — which is exactly what the founder photographed a minute after the first attempt. */
346
346
  @media (prefers-color-scheme: light) {
347
- .tb-slot::before { background: var(--toolbar-bg, #fbfcfe); }
347
+ /* Guarded like every light fork in the fleet: data-theme="dark" wins over the media query. */
348
+ :global(html:not([data-theme="dark"])) .tb-slot::before { background: var(--toolbar-bg, #fbfcfe); }
348
349
  }
349
350
  .toolbar {
350
351
  /* position, top and z-index live on .tb-slot above — this element is static inside it now. */
@@ -389,7 +390,7 @@ import AuthMount from "./AuthMount.astro";
389
390
  border-block-end: none;
390
391
  }
391
392
  @media (prefers-color-scheme: light) {
392
- .toolbar {
393
+ :global(html:not([data-theme="dark"])) .toolbar {
393
394
  background: var(--toolbar-bg, #fbfcfe);
394
395
  box-shadow: var(--toolbar-shadow, 0 1px 2px rgba(16, 24, 40, 0.06), 0 8px 20px -8px rgba(16, 24, 40, 0.16));
395
396
  }
@@ -457,21 +458,15 @@ import AuthMount from "./AuthMount.astro";
457
458
  * Opacity and pointer-events here; the INERT that takes them out of the tab order is set in the
458
459
  * script, because CSS cannot. The transition matters more than it looks: chrome that snaps away
459
460
  * reads as breaking, chrome that withdraws reads as dimming the house lights. */
460
- /* ── IN THE DARK, NOT DISABLED — how this row goes quiet while a game plays ─────────────────
461
- * queries: toolbar looks disabled · faded but clickable · toolbar during a game · theater
461
+ /* ── HOW THIS ROW GOES QUIET WHILE A GAME PLAYS: IT DOES NOT ─────────────────────────────────
462
+ * queries: toolbar during a game · theater · toolbar looks disabled
462
463
  *
463
- * Three versions of quieting, each struck by the founder. Fading the elements was the last one:
464
- * *"neden disable mis gibi gozukuyor ama tiklanabiliyor? tiklanabiliyorsa disable gibi
465
- * gozukmemeli."* Right - reduced opacity IS the visual language of disabled, and nothing
466
- * clickable may speak it.
467
- *
468
- * The answer was in the theater metaphor the whole time: in a dark cinema the seats are not
469
- * disabled, they are in the dark. So the elements keep their full strength and the ROW STEPS
470
- * UNDER THE ROOM'S DIM - below the .pr-room layer the stage protocol hangs over the page - and
471
- * darkens with everything else in one coherent light change. The dim takes no pointer events,
472
- * so every control underneath works exactly as it looks like it should: like chrome in a dark
473
- * room, waiting. */
474
- :global(html[data-stage="playing"]) .tb-slot { z-index: 20; }
464
+ * Four versions in one day, each struck by the founder, and the last one is no CSS at all. A
465
+ * pause handle in the row; then inert; then faded ("disable gibi gozukmemeli - tiklanabiliyorsa");
466
+ * then stepped under a custom dim layer. The standing answer: the stage switches the page to the
467
+ * persona's own DARK THEME (data-theme on html, see Preroll), and this row simply wears it - the
468
+ * same bar every dark-system visitor already sees, fully alive, auto-pausing the game when
469
+ * touched. Nothing here needs to know a game exists. */
475
470
  /* icon-only section nav — current cut highlighted */
476
471
  .tb-nav {
477
472
  display: grid; place-items: center; width: 38px; height: 36px;
package/src/ground.css CHANGED
@@ -83,8 +83,14 @@
83
83
  --card-shadow-hover: 0 2px 6px rgba(0, 0, 0, 0.4), 0 12px 28px -14px rgba(0, 0, 0, 0.55);
84
84
  }
85
85
 
86
+ /* queries: force dark while playing · theme switch · data-theme dark · stage theme
87
+ * THE GUARD IS THE FLEET'S OWN IDIOM — time's wall wrote it first: *"data-theme wins over the media
88
+ * query in both directions."* A light-system visitor whose page carries data-theme="dark" gets the
89
+ * dark theme this file already ships; removing the attribute returns them to auto. The stage
90
+ * protocol is the first caster (a game playing = dark), and any future manual theme switch gets
91
+ * this seam for free. */
86
92
  @media (prefers-color-scheme: light) {
87
- :where(:root) {
93
+ :where(:root:not([data-theme="dark"])) {
88
94
  --bg: #f6f7f9;
89
95
  --text: #0a0c10;
90
96
  --text-dim: #5b6472;