studio-engine 0.74.7 → 0.74.8
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +802 -631
- data/README.md +5 -2
- data/app/views/studio/modals/blocks/_close_x.html.erb +6 -4
- data/app/views/studio/modals/blocks/_rail_row.html.erb +3 -1
- data/app/views/style/_modals.html.erb +190 -380
- data/app/views/style/modals/_ds_email_field.html.erb +58 -0
- data/app/views/style/modals/_ds_resend_footer.html.erb +70 -0
- data/app/views/style/modals/_ds_stack_demo.html.erb +63 -0
- data/app/views/style/modals/_wallet_connect.html.erb +26 -12
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +4 -2
- metadata +4 -6
- data/app/views/style/modals/_auth.html.erb +0 -307
- data/app/views/style/modals/_ds_onramp_hub.html.erb +0 -71
- data/app/views/style/modals/_ds_wallet_topup.html.erb +0 -69
- data/app/views/style/modals/_entry_tokens.html.erb +0 -188
- data/app/views/style/modals/_onchain_tx.html.erb +0 -86
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package and pin the floor each one needs — the pins differ on purpose, and every consumer records why beside its own. Bumping the gem version and updating consumer lockfiles is a release; `bin/release prepare` allocates the version and does both (see [`docs/RELEASE.md`](./docs/RELEASE.md)).
|
|
3
|
+
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package and pin the floor each one needs — the pins differ on purpose, and every consumer records why beside its own. Bumping the gem version and updating consumer lockfiles is a release; `bin/release prepare` allocates the version and does both (see [`docs/RELEASE.md`](./docs/RELEASE.md)). `prepare` does NOT roll this file: renaming `## Unreleased` to the allocated version and opening a fresh empty one is a manual conductor step (see [`docs/RELEASE.md`](./docs/RELEASE.md), *Rolling `Unreleased` into a version*), guarded by `test/docs/changelog_structure_test.rb`.
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
### Docs
|
|
8
|
+
|
|
9
|
+
- **Thirty-five minor versions of shipped entries left `## Unreleased` and moved
|
|
10
|
+
under the version that actually shipped them.** The block spanned 2,382 lines and every
|
|
11
|
+
entry in it had already been published — `accepted` sat on the `v0.74.4` release
|
|
12
|
+
commit when they were rolled — so the heading said "pending" about the whole history of the gem, and
|
|
13
|
+
each reviewer who opened the file had to settle per entry which half it was in.
|
|
14
|
+
85 entries plus three unbulleted prose blocks now sit under 46 version headings
|
|
15
|
+
from `0.40.0` to `0.74.3`; `## Unreleased` holds only this note.
|
|
16
|
+
|
|
17
|
+
**Bookkeeping only — no entry was reworded, added or dropped.** Attribution came
|
|
18
|
+
from git, never from position in the file: `git blame -w -M` on each entry, the
|
|
19
|
+
pickaxe (`git log -S`) for its introducing commit, and a scan of `CHANGELOG.md`
|
|
20
|
+
at all 81 release tags from `v0.39.0` up for the earliest tag whose file already
|
|
21
|
+
contained the entry's text. All three agreed on 81 of 85; each of the four splits
|
|
22
|
+
was a false `-M` match on boilerplate — a bare fenced-Ruby opener, a
|
|
23
|
+
`bin/rails … install:migrations` line — and resolved to the majority of five
|
|
24
|
+
per-entry probes. Verified as a permutation: 1,991 content lines before, the
|
|
25
|
+
same 1,991 after, none added, none lost.
|
|
26
|
+
|
|
27
|
+
**The cause, and the guard.** `bin/release prepare` commits `version.rb` with its
|
|
28
|
+
lockfile, publishes and tags — it never touches this file, and nothing failed
|
|
29
|
+
when the roll was skipped. `test/docs/changelog_structure_test.rb` now asserts the
|
|
30
|
+
file's SHAPE, never its prose: one leading `## Unreleased`, strictly decreasing
|
|
31
|
+
versions, no duplicate version, no orphan `###`, a parse floor so a rotted regex
|
|
32
|
+
fails loudly rather than passing vacuously, and — the one that would have caught
|
|
33
|
+
this in week one — `Studio::VERSION` no more than two minor versions ahead of the
|
|
34
|
+
newest heading. Against the pre-change file it reports "35 minor versions of
|
|
35
|
+
entries are still filed under `## Unreleased`". Automating the roll inside
|
|
36
|
+
`bin/release prepare` (mcritchie-studio) is the real fix and is not done.
|
|
37
|
+
|
|
38
|
+
## 0.74.3 — 2026-09-08
|
|
39
|
+
|
|
7
40
|
### Fixed
|
|
8
41
|
|
|
9
42
|
- **A store name that is not a JS identifier half-worked, silently, in seventeen
|
|
@@ -52,6 +85,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
52
85
|
regex inline as the worked example and now calls the shared one — same pattern,
|
|
53
86
|
same error, same message.
|
|
54
87
|
|
|
88
|
+
## 0.74.2 — 2026-09-08
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
55
92
|
- **The email banner assembled two attributes INCLUDING THEIR OWN QUOTES in Ruby,
|
|
56
93
|
so ERB escaping never ran on them.** `_layered_banner.html.erb` wrote
|
|
57
94
|
`background="..."` and `bgcolor="..."` as Ruby strings and marked them
|
|
@@ -130,6 +167,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
130
167
|
FOUR, so a false completeness claim was shipping one sentence above the entry
|
|
131
168
|
announcing a fix for exactly that.
|
|
132
169
|
|
|
170
|
+
## 0.74.1 — 2026-09-08
|
|
171
|
+
|
|
172
|
+
### Fixed
|
|
173
|
+
|
|
133
174
|
- **A quote in a host local can no longer close an Alpine attribute and turn the
|
|
134
175
|
rest of the element into markup.** Four partials built an attribute — its OWN
|
|
135
176
|
QUOTES included — as a Ruby String and marked it `html_safe`, which is the one
|
|
@@ -242,6 +283,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
242
283
|
already clean. Both claims are corrected, and the comment now points at the test
|
|
243
284
|
rather than restating that the sweep is done.
|
|
244
285
|
|
|
286
|
+
## 0.74.0 — 2026-09-08
|
|
287
|
+
|
|
288
|
+
### Fixed
|
|
289
|
+
|
|
245
290
|
- **The app census in these comments was short by the most important app.** Ten
|
|
246
291
|
sites said this engine is mounted by SIX apps and that THREE of them bundle no
|
|
247
292
|
`solana-studio`. Measured 2026-09-07 by the criterion that reproduces it — a
|
|
@@ -285,6 +330,164 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
285
330
|
|
|
286
331
|
Comment and prose only. No behaviour changes.
|
|
287
332
|
|
|
333
|
+
- **A host's apostrophe no longer kills the card it was passed to — now across the
|
|
334
|
+
blocks, not just the first-name step.** The same defect
|
|
335
|
+
`onboarding/_first_name` fixed one release ago turns out to be an engine-wide
|
|
336
|
+
idiom: a host-supplied local interpolated into a JS string literal inside a
|
|
337
|
+
JS-evaluating attribute. Repaired at eleven more splices.
|
|
338
|
+
|
|
339
|
+
**WHY IT IS WORTH A CHANGELOG LINE WHEN NOTHING VISIBLE CHANGES.** The failure
|
|
340
|
+
mode is silent. A bare apostrophe closes the JS literal, the whole expression
|
|
341
|
+
becomes a SyntaxError, and Alpine mounts the component as a NO-OP that still
|
|
342
|
+
renders every element — a card that looks perfect and whose buttons do nothing.
|
|
343
|
+
There is no error on screen, nothing in the server log, and no markup assertion
|
|
344
|
+
that can see it. Every value in every consumer resolves to a source literal or a
|
|
345
|
+
frozen constant today, so this is latent cover rather than a live fix; it is
|
|
346
|
+
worth doing because the next local to carry prose will look like an ordinary
|
|
347
|
+
change to whoever writes it.
|
|
348
|
+
|
|
349
|
+
**WHAT MOVED.** `blocks/_success_card` (`cta_event` at both CTA branches,
|
|
350
|
+
`secondary_event`), `blocks/_error_card` (`cta_event`, `secondary_event`),
|
|
351
|
+
`blocks/_entry_confirmed` and `blocks/_solana_tx_link` (`cluster_param`),
|
|
352
|
+
`modals/_crop_photo` (`store`), `studio/emails/show` (the two upload filenames
|
|
353
|
+
and the success sentence), and `profiles/_birthday_fields` (the date value).
|
|
354
|
+
No default or in-repo value contains a character either escaper touches, so
|
|
355
|
+
every shipped card renders byte-for-byte what it did.
|
|
356
|
+
|
|
357
|
+
**THE MECHANISM NOW HAS ONE HOME AND ONE GUARD.** `Studio::JsLiteral.in_attribute`
|
|
358
|
+
replaces the four inline copies in `_first_name`. TWO escapers have to run — one
|
|
359
|
+
for the JS literal, one for the HTML attribute — and the second only runs on a
|
|
360
|
+
value ERB still believes is unsafe, which is why the value is interpolated before
|
|
361
|
+
it is escaped. That subtlety was re-derived at every call site and had no test
|
|
362
|
+
anywhere; deleting it used to leave the suite green.
|
|
363
|
+
|
|
364
|
+
**NOT A FIX FOR IDENTIFIER POSITION, deliberately.** A local spliced in as a bare
|
|
365
|
+
NAME — `$store.<name>.close()` — must be VALIDATED, never escaped, because
|
|
366
|
+
`escape_javascript` also escapes `$` and mangles a legal store name. That fleet
|
|
367
|
+
(about 36 splices across 19 partials) and a third class found alongside it —
|
|
368
|
+
Ruby-ASSEMBLED JS emitted into an attribute, some of it already `html_safe` — are
|
|
369
|
+
scoped OUT of this change and carry their own tickets. The SHAPE of the splice
|
|
370
|
+
decides the repair, never the name of the local.
|
|
371
|
+
|
|
372
|
+
- **`Studio::S3` no longer mistakes a Rails NAMESPACE for a Rails application.**
|
|
373
|
+
`environment` guarded on `defined?(Rails)` and then called `Rails.env`.
|
|
374
|
+
rails-html-sanitizer — a transitive dependency of `action_view`, which arrives
|
|
375
|
+
long before any application does — ships a namespace-only `module Rails` with no
|
|
376
|
+
singleton methods, so that guard reads true and the next call raises
|
|
377
|
+
`NoMethodError: undefined method 'env' for module Rails`. It now asks
|
|
378
|
+
`Rails.respond_to?(:env)`, which is the form `lib/studio.rb` now uses in FOUR
|
|
379
|
+
places: the three it already carried (lines 701, 707 and 742) plus the route
|
|
380
|
+
guard at line 859. **This entry originally called it "the straggler"; that was
|
|
381
|
+
wrong** — three more sites carried the bare form, line 859 among them, and all
|
|
382
|
+
three are swept in *The Rails guard sweep really is finished now* above, in this
|
|
383
|
+
same unreleased block. No shipped app can reach it — every host boots a
|
|
384
|
+
real application — but the engine's own pure-Ruby unit lane can, and it did:
|
|
385
|
+
adding one `require` for a file that needs `action_view` turned an untouched
|
|
386
|
+
`email_catalog_test` red with two errors about email uploads.
|
|
387
|
+
|
|
388
|
+
## 0.72.3 — 2026-09-07
|
|
389
|
+
|
|
390
|
+
### Changed
|
|
391
|
+
|
|
392
|
+
- **The pinned stack composes itself, and publishes in the frame the change
|
|
393
|
+
happened.** `data-pin` layers now also publish `--pin-stack-bottom` (the bottom
|
|
394
|
+
of the whole stack) and `--pin-<name>-top` (the bottom of everything above that
|
|
395
|
+
layer), and the publisher writes them from inside its ResizeObserver callback
|
|
396
|
+
rather than deferring to `requestAnimationFrame`.
|
|
397
|
+
|
|
398
|
+
**THE DEFECT.** A frame runs `rAF -> style/layout -> ResizeObserver -> paint`,
|
|
399
|
+
so a write made in the frame callback lands at the top of the NEXT frame. Every
|
|
400
|
+
frame in which a pinned layer appeared or disappeared therefore painted with the
|
|
401
|
+
previous frame's number. Measured in isolation by toggling a layer's `display`
|
|
402
|
+
and sampling what the changing frame paints: rAF-deferred wrong on 8/8 changing
|
|
403
|
+
frames, synchronous wrong on 0/8. On mcritchie-studio's production
|
|
404
|
+
`/deployments` it showed as the board's swim-lane headers slamming 99px and back
|
|
405
|
+
on a page nobody was touching — parked at a fixed scroll offset with document
|
|
406
|
+
height, row position and nav height all constant, the header's `top` went
|
|
407
|
+
152 -> 53 -> 152 -> 53px in 240ms behind two Turbo broadcasts.
|
|
408
|
+
|
|
409
|
+
**AND CONSUMERS NO LONGER COMPOSE THE STACK.** `top: max(var(--pin-nav-bottom),
|
|
410
|
+
var(--pin-apps-bottom))` fails twice: it does not scale, because a fourth layer
|
|
411
|
+
means editing every consumer that ever wanted to sit under the stack; and it is
|
|
412
|
+
not sound, because a `max()` over two custom properties is only meaningful if
|
|
413
|
+
they were written in the same frame. `--pin-stack-bottom` is one value that
|
|
414
|
+
cannot disagree with itself. A layer that is ITSELF in the stack reads
|
|
415
|
+
`--pin-<name>-top` instead, which excludes its own edge — positioning off the
|
|
416
|
+
stack bottom would make it chase itself down the page. Stacking order is
|
|
417
|
+
document order, with `data-pin-order` overriding it for a layer whose DOM
|
|
418
|
+
position does not match where it sits on screen.
|
|
419
|
+
|
|
420
|
+
**THE REGISTRY IS DERIVED PER PUBLISH, never cached.** A Turbo Stream replaces a
|
|
421
|
+
pinned layer's node without a `turbo:load`, and a held reference then points at
|
|
422
|
+
the DETACHED predecessor, whose rect is all zeros — indistinguishable from a
|
|
423
|
+
layer that is legitimately hidden. Re-querying makes the stale reference
|
|
424
|
+
impossible rather than merely unlikely.
|
|
425
|
+
|
|
426
|
+
**THE COST, re-measured**, because the rule this reverses was itself justified by
|
|
427
|
+
a measurement (turf-monster, 6x CPU throttle, frames over 20ms 13/24 through the
|
|
428
|
+
collapse ramp vs 0/24 once the observer was coalesced). What was expensive there
|
|
429
|
+
was read-write thrash, not the observer: the publisher now takes one read pass
|
|
430
|
+
and one write pass per invocation, and in an RO callback layout is already clean,
|
|
431
|
+
so those reads force nothing. On the engine lab at the same 6x throttle, ~700
|
|
432
|
+
frames of ramp each way — `RO through rAF` median 8.3ms / p90 10.0ms / 0 frames
|
|
433
|
+
over 20ms; `RO synchronous` median 8.4-8.5ms / p90 11.0ms / 3-6 frames over 20ms;
|
|
434
|
+
ZERO frames over 32ms on either, so neither drops one.
|
|
435
|
+
|
|
436
|
+
**BACK-COMPAT.** `--nav-h`, `--nav-bottom` and `--pin-<name>-h` /
|
|
437
|
+
`--pin-<name>-bottom` publish exactly as before, from the same measurements, so
|
|
438
|
+
the 26 consumers across turf-monster, rolio and mcritchie-studio need no change
|
|
439
|
+
and no floor bump. A structural change (a Turbo patch, a fresh document, a layer
|
|
440
|
+
joining or leaving) now forces a full write, because the unchanged-write skip is
|
|
441
|
+
only safe while nothing else touches these properties — the lab caught
|
|
442
|
+
`--nav-h` staying empty through a re-scan on exactly the host-owned-header path
|
|
443
|
+
both live consumers use. The lane contract moves 131 -> 132
|
|
444
|
+
(`config/e2e_lane.yml`), re-derived with the lister.
|
|
445
|
+
|
|
446
|
+
## 0.72.2 — 2026-09-07
|
|
447
|
+
|
|
448
|
+
### Fixed
|
|
449
|
+
|
|
450
|
+
- **The style guide's two "Sign Wallet" thumbnails no longer crown themselves
|
|
451
|
+
with a padlock the card stopped drawing, and the guide's lock no longer trails
|
|
452
|
+
the release that removed it.** solana-studio 0.6.1 replaced the step-up card's
|
|
453
|
+
padlock with the remembered wallet's own brand mark, falling back to a neutral
|
|
454
|
+
billfold where no brand is remembered. Both hand-drawn thumbnails in
|
|
455
|
+
`style/_modals` went on drawing the padlock, so the first thing a designer
|
|
456
|
+
reads advertised a glyph the design system had retired.
|
|
457
|
+
|
|
458
|
+
**THE THUMBNAILS WERE HALF OF IT.** The opened specimen renders the REAL
|
|
459
|
+
shared partial, but it does so through whatever `Gemfile.lock` resolves — so
|
|
460
|
+
it is only ever as current as the lock, and the lock sat on 0.6.0 while
|
|
461
|
+
turf-monster shipped 0.6.1. The guide's opened card was still drawing the
|
|
462
|
+
padlock too, which `bin/gem-drift-check` had already been failing on
|
|
463
|
+
(`engine 0.6.0 TRAILS turf-monster 0.6.1`). Correcting the sketches alone
|
|
464
|
+
would have put a brand mark on the thumbnail and a padlock in the card it
|
|
465
|
+
opens — moving the contradiction onto one screen rather than removing it. So
|
|
466
|
+
the lock had to move too, and it has: `accepted` carried the engine to
|
|
467
|
+
solana-studio 0.7.0 while this change sat in review, which is past the 0.6.1
|
|
468
|
+
that removed the padlock. This change no longer moves the lock itself — it
|
|
469
|
+
defers to the line `accepted` already holds — so the sketch and the card it
|
|
470
|
+
opens agree without it.
|
|
471
|
+
|
|
472
|
+
The sketches follow the card: a centered brand tile where a wallet is
|
|
473
|
+
remembered, the card's own billfold outline in an inset square where none is.
|
|
474
|
+
They stay SKETCHES rather than real brand marks for a mechanical reason worth
|
|
475
|
+
recording — every `se-wallet` symbol on the guide is defined inside a
|
|
476
|
+
`template x-if`, whose content is inert until Alpine clones it, so a `use`
|
|
477
|
+
placed outside one resolves to nothing and paints an empty box.
|
|
478
|
+
|
|
479
|
+
Guarded by `test/views/step_up_specimen_thumbnails_test.rb`, which slices each
|
|
480
|
+
thumbnail by a `data-test` hook before asserting. That slicing is the point:
|
|
481
|
+
the rendered guide already carried 17 `se-wallet-` hits from the picker and
|
|
482
|
+
the card's own CTA, so a page-wide assertion for the mark could not fail, and
|
|
483
|
+
a page-wide assertion against the padlock could not pass — the drag-board
|
|
484
|
+
specimen's prose legitimately contains one. The padlock is pinned in BOTH the
|
|
485
|
+
codepoint and the HTML-entity form. MEASURED: five mutants, five killed —
|
|
486
|
+
the padlock restored in either form, either header mark removed, and the lock
|
|
487
|
+
reverted to 0.6.0 each reddens the assertion that owns it.
|
|
488
|
+
|
|
489
|
+
## 0.72.0 — 2026-09-07
|
|
490
|
+
|
|
288
491
|
### Added
|
|
289
492
|
|
|
290
493
|
- **The first-name card now says WHICH path finished it, and can type its
|
|
@@ -344,6 +547,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
344
547
|
test, while forcing the typed mode on, un-guarding the refocus, and treating
|
|
345
548
|
an empty pool as present each hit a placeholder test.
|
|
346
549
|
|
|
550
|
+
## 0.71.0 — 2026-09-07
|
|
551
|
+
|
|
552
|
+
### Added
|
|
553
|
+
|
|
347
554
|
- **The living style guide grows a HOST SECTION seam.** `/admin/style` rendered
|
|
348
555
|
four hard-coded engine sections (Theme, Modals, Tricks, Tasks) with no way for
|
|
349
556
|
a consuming app to contribute one of its own. An app now defines
|
|
@@ -377,6 +584,11 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
377
584
|
|
|
378
585
|
**The seam ships no host section itself.** The first consumer is
|
|
379
586
|
turf-monster's own `style/host/_modals`, which lands as its own task.
|
|
587
|
+
|
|
588
|
+
## 0.70.0 — 2026-09-06
|
|
589
|
+
|
|
590
|
+
### Added
|
|
591
|
+
|
|
380
592
|
- **A `required` mode on the shared first-name card.**
|
|
381
593
|
`studio/modals/onboarding/_first_name` grows one local. `required: true`
|
|
382
594
|
renders the card with BOTH skip affordances gone — the "Skip for now" button
|
|
@@ -414,21 +626,6 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
414
626
|
runtime — which is how the two cards already differ in copy, not just in
|
|
415
627
|
affordances.
|
|
416
628
|
|
|
417
|
-
- **Knowledge coverage view** — `/admin/knowledge/coverage` +
|
|
418
|
-
`Studio::KnowledgeExpectation`: the "what SHOULD exist" half of the
|
|
419
|
-
knowledge layer. Expectations carry an entity, folder, provenance
|
|
420
|
-
(`source_note`, e.g. "diligence tracker item 14"), and a cadence — `once`
|
|
421
|
-
(an LOI) or `monthly` (aging inventory), where each calendar month from
|
|
422
|
-
`start_on` is a slot and the view names missing months outright
|
|
423
|
-
(`2026-09 MISSING`). Documents fulfill expectations by an EXPLICIT
|
|
424
|
-
`expectation_id` set at triage (the show page grows a "fulfills expectation"
|
|
425
|
-
select; a fuzzy name-match would silently merge lookalikes, an id never
|
|
426
|
-
does), and superseded documents never fill a slot. Ships two reference
|
|
427
|
-
migrations (`create_studio_knowledge_expectations`,
|
|
428
|
-
`add_expectation_to_studio_knowledge_docs`); routes ride the existing
|
|
429
|
-
`Studio.draw_knowledge_routes` opt-in. Built for the Commercial Welding
|
|
430
|
-
65-item diligence tracker; app-agnostic like the rest of the layer.
|
|
431
|
-
|
|
432
629
|
- **The style guide grows the two modal simulators that review ENGINE
|
|
433
630
|
behaviour** — `#modals-motion-registry` ("Enter & leave animations") and
|
|
434
631
|
`#modals-stack-mechanics` ("Stack behaviour") in `style/_modals`. Both are
|
|
@@ -464,212 +661,35 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
464
661
|
reintroduced, with the lab server restarted between runs. The lane contract
|
|
465
662
|
moves 116 → 123 (`config/e2e_lane.yml`), re-derived with the lister.
|
|
466
663
|
|
|
467
|
-
###
|
|
468
|
-
|
|
469
|
-
- **The pinned stack composes itself, and publishes in the frame the change
|
|
470
|
-
happened.** `data-pin` layers now also publish `--pin-stack-bottom` (the bottom
|
|
471
|
-
of the whole stack) and `--pin-<name>-top` (the bottom of everything above that
|
|
472
|
-
layer), and the publisher writes them from inside its ResizeObserver callback
|
|
473
|
-
rather than deferring to `requestAnimationFrame`.
|
|
664
|
+
### Fixed
|
|
474
665
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
666
|
+
- **The style guide's page-scoped modal store now resolves animations through
|
|
667
|
+
the LIVE registry, so the guide can no longer disagree with itself.**
|
|
668
|
+
`style/_modals`' `dsModals` carried a hard-coded COPY of the animation table
|
|
669
|
+
while the new simulator builds its controls from `window.ModalAnimations`.
|
|
670
|
+
The two agreed today, which is exactly why this was invisible — but a
|
|
671
|
+
consumer registering a key would have grown a control from the registry that
|
|
672
|
+
the store then resolved back to `pop`: the button said "shake", the card
|
|
673
|
+
popped, and nothing reported the gap. Its `modalAnim` now reads
|
|
674
|
+
`window.ModalAnimations` at CALL time with the same late-binding guard the
|
|
675
|
+
shared host uses (unknown keys and a gutted registry still fall back to
|
|
676
|
+
`pop`, so a miss can never throw on `.ms` and strand a modal open); the local
|
|
677
|
+
table remains only as the fallback for a guide rendered without the host.
|
|
678
|
+
MEASURED in a browser both ways: registering a new key at runtime surfaces a
|
|
679
|
+
control AND plays it, and with this fix reverted the same key surfaces a
|
|
680
|
+
control that plays `modal-card-mount`.
|
|
485
681
|
|
|
486
|
-
|
|
487
|
-
var(--pin-apps-bottom))` fails twice: it does not scale, because a fourth layer
|
|
488
|
-
means editing every consumer that ever wanted to sit under the stack; and it is
|
|
489
|
-
not sound, because a `max()` over two custom properties is only meaningful if
|
|
490
|
-
they were written in the same frame. `--pin-stack-bottom` is one value that
|
|
491
|
-
cannot disagree with itself. A layer that is ITSELF in the stack reads
|
|
492
|
-
`--pin-<name>-top` instead, which excludes its own edge — positioning off the
|
|
493
|
-
stack bottom would make it chase itself down the page. Stacking order is
|
|
494
|
-
document order, with `data-pin-order` overriding it for a layer whose DOM
|
|
495
|
-
position does not match where it sits on screen.
|
|
682
|
+
## 0.69.5 — 2026-09-06
|
|
496
683
|
|
|
497
|
-
|
|
498
|
-
pinned layer's node without a `turbo:load`, and a held reference then points at
|
|
499
|
-
the DETACHED predecessor, whose rect is all zeros — indistinguishable from a
|
|
500
|
-
layer that is legitimately hidden. Re-querying makes the stale reference
|
|
501
|
-
impossible rather than merely unlikely.
|
|
684
|
+
### Fixed
|
|
502
685
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
over 20ms; `RO synchronous` median 8.4-8.5ms / p90 11.0ms / 3-6 frames over 20ms;
|
|
511
|
-
ZERO frames over 32ms on either, so neither drops one.
|
|
512
|
-
|
|
513
|
-
**BACK-COMPAT.** `--nav-h`, `--nav-bottom` and `--pin-<name>-h` /
|
|
514
|
-
`--pin-<name>-bottom` publish exactly as before, from the same measurements, so
|
|
515
|
-
the 26 consumers across turf-monster, rolio and mcritchie-studio need no change
|
|
516
|
-
and no floor bump. A structural change (a Turbo patch, a fresh document, a layer
|
|
517
|
-
joining or leaving) now forces a full write, because the unchanged-write skip is
|
|
518
|
-
only safe while nothing else touches these properties — the lab caught
|
|
519
|
-
`--nav-h` staying empty through a re-scan on exactly the host-owned-header path
|
|
520
|
-
both live consumers use. The lane contract moves 131 -> 132
|
|
521
|
-
(`config/e2e_lane.yml`), re-derived with the lister.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
- **`Gemfile.lock` resolves solana-studio 0.5.7, and a gate now keeps it there.** The lock had sat on **0.5.3 for four patch releases** while BOTH consumers shipped 0.5.7 (turf-monster `~> 0.5.3`, mcritchie-studio `~> 0.5`). Nothing was red and nothing could have been: engine CI installs with `bundler-cache: true`, so it resolves from the lock and never fresh — the drift does not self-correct and never surfaces as flakiness. It matters because `test/views/style_web3_specimens_test.rb` exists to prove "the style guide renders the REAL gem cards" and reads them off whatever the LOCK resolved; four versions behind, that guard certifies a card no consumer receives. It still passes — only its MEANING changes. MEASURED on this span, the gem's whole `app/` tree was byte-identical 0.5.3 → 0.5.7 (only `CHANGELOG.md`, `README.md` and `version.rb` differ), so this instance cost nothing, which is exactly why it went four releases unnoticed.
|
|
525
|
-
- **The constraint stays `>= 0.5.3`, deliberately.** 0.5.3 is a real FLOOR (0.5.2 shipped the credential partial without `solana_studio/modals/_wallet_connect`, which the web3 capability gate requires). Above it this engine claims no ceiling: it is the BASE half of the base/bolt-on split, and a pessimistic `~> 0.5.3` here would be NARROWER than mcritchie-studio's own `~> 0.5` — a dev-only dependency constraining a resolution it does not own. The defect was the LOCK, not the pin, so the fix is a gate rather than a tighter pin.
|
|
526
|
-
- **`bin/gem-drift-check`** — fails when this engine's lock resolves a tracked gem OLDER than a consumer's, wired into `consumer-ci.yml` after the consumer bundle install. That lane is the only place two repos' lockfiles exist at once (engine at `studio/`, consumer beside it); `test/lib/consumer_ci_shard_contract_test.rb` records the same constraint for its own cross-repo contract. Direction is ONE-WAY on purpose: engine behind FAILS, engine level or ahead passes (the engine is the producer and may test an unreleased gem), and a consumer bundling no tracked gem is a SKIP, not a failure — `mcritchie_industries` is the base half working as designed. Stdlib-only, and it names its one-command remedy (`bundle update solana-studio`) in the failure.
|
|
527
|
-
|
|
528
|
-
- **The auth modal's Solana button moved to solana-studio, behind a CREDENTIAL
|
|
529
|
-
SLOT.** `style/modals/_auth` no longer draws the button; it renders whatever
|
|
530
|
-
resolves at `solana_studio/auth/wallet_credential` and nothing when that path
|
|
531
|
-
is empty, so bundling the gem IS the registration and a web2 app carries no
|
|
532
|
-
wallet markup at all. This completes the two-template split begun in 0.67.0,
|
|
533
|
-
which moved the four web3 MODALS to the gem and left the sign-in button as the
|
|
534
|
-
last web3 markup in the base engine. The gem half has shipped since
|
|
535
|
-
solana-studio **0.5.2** — until now this engine referenced that partial
|
|
536
|
-
nowhere, so the gem shipped a button no host rendered.
|
|
537
|
-
**Supersedes the stranded PR #245**, whose consumer lane was red only because
|
|
538
|
-
it opened before solana-studio 0.5.2 published.
|
|
539
|
-
|
|
540
|
-
**What did NOT change, deliberately:** `_methodDefaults.wallet` is still
|
|
541
|
-
`Studio.auth_method?(:wallet) && Studio.feature?(:web3)`. The Ruby gate
|
|
542
|
-
answers "is it implemented" (is the picker registered, does the credential
|
|
543
|
-
partial resolve) and gates the RENDER; Alpine's `methodOn('wallet')` still
|
|
544
|
-
answers "should it show" and gates VISIBILITY, which keeps the style guide's
|
|
545
|
-
method toggles working. Folding policy into the Ruby gate would delete the
|
|
546
|
-
button from the DOM on a web3-off app that bundles the gem, and the "or"
|
|
547
|
-
divider — which reads `methodOn('wallet')` too — would then float above a
|
|
548
|
-
button that is not there.
|
|
549
|
-
|
|
550
|
-
Hosts need no change: the engine's auth modal is a style-guide specimen
|
|
551
|
-
reached through `/admin/style`, and an app that bundles solana-studio 0.5.2 or
|
|
552
|
-
later sees the same button in the same place.
|
|
553
|
-
|
|
554
|
-
### Fixed
|
|
555
|
-
|
|
556
|
-
- **A host's apostrophe no longer kills the card it was passed to — now across the
|
|
557
|
-
blocks, not just the first-name step.** The same defect
|
|
558
|
-
`onboarding/_first_name` fixed one release ago turns out to be an engine-wide
|
|
559
|
-
idiom: a host-supplied local interpolated into a JS string literal inside a
|
|
560
|
-
JS-evaluating attribute. Repaired at eleven more splices.
|
|
561
|
-
|
|
562
|
-
**WHY IT IS WORTH A CHANGELOG LINE WHEN NOTHING VISIBLE CHANGES.** The failure
|
|
563
|
-
mode is silent. A bare apostrophe closes the JS literal, the whole expression
|
|
564
|
-
becomes a SyntaxError, and Alpine mounts the component as a NO-OP that still
|
|
565
|
-
renders every element — a card that looks perfect and whose buttons do nothing.
|
|
566
|
-
There is no error on screen, nothing in the server log, and no markup assertion
|
|
567
|
-
that can see it. Every value in every consumer resolves to a source literal or a
|
|
568
|
-
frozen constant today, so this is latent cover rather than a live fix; it is
|
|
569
|
-
worth doing because the next local to carry prose will look like an ordinary
|
|
570
|
-
change to whoever writes it.
|
|
571
|
-
|
|
572
|
-
**WHAT MOVED.** `blocks/_success_card` (`cta_event` at both CTA branches,
|
|
573
|
-
`secondary_event`), `blocks/_error_card` (`cta_event`, `secondary_event`),
|
|
574
|
-
`blocks/_entry_confirmed` and `blocks/_solana_tx_link` (`cluster_param`),
|
|
575
|
-
`modals/_crop_photo` (`store`), `studio/emails/show` (the two upload filenames
|
|
576
|
-
and the success sentence), and `profiles/_birthday_fields` (the date value).
|
|
577
|
-
No default or in-repo value contains a character either escaper touches, so
|
|
578
|
-
every shipped card renders byte-for-byte what it did.
|
|
579
|
-
|
|
580
|
-
**THE MECHANISM NOW HAS ONE HOME AND ONE GUARD.** `Studio::JsLiteral.in_attribute`
|
|
581
|
-
replaces the four inline copies in `_first_name`. TWO escapers have to run — one
|
|
582
|
-
for the JS literal, one for the HTML attribute — and the second only runs on a
|
|
583
|
-
value ERB still believes is unsafe, which is why the value is interpolated before
|
|
584
|
-
it is escaped. That subtlety was re-derived at every call site and had no test
|
|
585
|
-
anywhere; deleting it used to leave the suite green.
|
|
586
|
-
|
|
587
|
-
**NOT A FIX FOR IDENTIFIER POSITION, deliberately.** A local spliced in as a bare
|
|
588
|
-
NAME — `$store.<name>.close()` — must be VALIDATED, never escaped, because
|
|
589
|
-
`escape_javascript` also escapes `$` and mangles a legal store name. That fleet
|
|
590
|
-
(about 36 splices across 19 partials) and a third class found alongside it —
|
|
591
|
-
Ruby-ASSEMBLED JS emitted into an attribute, some of it already `html_safe` — are
|
|
592
|
-
scoped OUT of this change and carry their own tickets. The SHAPE of the splice
|
|
593
|
-
decides the repair, never the name of the local.
|
|
594
|
-
|
|
595
|
-
- **`Studio::S3` no longer mistakes a Rails NAMESPACE for a Rails application.**
|
|
596
|
-
`environment` guarded on `defined?(Rails)` and then called `Rails.env`.
|
|
597
|
-
rails-html-sanitizer — a transitive dependency of `action_view`, which arrives
|
|
598
|
-
long before any application does — ships a namespace-only `module Rails` with no
|
|
599
|
-
singleton methods, so that guard reads true and the next call raises
|
|
600
|
-
`NoMethodError: undefined method 'env' for module Rails`. It now asks
|
|
601
|
-
`Rails.respond_to?(:env)`, which is the form `lib/studio.rb` now uses in FOUR
|
|
602
|
-
places: the three it already carried (lines 701, 707 and 742) plus the route
|
|
603
|
-
guard at line 859. **This entry originally called it "the straggler"; that was
|
|
604
|
-
wrong** — three more sites carried the bare form, line 859 among them, and all
|
|
605
|
-
three are swept in *The Rails guard sweep really is finished now* above, in this
|
|
606
|
-
same unreleased block. No shipped app can reach it — every host boots a
|
|
607
|
-
real application — but the engine's own pure-Ruby unit lane can, and it did:
|
|
608
|
-
adding one `require` for a file that needs `action_view` turned an untouched
|
|
609
|
-
`email_catalog_test` red with two errors about email uploads.
|
|
610
|
-
|
|
611
|
-
- **The style guide's two "Sign Wallet" thumbnails no longer crown themselves
|
|
612
|
-
with a padlock the card stopped drawing, and the guide's lock no longer trails
|
|
613
|
-
the release that removed it.** solana-studio 0.6.1 replaced the step-up card's
|
|
614
|
-
padlock with the remembered wallet's own brand mark, falling back to a neutral
|
|
615
|
-
billfold where no brand is remembered. Both hand-drawn thumbnails in
|
|
616
|
-
`style/_modals` went on drawing the padlock, so the first thing a designer
|
|
617
|
-
reads advertised a glyph the design system had retired.
|
|
618
|
-
|
|
619
|
-
**THE THUMBNAILS WERE HALF OF IT.** The opened specimen renders the REAL
|
|
620
|
-
shared partial, but it does so through whatever `Gemfile.lock` resolves — so
|
|
621
|
-
it is only ever as current as the lock, and the lock sat on 0.6.0 while
|
|
622
|
-
turf-monster shipped 0.6.1. The guide's opened card was still drawing the
|
|
623
|
-
padlock too, which `bin/gem-drift-check` had already been failing on
|
|
624
|
-
(`engine 0.6.0 TRAILS turf-monster 0.6.1`). Correcting the sketches alone
|
|
625
|
-
would have put a brand mark on the thumbnail and a padlock in the card it
|
|
626
|
-
opens — moving the contradiction onto one screen rather than removing it. So
|
|
627
|
-
the lock had to move too, and it has: `accepted` carried the engine to
|
|
628
|
-
solana-studio 0.7.0 while this change sat in review, which is past the 0.6.1
|
|
629
|
-
that removed the padlock. This change no longer moves the lock itself — it
|
|
630
|
-
defers to the line `accepted` already holds — so the sketch and the card it
|
|
631
|
-
opens agree without it.
|
|
632
|
-
|
|
633
|
-
The sketches follow the card: a centered brand tile where a wallet is
|
|
634
|
-
remembered, the card's own billfold outline in an inset square where none is.
|
|
635
|
-
They stay SKETCHES rather than real brand marks for a mechanical reason worth
|
|
636
|
-
recording — every `se-wallet` symbol on the guide is defined inside a
|
|
637
|
-
`template x-if`, whose content is inert until Alpine clones it, so a `use`
|
|
638
|
-
placed outside one resolves to nothing and paints an empty box.
|
|
639
|
-
|
|
640
|
-
Guarded by `test/views/step_up_specimen_thumbnails_test.rb`, which slices each
|
|
641
|
-
thumbnail by a `data-test` hook before asserting. That slicing is the point:
|
|
642
|
-
the rendered guide already carried 17 `se-wallet-` hits from the picker and
|
|
643
|
-
the card's own CTA, so a page-wide assertion for the mark could not fail, and
|
|
644
|
-
a page-wide assertion against the padlock could not pass — the drag-board
|
|
645
|
-
specimen's prose legitimately contains one. The padlock is pinned in BOTH the
|
|
646
|
-
codepoint and the HTML-entity form. MEASURED: five mutants, five killed —
|
|
647
|
-
the padlock restored in either form, either header mark removed, and the lock
|
|
648
|
-
reverted to 0.6.0 each reddens the assertion that owns it.
|
|
649
|
-
|
|
650
|
-
- **The style guide's page-scoped modal store now resolves animations through
|
|
651
|
-
the LIVE registry, so the guide can no longer disagree with itself.**
|
|
652
|
-
`style/_modals`' `dsModals` carried a hard-coded COPY of the animation table
|
|
653
|
-
while the new simulator builds its controls from `window.ModalAnimations`.
|
|
654
|
-
The two agreed today, which is exactly why this was invisible — but a
|
|
655
|
-
consumer registering a key would have grown a control from the registry that
|
|
656
|
-
the store then resolved back to `pop`: the button said "shake", the card
|
|
657
|
-
popped, and nothing reported the gap. Its `modalAnim` now reads
|
|
658
|
-
`window.ModalAnimations` at CALL time with the same late-binding guard the
|
|
659
|
-
shared host uses (unknown keys and a gutted registry still fall back to
|
|
660
|
-
`pop`, so a miss can never throw on `.ms` and strand a modal open); the local
|
|
661
|
-
table remains only as the fallback for a guide rendered without the host.
|
|
662
|
-
MEASURED in a browser both ways: registering a new key at runtime surfaces a
|
|
663
|
-
control AND plays it, and with this fix reverted the same key surfaces a
|
|
664
|
-
control that plays `modal-card-mount`.
|
|
665
|
-
|
|
666
|
-
- **Onboarding no longer truncates a surname to fit a first name's cap.**
|
|
667
|
-
`Studio::OnboardingController#first_name` measured the WHOLE typed answer with
|
|
668
|
-
`Studio::FIRST_NAME_MAX_LENGTH` — the PER-FIELD cap, 40. The field asks for a
|
|
669
|
-
name and people type a full one, so "Bartholomew Fitzwilliam
|
|
670
|
-
Montgomery-Smythe" (41 characters) stored 40 of them and handed the account
|
|
671
|
-
back its own surname misspelled, **"Montgomery-Smyth"**, on every surface
|
|
672
|
-
`name` feeds.
|
|
686
|
+
- **Onboarding no longer truncates a surname to fit a first name's cap.**
|
|
687
|
+
`Studio::OnboardingController#first_name` measured the WHOLE typed answer with
|
|
688
|
+
`Studio::FIRST_NAME_MAX_LENGTH` — the PER-FIELD cap, 40. The field asks for a
|
|
689
|
+
name and people type a full one, so "Bartholomew Fitzwilliam
|
|
690
|
+
Montgomery-Smythe" (41 characters) stored 40 of them and handed the account
|
|
691
|
+
back its own surname misspelled, **"Montgomery-Smyth"**, on every surface
|
|
692
|
+
`name` feeds.
|
|
673
693
|
|
|
674
694
|
**ONE CONSTANT WAS ANSWERING TWO QUESTIONS**, which is why neither call site
|
|
675
695
|
looked wrong. `/profile` applies the same number PER FIELD (`normalized_name`,
|
|
@@ -714,6 +734,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
714
734
|
a row whose halves disagree with its `name` because of a `/profile` edit must
|
|
715
735
|
never be auto-repaired anyway.
|
|
716
736
|
|
|
737
|
+
## 0.69.4 — 2026-09-05
|
|
738
|
+
|
|
739
|
+
### Fixed
|
|
740
|
+
|
|
717
741
|
- **Onboarding no longer stores a two-word answer as the whole first name.**
|
|
718
742
|
`Studio::OnboardingController#first_name` writes with `update_columns`, which
|
|
719
743
|
skips callbacks — so the host's `before_save :set_name_parts` never ran and
|
|
@@ -748,6 +772,69 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
748
772
|
and a fossil repair belongs to the consuming app that owns its `users` table,
|
|
749
773
|
not to the gem.
|
|
750
774
|
|
|
775
|
+
## 0.69.1 — 2026-09-02
|
|
776
|
+
|
|
777
|
+
### Changed
|
|
778
|
+
|
|
779
|
+
- **`Gemfile.lock` resolves solana-studio 0.5.7, and a gate now keeps it there.** The lock had sat on **0.5.3 for four patch releases** while BOTH consumers shipped 0.5.7 (turf-monster `~> 0.5.3`, mcritchie-studio `~> 0.5`). Nothing was red and nothing could have been: engine CI installs with `bundler-cache: true`, so it resolves from the lock and never fresh — the drift does not self-correct and never surfaces as flakiness. It matters because `test/views/style_web3_specimens_test.rb` exists to prove "the style guide renders the REAL gem cards" and reads them off whatever the LOCK resolved; four versions behind, that guard certifies a card no consumer receives. It still passes — only its MEANING changes. MEASURED on this span, the gem's whole `app/` tree was byte-identical 0.5.3 → 0.5.7 (only `CHANGELOG.md`, `README.md` and `version.rb` differ), so this instance cost nothing, which is exactly why it went four releases unnoticed.
|
|
780
|
+
|
|
781
|
+
- **The constraint stays `>= 0.5.3`, deliberately.** 0.5.3 is a real FLOOR (0.5.2 shipped the credential partial without `solana_studio/modals/_wallet_connect`, which the web3 capability gate requires). Above it this engine claims no ceiling: it is the BASE half of the base/bolt-on split, and a pessimistic `~> 0.5.3` here would be NARROWER than mcritchie-studio's own `~> 0.5` — a dev-only dependency constraining a resolution it does not own. The defect was the LOCK, not the pin, so the fix is a gate rather than a tighter pin.
|
|
782
|
+
|
|
783
|
+
- **`bin/gem-drift-check`** — fails when this engine's lock resolves a tracked gem OLDER than a consumer's, wired into `consumer-ci.yml` after the consumer bundle install. That lane is the only place two repos' lockfiles exist at once (engine at `studio/`, consumer beside it); `test/lib/consumer_ci_shard_contract_test.rb` records the same constraint for its own cross-repo contract. Direction is ONE-WAY on purpose: engine behind FAILS, engine level or ahead passes (the engine is the producer and may test an unreleased gem), and a consumer bundling no tracked gem is a SKIP, not a failure — `mcritchie_industries` is the base half working as designed. Stdlib-only, and it names its one-command remedy (`bundle update solana-studio`) in the failure.
|
|
784
|
+
|
|
785
|
+
## 0.69.0 — 2026-09-02
|
|
786
|
+
|
|
787
|
+
### Added
|
|
788
|
+
|
|
789
|
+
- **Knowledge coverage view** — `/admin/knowledge/coverage` +
|
|
790
|
+
`Studio::KnowledgeExpectation`: the "what SHOULD exist" half of the
|
|
791
|
+
knowledge layer. Expectations carry an entity, folder, provenance
|
|
792
|
+
(`source_note`, e.g. "diligence tracker item 14"), and a cadence — `once`
|
|
793
|
+
(an LOI) or `monthly` (aging inventory), where each calendar month from
|
|
794
|
+
`start_on` is a slot and the view names missing months outright
|
|
795
|
+
(`2026-09 MISSING`). Documents fulfill expectations by an EXPLICIT
|
|
796
|
+
`expectation_id` set at triage (the show page grows a "fulfills expectation"
|
|
797
|
+
select; a fuzzy name-match would silently merge lookalikes, an id never
|
|
798
|
+
does), and superseded documents never fill a slot. Ships two reference
|
|
799
|
+
migrations (`create_studio_knowledge_expectations`,
|
|
800
|
+
`add_expectation_to_studio_knowledge_docs`); routes ride the existing
|
|
801
|
+
`Studio.draw_knowledge_routes` opt-in. Built for the Commercial Welding
|
|
802
|
+
65-item diligence tracker; app-agnostic like the rest of the layer.
|
|
803
|
+
|
|
804
|
+
## 0.68.0 — 2026-09-01
|
|
805
|
+
|
|
806
|
+
### Changed
|
|
807
|
+
|
|
808
|
+
- **The auth modal's Solana button moved to solana-studio, behind a CREDENTIAL
|
|
809
|
+
SLOT.** `style/modals/_auth` no longer draws the button; it renders whatever
|
|
810
|
+
resolves at `solana_studio/auth/wallet_credential` and nothing when that path
|
|
811
|
+
is empty, so bundling the gem IS the registration and a web2 app carries no
|
|
812
|
+
wallet markup at all. This completes the two-template split begun in 0.67.0,
|
|
813
|
+
which moved the four web3 MODALS to the gem and left the sign-in button as the
|
|
814
|
+
last web3 markup in the base engine. The gem half has shipped since
|
|
815
|
+
solana-studio **0.5.2** — until now this engine referenced that partial
|
|
816
|
+
nowhere, so the gem shipped a button no host rendered.
|
|
817
|
+
**Supersedes the stranded PR #245**, whose consumer lane was red only because
|
|
818
|
+
it opened before solana-studio 0.5.2 published.
|
|
819
|
+
|
|
820
|
+
**What did NOT change, deliberately:** `_methodDefaults.wallet` is still
|
|
821
|
+
`Studio.auth_method?(:wallet) && Studio.feature?(:web3)`. The Ruby gate
|
|
822
|
+
answers "is it implemented" (is the picker registered, does the credential
|
|
823
|
+
partial resolve) and gates the RENDER; Alpine's `methodOn('wallet')` still
|
|
824
|
+
answers "should it show" and gates VISIBILITY, which keeps the style guide's
|
|
825
|
+
method toggles working. Folding policy into the Ruby gate would delete the
|
|
826
|
+
button from the DOM on a web3-off app that bundles the gem, and the "or"
|
|
827
|
+
divider — which reads `methodOn('wallet')` too — would then float above a
|
|
828
|
+
button that is not there.
|
|
829
|
+
|
|
830
|
+
Hosts need no change: the engine's auth modal is a style-guide specimen
|
|
831
|
+
reached through `/admin/style`, and an app that bundles solana-studio 0.5.2 or
|
|
832
|
+
later sees the same button in the same place.
|
|
833
|
+
|
|
834
|
+
## 0.67.2 — 2026-09-01
|
|
835
|
+
|
|
836
|
+
### Fixed
|
|
837
|
+
|
|
751
838
|
- **Knowledge layer hardening** — the five findings from the 0.67.0 reviews:
|
|
752
839
|
upload keys carry a random suffix (same-named uploads in the same second no
|
|
753
840
|
longer overwrite the first object before the unique index can refuse);
|
|
@@ -761,6 +848,8 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
761
848
|
noted in the migration header (consumer copies are linted by the consumer's
|
|
762
849
|
rubocop — measured on the Industries adoption ship).
|
|
763
850
|
|
|
851
|
+
## 0.67.0 — 2026-09-01
|
|
852
|
+
|
|
764
853
|
### Added
|
|
765
854
|
|
|
766
855
|
- **Knowledge layer primitive** — `Studio::KnowledgeDoc` + `/admin/knowledge`:
|
|
@@ -787,6 +876,62 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
787
876
|
Deferred to a later cut, recorded on the task: coverage view against a
|
|
788
877
|
diligence tracker, recurring-series tracking, per-folder access defaults.
|
|
789
878
|
|
|
879
|
+
## 0.66.0 — 2026-08-31
|
|
880
|
+
|
|
881
|
+
### Changed
|
|
882
|
+
|
|
883
|
+
- **The base template is web2: `:wallet` has left the `auth_methods` default.**
|
|
884
|
+
`Studio.auth_methods` defaulted to `%i[magic_link google wallet]` while
|
|
885
|
+
`Studio.features` defaulted to `[]`. Those two defaults disagree, and the
|
|
886
|
+
disagreement was load-bearing: the auth modal renders its wallet button from
|
|
887
|
+
`auth_method?(:wallet) && feature?(:web3)`, so a stock app rendered **no**
|
|
888
|
+
wallet button — while `Studio.routes` draws the Solana trio from
|
|
889
|
+
`auth_method?(:wallet)` alone, so the same stock app published
|
|
890
|
+
`GET /auth/solana/nonce`, `POST /auth/solana/verify` and
|
|
891
|
+
`GET /auth/phantom/callback`. All three `skip_before_action
|
|
892
|
+
:require_authentication`, and `#verify` lands in `User.from_solana_wallet`,
|
|
893
|
+
which `validate_user_contract!` never requires a host to implement. A brand-new
|
|
894
|
+
newsletter app inherited three public endpoints into an unvalidated contract.
|
|
895
|
+
|
|
896
|
+
studio-engine + McRitchie Studio is the base template for **every** app, web2
|
|
897
|
+
and web3 alike; solana-studio + Turf Monster is the web3 bolt-on. Most apps are
|
|
898
|
+
web2, so the default now says so: `%i[magic_link google]`. `README.md` and
|
|
899
|
+
`docs/NEW_APP_SETUP.md` have always printed that exact line as the new-app
|
|
900
|
+
configuration — the code simply disagreed with the docs it shipped.
|
|
901
|
+
|
|
902
|
+
**No current consumer changes behaviour**, because all three declare
|
|
903
|
+
`auth_methods` explicitly: McRitchie Studio (`magic_link google wallet`),
|
|
904
|
+
Turf Monster (`magic_link google wallet`, and `draw_auth_routes = false`
|
|
905
|
+
besides, so the engine draws neither group for it), mcritchie-industries
|
|
906
|
+
(`magic_link`). A wallet app opts in to both knobs — `auth_methods` including
|
|
907
|
+
`:wallet` **and** `features` including `:web3`, the second being what makes the
|
|
908
|
+
button appear.
|
|
909
|
+
|
|
910
|
+
- **The auth route gate is pinned, and the gate choice is written down.**
|
|
911
|
+
Nothing tested which auth routes `Studio.routes` draws, on an engine whose
|
|
912
|
+
route changes break consumers at **boot** rather than at test time.
|
|
913
|
+
`test/integration/auth_route_gating_test.rb` draws the real host route table
|
|
914
|
+
under each consumer's configuration and reads the drawn routes back, so a
|
|
915
|
+
comment naming `auth/solana/nonce` cannot satisfy it.
|
|
916
|
+
|
|
917
|
+
The gate stays `draw_auth_routes && auth_method?(:wallet)` — **not** `&&
|
|
918
|
+
feature?(:web3)`, even though the modal uses both. `auth_methods` says which
|
|
919
|
+
CREDENTIALS an app accepts and these three paths are the credential exchange
|
|
920
|
+
itself; `features` gates product surfaces. `phantom_callback` is the mobile
|
|
921
|
+
deep-link RETURN url, so an app that declared `:wallet` and forgot `:web3`
|
|
922
|
+
would dead-end the handshake inside the user's wallet app with no server-side
|
|
923
|
+
trace — a worse failure than an endpoint that draws while the UI hides its
|
|
924
|
+
button. That decision is now a test with its rationale attached, so adding the
|
|
925
|
+
feature check flips a red assertion instead of silently unpublishing a live
|
|
926
|
+
consumer's routes. `Studio.draw_auth_routes`' comment likewise now records that
|
|
927
|
+
it is the OUTER switch, not the only gate: each group carries its own
|
|
928
|
+
`auth_methods` sub-gate, which is how a web2 app keeps magic-link while drawing
|
|
929
|
+
no `/auth/solana/*`.
|
|
930
|
+
|
|
931
|
+
## 0.65.3 — 2026-08-31
|
|
932
|
+
|
|
933
|
+
### Added
|
|
934
|
+
|
|
790
935
|
- **A green comment-leak scan used to mean "not looked at".**
|
|
791
936
|
`test/views/erb_comment_leak_test.rb` guards the ERB comment form in
|
|
792
937
|
`app/views/**/*.erb`. It never looked inside `<script>`, and that is where this
|
|
@@ -808,22 +953,81 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
808
953
|
line in an HTML comment does not help — ERB runs inside those too. Describing the
|
|
809
954
|
tag in words stays legal, exactly as it is for ERB comments.
|
|
810
955
|
|
|
811
|
-
**Finding a comment is the hard half, and the first cut of this got it wrong.**
|
|
812
|
-
Anchoring `<script>` blocks on raw source let an ERB comment's *prose mention* of
|
|
813
|
-
a script tag open the block: `studio/_board_assets.html.erb` reported 19_512
|
|
814
|
-
bytes of markup as JavaScript and never saw its real program on its own terms.
|
|
815
|
-
Blocks are now anchored on a copy with ERB tags and HTML comments blanked out,
|
|
816
|
-
length preserved so offsets and line numbers still land. Inside a block the
|
|
817
|
-
walker tracks the states that can hide a comment opener — a string
|
|
818
|
-
(`"https://…"` is not a comment), a template literal, a regex (`/\/\//`) — and a
|
|
819
|
-
literal or comment that never closes is REPORTED rather than absorbed, because a
|
|
820
|
-
walker that quietly lost its place is the same green as a clean file. The
|
|
821
|
-
assertions check the input as well as the verdict: how many blocks, comments and
|
|
822
|
-
bytes the scan actually reached, and that every block it opened matches a real
|
|
823
|
-
closing tag.
|
|
956
|
+
**Finding a comment is the hard half, and the first cut of this got it wrong.**
|
|
957
|
+
Anchoring `<script>` blocks on raw source let an ERB comment's *prose mention* of
|
|
958
|
+
a script tag open the block: `studio/_board_assets.html.erb` reported 19_512
|
|
959
|
+
bytes of markup as JavaScript and never saw its real program on its own terms.
|
|
960
|
+
Blocks are now anchored on a copy with ERB tags and HTML comments blanked out,
|
|
961
|
+
length preserved so offsets and line numbers still land. Inside a block the
|
|
962
|
+
walker tracks the states that can hide a comment opener — a string
|
|
963
|
+
(`"https://…"` is not a comment), a template literal, a regex (`/\/\//`) — and a
|
|
964
|
+
literal or comment that never closes is REPORTED rather than absorbed, because a
|
|
965
|
+
walker that quietly lost its place is the same green as a clean file. The
|
|
966
|
+
assertions check the input as well as the verdict: how many blocks, comments and
|
|
967
|
+
bytes the scan actually reached, and that every block it opened matches a real
|
|
968
|
+
closing tag.
|
|
969
|
+
|
|
970
|
+
Zero findings across the engine at `origin/accepted`, so this lands green with no
|
|
971
|
+
allowlist.
|
|
972
|
+
|
|
973
|
+
## 0.65.2 — 2026-08-28
|
|
974
|
+
|
|
975
|
+
### Fixed
|
|
976
|
+
|
|
977
|
+
- **A toast with buttons could not be dismissed while a modal was open.** The
|
|
978
|
+
layer scale put `--z-banner` (500) above `--z-toast` (400), and `body.modal-open`
|
|
979
|
+
lifts the environment bar stack to that tier at `position: sticky; top: 0`.
|
|
980
|
+
`#toast-container` is fixed at top 0 with `1rem` of padding, so the two land on
|
|
981
|
+
the same pixels: measured in a browser, the bars own y0-47 while the toast's
|
|
982
|
+
Dismiss button runs y28-40 — an 8-14px overlap at every viewport tested.
|
|
983
|
+
`elementFromPoint` at that button returned the **banner**, and a real click left
|
|
984
|
+
the toast on the page. Because the toast manager gives any toast carrying
|
|
985
|
+
buttons `duration: 0`, that X was the toast's **only** exit: it was stuck for
|
|
986
|
+
the rest of the session.
|
|
987
|
+
|
|
988
|
+
**The tiers are reordered** — `--z-banner` 400, `--z-toast-blur` 499, `--z-toast`
|
|
989
|
+
500 — and that is a semantic decision rather than a nudge around the geometry: a
|
|
990
|
+
toast is transient and demands interaction, a banner is persistent chrome that
|
|
991
|
+
will still be there afterwards. `--z-banner` still clears `--z-modal` (200), so
|
|
992
|
+
the property the lift exists for — DEV MODE and the email chip stay lit and
|
|
993
|
+
clickable over a modal — is unchanged and now has its own spec.
|
|
994
|
+
|
|
995
|
+
**One measured cost.** A banner button's tooltip is a descendant of the bar
|
|
996
|
+
stack, and the modal-open lift makes that stack a stacking context, so the
|
|
997
|
+
tooltip composites at `--z-banner` rather than at its own `--z-tooltip` (600).
|
|
998
|
+
At 1440x900 the tooltip and the toast card never meet; at 390x844 they overlap
|
|
999
|
+
by 260x45px, so a banner tooltip opened with both a modal and a toast up is
|
|
1000
|
+
drawn under the toast. Hover/focus-only, purely visual, and recorded beside the
|
|
1001
|
+
scale.
|
|
1002
|
+
|
|
1003
|
+
**Consumers that redefine these tokens in their own `:root` after the engine
|
|
1004
|
+
import still win**, as they always have, so an app carrying a local copy of the
|
|
1005
|
+
scale keeps the old order (and the bug) until it drops the copy.
|
|
1006
|
+
|
|
1007
|
+
Two literals moved with the tiers: `#toast-container` and `.toast-page-blur` in
|
|
1008
|
+
`layouts/studio/_flash` carry `var(--studio-toast-z, var(--z-toast, …))`
|
|
1009
|
+
fallbacks, and a fallback that disagreed with the scale would hand the bug to
|
|
1010
|
+
any app rendering the partial without the engine sheet.
|
|
1011
|
+
|
|
1012
|
+
**The existing test could not have caught this and still cannot on its own.**
|
|
1013
|
+
`layer_scale_contract_test` asserted `--z-toast > --z-modal`, which was true the
|
|
1014
|
+
entire time the toast was unusable — both tiers cleared the modal, which says
|
|
1015
|
+
nothing about which of *them* wins. It now names the banner, and pins the halo
|
|
1016
|
+
directly beneath its own toast so no tier can settle between them. The property
|
|
1017
|
+
is also asserted where the defect lives, in `e2e/toast_over_banner.spec.js`: a
|
|
1018
|
+
hit test and a real mouse click at the Dismiss button, at desktop and phone
|
|
1019
|
+
widths, at scroll-top and at an offset. And once more on the artifact a
|
|
1020
|
+
consumer is actually served — `test/integration/layer_scale_build_test.rb`
|
|
1021
|
+
runs the real Tailwind binary over the engine's entry point and reads the
|
|
1022
|
+
tiers out of the COMPILED bundle, where `@import` resolution, layer ordering
|
|
1023
|
+
and a shadowing `:root` are all in play and a source read sees none of them. One trap is recorded there — at 390px
|
|
1024
|
+
the point under that button is the banner's Email link, so on the broken build a
|
|
1025
|
+
click *navigated* and the toast count on the new page was zero; asserting the
|
|
1026
|
+
count alone passes over the bug.
|
|
1027
|
+
|
|
1028
|
+
## 0.65.0 — 2026-08-28
|
|
824
1029
|
|
|
825
|
-
|
|
826
|
-
allowlist.
|
|
1030
|
+
### Added
|
|
827
1031
|
|
|
828
1032
|
- **`data-pin` — the pinned stack publishes itself.** `--nav-h` / `--nav-bottom`
|
|
829
1033
|
answer for one element. Everything else that pins has been re-deriving the
|
|
@@ -885,56 +1089,28 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
885
1089
|
read them, and `nav_offset_contract_test` still pins both to their exact
|
|
886
1090
|
sources. This is additive.
|
|
887
1091
|
|
|
1092
|
+
## 0.63.0 — 2026-08-27
|
|
888
1093
|
|
|
889
|
-
###
|
|
890
|
-
|
|
891
|
-
- **The base template is web2: `:wallet` has left the `auth_methods` default.**
|
|
892
|
-
`Studio.auth_methods` defaulted to `%i[magic_link google wallet]` while
|
|
893
|
-
`Studio.features` defaulted to `[]`. Those two defaults disagree, and the
|
|
894
|
-
disagreement was load-bearing: the auth modal renders its wallet button from
|
|
895
|
-
`auth_method?(:wallet) && feature?(:web3)`, so a stock app rendered **no**
|
|
896
|
-
wallet button — while `Studio.routes` draws the Solana trio from
|
|
897
|
-
`auth_method?(:wallet)` alone, so the same stock app published
|
|
898
|
-
`GET /auth/solana/nonce`, `POST /auth/solana/verify` and
|
|
899
|
-
`GET /auth/phantom/callback`. All three `skip_before_action
|
|
900
|
-
:require_authentication`, and `#verify` lands in `User.from_solana_wallet`,
|
|
901
|
-
which `validate_user_contract!` never requires a host to implement. A brand-new
|
|
902
|
-
newsletter app inherited three public endpoints into an unvalidated contract.
|
|
903
|
-
|
|
904
|
-
studio-engine + McRitchie Studio is the base template for **every** app, web2
|
|
905
|
-
and web3 alike; solana-studio + Turf Monster is the web3 bolt-on. Most apps are
|
|
906
|
-
web2, so the default now says so: `%i[magic_link google]`. `README.md` and
|
|
907
|
-
`docs/NEW_APP_SETUP.md` have always printed that exact line as the new-app
|
|
908
|
-
configuration — the code simply disagreed with the docs it shipped.
|
|
1094
|
+
### Added
|
|
909
1095
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1096
|
+
- **`studio/modals/_wallet_connect` — the Connect Wallet picker, engine-owned.**
|
|
1097
|
+
The reown-style wallet chooser existed three times before this — turf-monster's
|
|
1098
|
+
226 lines, mcritchie-studio's 107 and the style guide's 176 — sharing no code.
|
|
1099
|
+
It is one partial now, and `style/modals/_wallet_connect` CONFIGURES it rather
|
|
1100
|
+
than porting it, so the specimen and production cannot drift again. Locals:
|
|
1101
|
+
`store` (default `"modals"`), `connect_fn` (default `"solanaConnectAndVerify"`),
|
|
1102
|
+
`title`, `extra_data` (extra x-data members as a brace-less JS fragment), plus a
|
|
1103
|
+
BLOCK for the pre-connect slot. App behaviour arrives as optional hook METHODS
|
|
1104
|
+
defined in `extra_data`, each called only if it exists: `onInit`, `canPick`,
|
|
1105
|
+
`verifyArgs`, `onConnected(result)`, `onDeepLink`, `onBack`. Carries the mobile
|
|
1106
|
+
Phantom single-row fix (one Phantom row in every state) and a `role="alert"` on
|
|
1107
|
+
the connect error that both app copies lacked.
|
|
917
1108
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
`test/integration/auth_route_gating_test.rb` draws the real host route table
|
|
922
|
-
under each consumer's configuration and reads the drawn routes back, so a
|
|
923
|
-
comment naming `auth/solana/nonce` cannot satisfy it.
|
|
1109
|
+
**NO SHIPPING CONSUMER RENDERS IT YET.** Both apps still ship their own copy and
|
|
1110
|
+
adopt this one after it releases, so the green consumer CI here says they still
|
|
1111
|
+
compile — it says nothing about this partial.
|
|
924
1112
|
|
|
925
|
-
|
|
926
|
-
feature?(:web3)`, even though the modal uses both. `auth_methods` says which
|
|
927
|
-
CREDENTIALS an app accepts and these three paths are the credential exchange
|
|
928
|
-
itself; `features` gates product surfaces. `phantom_callback` is the mobile
|
|
929
|
-
deep-link RETURN url, so an app that declared `:wallet` and forgot `:web3`
|
|
930
|
-
would dead-end the handshake inside the user's wallet app with no server-side
|
|
931
|
-
trace — a worse failure than an endpoint that draws while the UI hides its
|
|
932
|
-
button. That decision is now a test with its rationale attached, so adding the
|
|
933
|
-
feature check flips a red assertion instead of silently unpublishing a live
|
|
934
|
-
consumer's routes. `Studio.draw_auth_routes`' comment likewise now records that
|
|
935
|
-
it is the OUTER switch, not the only gate: each group carries its own
|
|
936
|
-
`auth_methods` sub-gate, which is how a web2 app keeps magic-link while drawing
|
|
937
|
-
no `/auth/solana/*`.
|
|
1113
|
+
### Changed
|
|
938
1114
|
|
|
939
1115
|
- **The navbar collapse primitive is broadcastable, rate-limited, and cheap per
|
|
940
1116
|
frame.** Three defects in the primitive as first landed, none of which a
|
|
@@ -985,60 +1161,103 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
985
1161
|
so the source-swap mutation it was written for still dies, and it gained a
|
|
986
1162
|
guard on the read-before-write order.
|
|
987
1163
|
|
|
1164
|
+
## 0.62.6 — 2026-08-27
|
|
1165
|
+
|
|
1166
|
+
### Added
|
|
1167
|
+
|
|
1168
|
+
- **`--nav-p` and `navCollapse()` — the navbar collapse becomes a primitive.**
|
|
1169
|
+
`layouts/studio/_head` now publishes a third header property beside `--nav-h`
|
|
1170
|
+
and `--nav-bottom`. The first two are measurements OF the header; `--nav-p` is
|
|
1171
|
+
an input TO it: collapse progress, `0` expanded to `1` collapsed, written on
|
|
1172
|
+
the `<header>` once per animation frame from `window.scrollY`, and registered
|
|
1173
|
+
with `@property` as a `<number>` so it is legal inside `calc()`, real before
|
|
1174
|
+
the first scroll, and interpolable.
|
|
1175
|
+
|
|
1176
|
+
**It ships no sizing opinion.** A host adopts it by putting `nav-shell` and
|
|
1177
|
+
`x-data="navCollapse()"` on its header, giving each breakpoint band a
|
|
1178
|
+
`--nav-ramp`, and writing its collapsing dimensions as `calc()`s off
|
|
1179
|
+
`--nav-p`. An app whose navbar collapses to different endpoints than this
|
|
1180
|
+
gem's — `mcritchie-studio` goes `w-8 → w-5`, not `w-12 → w-8` — can adopt the
|
|
1181
|
+
mechanism without changing its endpoints.
|
|
1182
|
+
|
|
1183
|
+
**NO SHIPPING CONSUMER EXERCISES THIS YET, and the adoptions are follow-ons.**
|
|
1184
|
+
`turf-monster` overrides `layouts/_navbar.html.erb` AND redefines
|
|
1185
|
+
`window.navCollapse` in `shared/_alpine_factories.html.erb`, rendered after the
|
|
1186
|
+
engine head, so the engine's copy is shadowed. `mcritchie-studio` never renders
|
|
1187
|
+
`layouts/navbar` at all — its inline header still carries `@scroll.window` and
|
|
1188
|
+
`transition-all duration-300`. Said plainly because it also means the green
|
|
1189
|
+
consumer CI on this change is **not evidence about this code path**: both apps
|
|
1190
|
+
compile and pass against a primitive neither of them runs.
|
|
988
1191
|
|
|
989
1192
|
### Fixed
|
|
990
1193
|
|
|
991
|
-
- **
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1194
|
+
- **The engine navbar's collapse tracks the finger instead of a clock.**
|
|
1195
|
+
`layouts/_navbar` flipped `scrolled` at `scrollY > 60` and fed that step into
|
|
1196
|
+
`transition-all duration-300` on padding, logo `width`/`height` and two font
|
|
1197
|
+
sizes. The finger set the step; an ease curve owned everything after it.
|
|
1198
|
+
Measured in `turf-monster` at 390×844 on the same code before the change: the
|
|
1199
|
+
header ran 178px → 139px and **34 of those 39px of document reflow landed
|
|
1200
|
+
AFTER the scroll had stopped, over 232ms**, at up to 3px per frame — plus a
|
|
1201
|
+
1px REVERSE lurch in the frame the class flipped, where a discrete
|
|
1202
|
+
`text-3xl → text-xl` swap collided with the stylesheet's own
|
|
1203
|
+
`transition: font-size`.
|
|
1001
1204
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
the property the lift exists for — DEV MODE and the email chip stay lit and
|
|
1007
|
-
clickable over a modal — is unchanged and now has its own spec.
|
|
1205
|
+
Every collapsing dimension is now a `calc()` off `--nav-p`, and nothing on the
|
|
1206
|
+
path carries a time-based transition. **Both endpoints are unchanged at every
|
|
1207
|
+
breakpoint** — this is the same navbar, re-plumbed, so a consumer sees no
|
|
1208
|
+
visual difference at rest.
|
|
1008
1209
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1210
|
+
Content still accelerates *during* the collapse: a sticky header is in flow,
|
|
1211
|
+
so shrinking it adds to the scroll and content necessarily outruns the finger.
|
|
1212
|
+
That is the point of a collapse, not a defect. What is tunable is the shape,
|
|
1213
|
+
and `--nav-ramp` is 3× the band's collapse total with a smoothstep ease, whose
|
|
1214
|
+
slope is zero at both ends — so the burst leaves 1×, peaks near 1.5×, and
|
|
1215
|
+
returns to 1× with no velocity step. A linear ramp equal to the collapse hits
|
|
1216
|
+
2× and steps straight back.
|
|
1016
1217
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1218
|
+
Also included: a passive, rAF-coalesced listener that writes to the header
|
|
1219
|
+
rather than `:root` (keeping each frame's style recalc inside the navbar
|
|
1220
|
+
subtree); a short-page guard, because collapsing shortens the document and on
|
|
1221
|
+
a barely-scrollable page that deletes the very scroll room that triggered it,
|
|
1222
|
+
clamps `scrollY` to 0, and flaps forever; and a `prefers-reduced-motion`
|
|
1223
|
+
branch that snaps `--nav-p` to `0`/`1`, since resizing type under a moving
|
|
1224
|
+
finger is itself motion worth dropping.
|
|
1020
1225
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1226
|
+
`/navbar`'s Scrolled toggle drops **twelve `!important` rules** that restated
|
|
1227
|
+
every collapsed value — it sets `--nav-p: 0|1` and transitions the registered
|
|
1228
|
+
property, so the preview exercises the shipped `calc()`s instead of a parallel
|
|
1229
|
+
copy of them.
|
|
1025
1230
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1231
|
+
## 0.61.2 — 2026-08-26
|
|
1232
|
+
|
|
1233
|
+
### Fixed
|
|
1234
|
+
|
|
1235
|
+
- **The age gate's back link now returns the date the person already entered.**
|
|
1236
|
+
`blocks/_age_gate`'s "Update your Birthday" swapped back to `blocks/_birthday`
|
|
1237
|
+
with an EMPTY props object, discarding the `dobYear`/`dobMonth`/`dobDay` parts
|
|
1238
|
+
the birthday factory had just handed across the store — and the factory started
|
|
1239
|
+
its three fields at `""` and never read them anyway. So the correction path came
|
|
1240
|
+
back BLANK: a mistyped year cost all three picks, on a card whose own header
|
|
1241
|
+
comment promised "a correction, not a restart".
|
|
1242
|
+
|
|
1243
|
+
Both sides of the seam moved. `back()` forwards the three date parts — and only
|
|
1244
|
+
those three, deliberately: `minAge`, `state` and `message` describe the refusal,
|
|
1245
|
+
and `validates` is load-bearing by its ABSENCE (the style guide specimen reads an
|
|
1246
|
+
absent prop as validating, and a forwarded stale one would let the refusal pick
|
|
1247
|
+
the next card's mode). `window.birthdayModal` gained an `init` that re-picks the
|
|
1248
|
+
three selects from the props of the modal entry it mounted in, clamping a day
|
|
1249
|
+
that the restored month cannot hold.
|
|
1250
|
+
|
|
1251
|
+
**The gate is not weakened.** Restoring the date restores the DATE and never the
|
|
1252
|
+
verdict: the card comes back submittable and the app's endpoint re-decides on the
|
|
1253
|
+
next submit exactly as it did on the first. `e2e/birthday_gate.spec.js` asserts a
|
|
1254
|
+
restored under-age date is refused again.
|
|
1255
|
+
|
|
1256
|
+
**No consumer action.** A host that renders `blocks/_age_gate` and
|
|
1257
|
+
`studio/_birthday_assets` gets this by upgrading; nothing in the call signature
|
|
1258
|
+
changed and no host wiring moves.
|
|
1041
1259
|
|
|
1260
|
+
## 0.60.1 — 2026-08-25
|
|
1042
1261
|
|
|
1043
1262
|
### Breaking
|
|
1044
1263
|
|
|
@@ -1067,38 +1286,14 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1067
1286
|
that pass a number are unaffected. There is still NO engine default, because 18
|
|
1068
1287
|
is itself a policy value.
|
|
1069
1288
|
|
|
1070
|
-
- **An under-age date now SUBMITS.** It used to disable the card's own submit
|
|
1071
|
-
button and turn the card red — the one screen state with nothing to press. The
|
|
1072
|
-
factory routes on the RESPONSE instead: `body.underage === true`, or HTTP 403,
|
|
1073
|
-
opens the age-gate card; anything else non-verified stays on the error line. An
|
|
1074
|
-
app adopting this must make its verify endpoint answer "too young" distinctly
|
|
1075
|
-
from "bad request", because a client that blocks submission never had to.
|
|
1076
|
-
|
|
1077
|
-
### Added
|
|
1078
|
-
|
|
1079
|
-
- **`--nav-p` and `navCollapse()` — the navbar collapse becomes a primitive.**
|
|
1080
|
-
`layouts/studio/_head` now publishes a third header property beside `--nav-h`
|
|
1081
|
-
and `--nav-bottom`. The first two are measurements OF the header; `--nav-p` is
|
|
1082
|
-
an input TO it: collapse progress, `0` expanded to `1` collapsed, written on
|
|
1083
|
-
the `<header>` once per animation frame from `window.scrollY`, and registered
|
|
1084
|
-
with `@property` as a `<number>` so it is legal inside `calc()`, real before
|
|
1085
|
-
the first scroll, and interpolable.
|
|
1086
|
-
|
|
1087
|
-
**It ships no sizing opinion.** A host adopts it by putting `nav-shell` and
|
|
1088
|
-
`x-data="navCollapse()"` on its header, giving each breakpoint band a
|
|
1089
|
-
`--nav-ramp`, and writing its collapsing dimensions as `calc()`s off
|
|
1090
|
-
`--nav-p`. An app whose navbar collapses to different endpoints than this
|
|
1091
|
-
gem's — `mcritchie-studio` goes `w-8 → w-5`, not `w-12 → w-8` — can adopt the
|
|
1092
|
-
mechanism without changing its endpoints.
|
|
1093
|
-
|
|
1094
|
-
**NO SHIPPING CONSUMER EXERCISES THIS YET, and the adoptions are follow-ons.**
|
|
1095
|
-
`turf-monster` overrides `layouts/_navbar.html.erb` AND redefines
|
|
1096
|
-
`window.navCollapse` in `shared/_alpine_factories.html.erb`, rendered after the
|
|
1097
|
-
engine head, so the engine's copy is shadowed. `mcritchie-studio` never renders
|
|
1098
|
-
`layouts/navbar` at all — its inline header still carries `@scroll.window` and
|
|
1099
|
-
`transition-all duration-300`. Said plainly because it also means the green
|
|
1100
|
-
consumer CI on this change is **not evidence about this code path**: both apps
|
|
1101
|
-
compile and pass against a primitive neither of them runs.
|
|
1289
|
+
- **An under-age date now SUBMITS.** It used to disable the card's own submit
|
|
1290
|
+
button and turn the card red — the one screen state with nothing to press. The
|
|
1291
|
+
factory routes on the RESPONSE instead: `body.underage === true`, or HTTP 403,
|
|
1292
|
+
opens the age-gate card; anything else non-verified stays on the error line. An
|
|
1293
|
+
app adopting this must make its verify endpoint answer "too young" distinctly
|
|
1294
|
+
from "bad request", because a client that blocks submission never had to.
|
|
1295
|
+
|
|
1296
|
+
### Added
|
|
1102
1297
|
|
|
1103
1298
|
- **`studio/modals/blocks/_age_gate` — the refusal card.** Where the birthday card
|
|
1104
1299
|
hands off when the date is under the app's bar. Headed "Easy, Young'un" under a
|
|
@@ -1109,61 +1304,54 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1109
1304
|
absent rather than rendered dead) and a back link that returns to the birthday
|
|
1110
1305
|
card. Displays `min_age` / `state`; computes no eligibility of its own.
|
|
1111
1306
|
|
|
1112
|
-
|
|
1113
|
-
The reown-style wallet chooser existed three times before this — turf-monster's
|
|
1114
|
-
226 lines, mcritchie-studio's 107 and the style guide's 176 — sharing no code.
|
|
1115
|
-
It is one partial now, and `style/modals/_wallet_connect` CONFIGURES it rather
|
|
1116
|
-
than porting it, so the specimen and production cannot drift again. Locals:
|
|
1117
|
-
`store` (default `"modals"`), `connect_fn` (default `"solanaConnectAndVerify"`),
|
|
1118
|
-
`title`, `extra_data` (extra x-data members as a brace-less JS fragment), plus a
|
|
1119
|
-
BLOCK for the pre-connect slot. App behaviour arrives as optional hook METHODS
|
|
1120
|
-
defined in `extra_data`, each called only if it exists: `onInit`, `canPick`,
|
|
1121
|
-
`verifyArgs`, `onConnected(result)`, `onDeepLink`, `onBack`. Carries the mobile
|
|
1122
|
-
Phantom single-row fix (one Phantom row in every state) and a `role="alert"` on
|
|
1123
|
-
the connect error that both app copies lacked.
|
|
1124
|
-
|
|
1125
|
-
**NO SHIPPING CONSUMER RENDERS IT YET.** Both apps still ship their own copy and
|
|
1126
|
-
adopt this one after it releases, so the green consumer CI here says they still
|
|
1127
|
-
compile — it says nothing about this partial.
|
|
1307
|
+
## 0.59.0 — 2026-08-21
|
|
1128
1308
|
|
|
1129
1309
|
### Fixed
|
|
1130
1310
|
|
|
1131
|
-
- **The
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
`
|
|
1140
|
-
|
|
1311
|
+
- **The Geo signpost now reaches every app whose admin chrome the engine owns.**
|
|
1312
|
+
The row added last release went into `components/_admin_dropdown` alone, on the
|
|
1313
|
+
premise that the shared dropdown reaches every app from one change. It does
|
|
1314
|
+
not, and counted against the apps that actually consume this engine it reached
|
|
1315
|
+
**none of them**. Wherever a host declares an admin-flagged sidebar section,
|
|
1316
|
+
`studio_sidebar_replaces_admin_menu?` SUPPRESSES the dropdown and the link
|
|
1317
|
+
sidebar is the admin menu — that is both `mcritchie-studio` and
|
|
1318
|
+
`mcritchie-industries`. An app that forks its navbar outright renders neither,
|
|
1319
|
+
which is `turf-monster`. Nobody was left on the plain engine navbar to see it,
|
|
1320
|
+
so the app carrying the largest admin surface in the ecosystem was among those
|
|
1321
|
+
guaranteed never to.
|
|
1141
1322
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1323
|
+
The row moved into its own partial, **`components/_geo_signpost`**, and BOTH
|
|
1324
|
+
engine chromes render it — the dropdown as before, and
|
|
1325
|
+
`components/_link_sidebar` under its own admin-chipped `Geo` heading. Nothing
|
|
1326
|
+
to configure: a host on either chrome gets the signage from the version bump.
|
|
1146
1327
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
and
|
|
1151
|
-
|
|
1152
|
-
returns to 1× with no velocity step. A linear ramp equal to the collapse hits
|
|
1153
|
-
2× and steps straight back.
|
|
1328
|
+
**Exactly one of them, never both.** The sidebar carries the row only where it
|
|
1329
|
+
IS the admin menu (the host declared an admin-flagged section, so the dropdown
|
|
1330
|
+
is suppressed). A host declaring only public sections renders BOTH chromes by
|
|
1331
|
+
design, and there the dropdown keeps the row — fixing a reach gap by showing
|
|
1332
|
+
an admin the same row twice is not a fix.
|
|
1154
1333
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
subtree); a short-page guard, because collapsing shortens the document and on
|
|
1158
|
-
a barely-scrollable page that deletes the very scroll room that triggered it,
|
|
1159
|
-
clamps `scrollY` to 0, and flaps forever; and a `prefers-reduced-motion`
|
|
1160
|
-
branch that snaps `--nav-p` to `0`/`1`, since resizing type under a moving
|
|
1161
|
-
finger is itself motion worth dropping.
|
|
1334
|
+
**A forked chrome still needs one line**, because the engine cannot reach a
|
|
1335
|
+
fork by rendering. Inside your own admin block:
|
|
1162
1336
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1337
|
+
```erb
|
|
1338
|
+
<%= render "components/geo_signpost",
|
|
1339
|
+
variant: :sidebar, close_action: "$store.sidebars.gearOpen = false" %>
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
`variant:` picks the chrome's row shape (`:dropdown`, the default, or
|
|
1343
|
+
`:sidebar`); `close_action:` is the Alpine expression that closes YOUR panel
|
|
1344
|
+
behind the row — the store flag belongs to the caller, so the partial never
|
|
1345
|
+
hardcodes one. See [`docs/GEO.md`](docs/GEO.md#finding-the-page--the-signpost-in-whichever-chrome-the-app-has).
|
|
1346
|
+
|
|
1347
|
+
Behavior is otherwise unchanged: same three states, same words, same
|
|
1348
|
+
self-gating on `admin?`, and `ENABLE_GEO_BLOCKING` still governs the LINK and
|
|
1349
|
+
never the gate. One test hook was renamed — `data-test="geo-signpost-disabled"`,
|
|
1350
|
+
was `admin-dropdown-geo-disabled` — since the row renders in two chromes now
|
|
1351
|
+
and a name claiming one of them was a lie. Grepped ecosystem-wide first: it
|
|
1352
|
+
appeared in no consumer.
|
|
1353
|
+
|
|
1354
|
+
## 0.58.0 — 2026-08-21
|
|
1167
1355
|
|
|
1168
1356
|
### Added
|
|
1169
1357
|
|
|
@@ -1181,6 +1369,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1181
1369
|
enforcement would silently stop a live legal blocklist on the next deploy. An
|
|
1182
1370
|
app that would rather decide in code sets `config.geo_blocking_enabled`.
|
|
1183
1371
|
|
|
1372
|
+
## 0.57.0 — 2026-08-20
|
|
1373
|
+
|
|
1374
|
+
### Added
|
|
1375
|
+
|
|
1184
1376
|
- **Geo — validation for every app, locking for the apps that need it.**
|
|
1185
1377
|
Turf Monster's geo stack, lifted into the engine and generalised from
|
|
1186
1378
|
US-states-only to country + subdivision. Full guide:
|
|
@@ -1236,6 +1428,51 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1236
1428
|
exist, and a *resolved foreign* country with no region stays allowed. Opt out
|
|
1237
1429
|
with `Studio.geo_fail_closed = false`.
|
|
1238
1430
|
|
|
1431
|
+
## 0.56.3 — 2026-08-19
|
|
1432
|
+
|
|
1433
|
+
### Changed
|
|
1434
|
+
|
|
1435
|
+
- **Montserrat is vendored into the engine and can no longer re-measure the page.**
|
|
1436
|
+
`layouts/studio/_head` linked the family from `fonts.googleapis.com` with
|
|
1437
|
+
`display=swap`. The stylesheet blocks the load event; the font FILES do not — so
|
|
1438
|
+
every app went interactive in the fallback font and re-measured every glyph when
|
|
1439
|
+
Montserrat landed. That reflow swallowed a synthesized click three times in one CI
|
|
1440
|
+
day (root-caused in `close-board-filter-flake`: `pointerdown` and `pointerup` hit
|
|
1441
|
+
different elements, so the browser fired `click` on their common ancestor), and on a
|
|
1442
|
+
cold cache it moved a filter chip under a real finger. Montserrat was the last
|
|
1443
|
+
third-party asset in the head; Alpine, SortableJS and canvas-confetti were already
|
|
1444
|
+
vendored.
|
|
1445
|
+
|
|
1446
|
+
What ships:
|
|
1447
|
+
|
|
1448
|
+
- **Two woff2 files** — `studio/montserrat-latin.woff2` (35KB) and
|
|
1449
|
+
`studio/montserrat-latin-ext.woff2` (68KB), served through the asset pipeline and
|
|
1450
|
+
precompiled for Sprockets hosts. The `fonts.googleapis.com` and `fonts.gstatic.com`
|
|
1451
|
+
preconnects are gone, and the head reaches no third party at all.
|
|
1452
|
+
- **`font-display: optional`, not `swap`.** This is the fix, not the self-hosting.
|
|
1453
|
+
`swap` has an unbounded swap period, so a late font always reflows wherever it is
|
|
1454
|
+
served from; `optional` has none, so a font that misses the block period is
|
|
1455
|
+
abandoned for that navigation instead of shifting the layout. The cost is named: a
|
|
1456
|
+
first visit on a slow link can render one page in the fallback. A `crossorigin`
|
|
1457
|
+
preload of the latin subset is what keeps that rare.
|
|
1458
|
+
- **One face per subset spanning `100 900`.** Montserrat v31 is a VARIABLE font — the
|
|
1459
|
+
six weights the old link requested all resolved to the same bytes — so every weight
|
|
1460
|
+
the fleet uses renders from real outlines rather than a synthesized fake, and asking
|
|
1461
|
+
for fewer weights would not have saved a byte.
|
|
1462
|
+
- **Only latin and latin-ext.** cyrillic, cyrillic-ext and vietnamese occur zero times
|
|
1463
|
+
in the engine's views, the consumers' views, and their seed and locale data, so they
|
|
1464
|
+
are not shipped. `unicode-range` means a page only downloads the subsets it renders,
|
|
1465
|
+
so keeping latin-ext (for roster names like Modrić and Çalhanoğlu) costs an English
|
|
1466
|
+
page nothing.
|
|
1467
|
+
|
|
1468
|
+
No consumer action is required: the family, the weights, and the Tailwind `sans` stack
|
|
1469
|
+
are unchanged. An app that sets its own `font-display` or preloads Montserrat itself
|
|
1470
|
+
should drop that.
|
|
1471
|
+
|
|
1472
|
+
## 0.56.0 — 2026-08-16
|
|
1473
|
+
|
|
1474
|
+
### Added
|
|
1475
|
+
|
|
1239
1476
|
- **The hold-to-confirm button, with its fizz — a new ACTION family in
|
|
1240
1477
|
`engine-motion.css`.** A press-and-hold CTA for an action a host does not want
|
|
1241
1478
|
taken by accident, ported from Turf Monster where it confirms a contest entry.
|
|
@@ -1283,6 +1520,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1283
1520
|
page matches the one it replaced) and two buttons on a page do not fizz in
|
|
1284
1521
|
lockstep.
|
|
1285
1522
|
|
|
1523
|
+
## 0.55.0 — 2026-08-16
|
|
1524
|
+
|
|
1525
|
+
### Added
|
|
1526
|
+
|
|
1286
1527
|
- **`.studio-team-glow` rings a host that paints its own background**, and can
|
|
1287
1528
|
carry **two colors**.
|
|
1288
1529
|
|
|
@@ -1304,118 +1545,13 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1304
1545
|
defaults to the first, so one-color callers see no change and a caller with a
|
|
1305
1546
|
pair to show — a Pokémon's two types — gets one per wedge.
|
|
1306
1547
|
|
|
1307
|
-
The bloom twin needed care: a mask applies AFTER a filter, so its box carries a
|
|
1308
|
-
transparent border twice the blur radius and its wedges stop at the padding
|
|
1309
|
-
box. Without that room the halo ends on a hard line at its own edge.
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
`layouts/studio/_head` linked the family from `fonts.googleapis.com` with
|
|
1315
|
-
`display=swap`. The stylesheet blocks the load event; the font FILES do not — so
|
|
1316
|
-
every app went interactive in the fallback font and re-measured every glyph when
|
|
1317
|
-
Montserrat landed. That reflow swallowed a synthesized click three times in one CI
|
|
1318
|
-
day (root-caused in `close-board-filter-flake`: `pointerdown` and `pointerup` hit
|
|
1319
|
-
different elements, so the browser fired `click` on their common ancestor), and on a
|
|
1320
|
-
cold cache it moved a filter chip under a real finger. Montserrat was the last
|
|
1321
|
-
third-party asset in the head; Alpine, SortableJS and canvas-confetti were already
|
|
1322
|
-
vendored.
|
|
1323
|
-
|
|
1324
|
-
What ships:
|
|
1325
|
-
|
|
1326
|
-
- **Two woff2 files** — `studio/montserrat-latin.woff2` (35KB) and
|
|
1327
|
-
`studio/montserrat-latin-ext.woff2` (68KB), served through the asset pipeline and
|
|
1328
|
-
precompiled for Sprockets hosts. The `fonts.googleapis.com` and `fonts.gstatic.com`
|
|
1329
|
-
preconnects are gone, and the head reaches no third party at all.
|
|
1330
|
-
- **`font-display: optional`, not `swap`.** This is the fix, not the self-hosting.
|
|
1331
|
-
`swap` has an unbounded swap period, so a late font always reflows wherever it is
|
|
1332
|
-
served from; `optional` has none, so a font that misses the block period is
|
|
1333
|
-
abandoned for that navigation instead of shifting the layout. The cost is named: a
|
|
1334
|
-
first visit on a slow link can render one page in the fallback. A `crossorigin`
|
|
1335
|
-
preload of the latin subset is what keeps that rare.
|
|
1336
|
-
- **One face per subset spanning `100 900`.** Montserrat v31 is a VARIABLE font — the
|
|
1337
|
-
six weights the old link requested all resolved to the same bytes — so every weight
|
|
1338
|
-
the fleet uses renders from real outlines rather than a synthesized fake, and asking
|
|
1339
|
-
for fewer weights would not have saved a byte.
|
|
1340
|
-
- **Only latin and latin-ext.** cyrillic, cyrillic-ext and vietnamese occur zero times
|
|
1341
|
-
in the engine's views, the consumers' views, and their seed and locale data, so they
|
|
1342
|
-
are not shipped. `unicode-range` means a page only downloads the subsets it renders,
|
|
1343
|
-
so keeping latin-ext (for roster names like Modrić and Çalhanoğlu) costs an English
|
|
1344
|
-
page nothing.
|
|
1345
|
-
|
|
1346
|
-
No consumer action is required: the family, the weights, and the Tailwind `sans` stack
|
|
1347
|
-
are unchanged. An app that sets its own `font-display` or preloads Montserrat itself
|
|
1348
|
-
should drop that.
|
|
1349
|
-
|
|
1350
|
-
### Fixed
|
|
1351
|
-
|
|
1352
|
-
- **The age gate's back link now returns the date the person already entered.**
|
|
1353
|
-
`blocks/_age_gate`'s "Update your Birthday" swapped back to `blocks/_birthday`
|
|
1354
|
-
with an EMPTY props object, discarding the `dobYear`/`dobMonth`/`dobDay` parts
|
|
1355
|
-
the birthday factory had just handed across the store — and the factory started
|
|
1356
|
-
its three fields at `""` and never read them anyway. So the correction path came
|
|
1357
|
-
back BLANK: a mistyped year cost all three picks, on a card whose own header
|
|
1358
|
-
comment promised "a correction, not a restart".
|
|
1359
|
-
|
|
1360
|
-
Both sides of the seam moved. `back()` forwards the three date parts — and only
|
|
1361
|
-
those three, deliberately: `minAge`, `state` and `message` describe the refusal,
|
|
1362
|
-
and `validates` is load-bearing by its ABSENCE (the style guide specimen reads an
|
|
1363
|
-
absent prop as validating, and a forwarded stale one would let the refusal pick
|
|
1364
|
-
the next card's mode). `window.birthdayModal` gained an `init` that re-picks the
|
|
1365
|
-
three selects from the props of the modal entry it mounted in, clamping a day
|
|
1366
|
-
that the restored month cannot hold.
|
|
1367
|
-
|
|
1368
|
-
**The gate is not weakened.** Restoring the date restores the DATE and never the
|
|
1369
|
-
verdict: the card comes back submittable and the app's endpoint re-decides on the
|
|
1370
|
-
next submit exactly as it did on the first. `e2e/birthday_gate.spec.js` asserts a
|
|
1371
|
-
restored under-age date is refused again.
|
|
1372
|
-
|
|
1373
|
-
**No consumer action.** A host that renders `blocks/_age_gate` and
|
|
1374
|
-
`studio/_birthday_assets` gets this by upgrading; nothing in the call signature
|
|
1375
|
-
changed and no host wiring moves.
|
|
1376
|
-
|
|
1377
|
-
- **The Geo signpost now reaches every app whose admin chrome the engine owns.**
|
|
1378
|
-
The row added last release went into `components/_admin_dropdown` alone, on the
|
|
1379
|
-
premise that the shared dropdown reaches every app from one change. It does
|
|
1380
|
-
not, and counted against the apps that actually consume this engine it reached
|
|
1381
|
-
**none of them**. Wherever a host declares an admin-flagged sidebar section,
|
|
1382
|
-
`studio_sidebar_replaces_admin_menu?` SUPPRESSES the dropdown and the link
|
|
1383
|
-
sidebar is the admin menu — that is both `mcritchie-studio` and
|
|
1384
|
-
`mcritchie-industries`. An app that forks its navbar outright renders neither,
|
|
1385
|
-
which is `turf-monster`. Nobody was left on the plain engine navbar to see it,
|
|
1386
|
-
so the app carrying the largest admin surface in the ecosystem was among those
|
|
1387
|
-
guaranteed never to.
|
|
1388
|
-
|
|
1389
|
-
The row moved into its own partial, **`components/_geo_signpost`**, and BOTH
|
|
1390
|
-
engine chromes render it — the dropdown as before, and
|
|
1391
|
-
`components/_link_sidebar` under its own admin-chipped `Geo` heading. Nothing
|
|
1392
|
-
to configure: a host on either chrome gets the signage from the version bump.
|
|
1393
|
-
|
|
1394
|
-
**Exactly one of them, never both.** The sidebar carries the row only where it
|
|
1395
|
-
IS the admin menu (the host declared an admin-flagged section, so the dropdown
|
|
1396
|
-
is suppressed). A host declaring only public sections renders BOTH chromes by
|
|
1397
|
-
design, and there the dropdown keeps the row — fixing a reach gap by showing
|
|
1398
|
-
an admin the same row twice is not a fix.
|
|
1399
|
-
|
|
1400
|
-
**A forked chrome still needs one line**, because the engine cannot reach a
|
|
1401
|
-
fork by rendering. Inside your own admin block:
|
|
1402
|
-
|
|
1403
|
-
```erb
|
|
1404
|
-
<%= render "components/geo_signpost",
|
|
1405
|
-
variant: :sidebar, close_action: "$store.sidebars.gearOpen = false" %>
|
|
1406
|
-
```
|
|
1407
|
-
|
|
1408
|
-
`variant:` picks the chrome's row shape (`:dropdown`, the default, or
|
|
1409
|
-
`:sidebar`); `close_action:` is the Alpine expression that closes YOUR panel
|
|
1410
|
-
behind the row — the store flag belongs to the caller, so the partial never
|
|
1411
|
-
hardcodes one. See [`docs/GEO.md`](docs/GEO.md#finding-the-page--the-signpost-in-whichever-chrome-the-app-has).
|
|
1412
|
-
|
|
1413
|
-
Behavior is otherwise unchanged: same three states, same words, same
|
|
1414
|
-
self-gating on `admin?`, and `ENABLE_GEO_BLOCKING` still governs the LINK and
|
|
1415
|
-
never the gate. One test hook was renamed — `data-test="geo-signpost-disabled"`,
|
|
1416
|
-
was `admin-dropdown-geo-disabled` — since the row renders in two chromes now
|
|
1417
|
-
and a name claiming one of them was a lie. Grepped ecosystem-wide first: it
|
|
1418
|
-
appeared in no consumer.
|
|
1548
|
+
The bloom twin needed care: a mask applies AFTER a filter, so its box carries a
|
|
1549
|
+
transparent border twice the blur radius and its wedges stop at the padding
|
|
1550
|
+
box. Without that room the halo ends on a hard line at its own edge.
|
|
1551
|
+
|
|
1552
|
+
## 0.54.1 — 2026-08-15
|
|
1553
|
+
|
|
1554
|
+
### Fixed
|
|
1419
1555
|
|
|
1420
1556
|
- **`.conic-surface` keeps its wash inside its own box.** The wash was an
|
|
1421
1557
|
OVERSIZED pseudo (`inset: -50%`) spun by a `transform` keyframe, cut back to
|
|
@@ -1450,6 +1586,8 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1450
1586
|
contract comment now all state the same thing. The primitive's geometry is
|
|
1451
1587
|
unchanged — the consumers tuned against it are untouched.
|
|
1452
1588
|
|
|
1589
|
+
## 0.54.0 — 2026-08-15
|
|
1590
|
+
|
|
1453
1591
|
### Added
|
|
1454
1592
|
|
|
1455
1593
|
- **`studio/fields/_date_of_birth`** — the engine's one date-of-birth field,
|
|
@@ -1485,6 +1623,7 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1485
1623
|
clearing a day the new month lost, refusing a partial date, and a year list
|
|
1486
1624
|
that stops at this year.
|
|
1487
1625
|
|
|
1626
|
+
## 0.53.1 — 2026-08-15
|
|
1488
1627
|
|
|
1489
1628
|
### Changed
|
|
1490
1629
|
|
|
@@ -1502,6 +1641,7 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1502
1641
|
`position: fixed`, placed from the trigger's rect, so it never relied on that
|
|
1503
1642
|
wrapper as a containing block.
|
|
1504
1643
|
|
|
1644
|
+
## 0.53.0 — 2026-08-15
|
|
1505
1645
|
|
|
1506
1646
|
### Added
|
|
1507
1647
|
|
|
@@ -1534,7 +1674,6 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1534
1674
|
Fires on both verbs, so a host that only cares about joins ignores the flag.
|
|
1535
1675
|
The default is inert.
|
|
1536
1676
|
|
|
1537
|
-
|
|
1538
1677
|
### Fixed
|
|
1539
1678
|
|
|
1540
1679
|
- **The birthday calendar keeps the side it opened on.** `place()` runs on every
|
|
@@ -1552,6 +1691,7 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1552
1691
|
should be re-decided at all. The flip spec added with it opens and asserts
|
|
1553
1692
|
once, so a side that changes **later** was invisible to it.
|
|
1554
1693
|
|
|
1694
|
+
## 0.52.3 — 2026-08-15
|
|
1555
1695
|
|
|
1556
1696
|
### Fixed
|
|
1557
1697
|
|
|
@@ -1575,6 +1715,40 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1575
1715
|
fits. The new spec forces the flip, asserts the flip actually happened, and then
|
|
1576
1716
|
asserts the popover's bottom edge sits against the trigger's top edge.
|
|
1577
1717
|
|
|
1718
|
+
## 0.52.0 — 2026-08-15
|
|
1719
|
+
|
|
1720
|
+
### Added
|
|
1721
|
+
|
|
1722
|
+
- **`/profile` gains the Newsletter row.** Lifted from turf-monster's `/account`
|
|
1723
|
+
card, which has run this flow in production, and stripped of everything
|
|
1724
|
+
turf-specific on the way: its 25-seed on-chain welcome bonus, its quest state,
|
|
1725
|
+
its seeds level-up payload. What the engine takes is the part every app needs.
|
|
1726
|
+
|
|
1727
|
+
New routes: `POST /profile/newsletter` (`profile_newsletter_path`) joins,
|
|
1728
|
+
`DELETE` on the same path leaves.
|
|
1729
|
+
|
|
1730
|
+
**TWO TIMESTAMPS, NOT A BOOLEAN.** `joined_email_list_at` and
|
|
1731
|
+
`left_email_list_at`, matching turf, because the pair carries three states a
|
|
1732
|
+
flag cannot: never asked (both nil), subscribed (joined after left, *including
|
|
1733
|
+
a rejoin where both are set*), and unsubscribed. `Studio::Newsletter` holds the
|
|
1734
|
+
rules — `subscribed?`, `ever_joined?`, `needs_email?` — pure and duck-typed
|
|
1735
|
+
like `Studio::OauthIdentity`.
|
|
1736
|
+
|
|
1737
|
+
`ever_joined?` is deliberately a different question from `subscribed?`: leaving
|
|
1738
|
+
stamps a date and never clears the join, so a consumer paying a once-ever
|
|
1739
|
+
welcome bonus cannot have it re-earned by cycling.
|
|
1740
|
+
|
|
1741
|
+
**ASYMMETRIC ON PURPOSE.** Joining is one click; leaving asks for confirmation
|
|
1742
|
+
in a modal. Joining is reversible from the same card, so a confirm step would
|
|
1743
|
+
be friction protecting nothing — a mis-click on leave is silent until the next
|
|
1744
|
+
send that never arrives. An account with **no address on file** (a wallet-only
|
|
1745
|
+
sign-in) is asked for one in a modal rather than allowed to submit and fail;
|
|
1746
|
+
the address is written but **not** marked verified, because typing an address
|
|
1747
|
+
is not the same as holding it.
|
|
1748
|
+
|
|
1749
|
+
Gated on `requires:` like every other row, so a host without the columns gets
|
|
1750
|
+
silence rather than a 500. The columns ship consumer-first under
|
|
1751
|
+
*Roll Out Standard Profile Columns*.
|
|
1578
1752
|
|
|
1579
1753
|
### Changed
|
|
1580
1754
|
|
|
@@ -1614,39 +1788,18 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1614
1788
|
The three integer columns are unchanged; the UI joins them for entry and
|
|
1615
1789
|
`ProfilesController#update` splits them again.
|
|
1616
1790
|
|
|
1791
|
+
- **The read page mounts a modal host only when a row asks for one.** This is the
|
|
1792
|
+
registry's `modals:` key finally doing the job it was documented for — until
|
|
1793
|
+
the newsletter row, nothing on `/profile` opened a modal and mounting a host
|
|
1794
|
+
would have been furniture for nobody.
|
|
1617
1795
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
turf-specific on the way: its 25-seed on-chain welcome bonus, its quest state,
|
|
1623
|
-
its seeds level-up payload. What the engine takes is the part every app needs.
|
|
1624
|
-
|
|
1625
|
-
New routes: `POST /profile/newsletter` (`profile_newsletter_path`) joins,
|
|
1626
|
-
`DELETE` on the same path leaves.
|
|
1627
|
-
|
|
1628
|
-
**TWO TIMESTAMPS, NOT A BOOLEAN.** `joined_email_list_at` and
|
|
1629
|
-
`left_email_list_at`, matching turf, because the pair carries three states a
|
|
1630
|
-
flag cannot: never asked (both nil), subscribed (joined after left, *including
|
|
1631
|
-
a rejoin where both are set*), and unsubscribed. `Studio::Newsletter` holds the
|
|
1632
|
-
rules — `subscribed?`, `ever_joined?`, `needs_email?` — pure and duck-typed
|
|
1633
|
-
like `Studio::OauthIdentity`.
|
|
1634
|
-
|
|
1635
|
-
`ever_joined?` is deliberately a different question from `subscribed?`: leaving
|
|
1636
|
-
stamps a date and never clears the join, so a consumer paying a once-ever
|
|
1637
|
-
welcome bonus cannot have it re-earned by cycling.
|
|
1638
|
-
|
|
1639
|
-
**ASYMMETRIC ON PURPOSE.** Joining is one click; leaving asks for confirmation
|
|
1640
|
-
in a modal. Joining is reversible from the same card, so a confirm step would
|
|
1641
|
-
be friction protecting nothing — a mis-click on leave is silent until the next
|
|
1642
|
-
send that never arrives. An account with **no address on file** (a wallet-only
|
|
1643
|
-
sign-in) is asked for one in a modal rather than allowed to submit and fail;
|
|
1644
|
-
the address is written but **not** marked verified, because typing an address
|
|
1645
|
-
is not the same as holding it.
|
|
1796
|
+
`modals:` is now **a partial path rather than a boolean**. A host row that
|
|
1797
|
+
declares modals keeps its partial in the host's own app, so any convention like
|
|
1798
|
+
`"studio/profiles/#{key}_modals"` would resolve to a path that does not exist
|
|
1799
|
+
there. It costs one string and works for everyone.
|
|
1646
1800
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
*Roll Out Standard Profile Columns*.
|
|
1801
|
+
The host is **not** the cropper: `/profile` mounts `studio/modals/_scoped_host`
|
|
1802
|
+
and never `studio/cropper_assets`, because the avatar is read-only on that page.
|
|
1650
1803
|
|
|
1651
1804
|
### Fixed
|
|
1652
1805
|
|
|
@@ -1668,21 +1821,7 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1668
1821
|
first browser spec — the view suite was green on the `@click` attribute
|
|
1669
1822
|
throughout.
|
|
1670
1823
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
- **The read page mounts a modal host only when a row asks for one.** This is the
|
|
1674
|
-
registry's `modals:` key finally doing the job it was documented for — until
|
|
1675
|
-
the newsletter row, nothing on `/profile` opened a modal and mounting a host
|
|
1676
|
-
would have been furniture for nobody.
|
|
1677
|
-
|
|
1678
|
-
`modals:` is now **a partial path rather than a boolean**. A host row that
|
|
1679
|
-
declares modals keeps its partial in the host's own app, so any convention like
|
|
1680
|
-
`"studio/profiles/#{key}_modals"` would resolve to a path that does not exist
|
|
1681
|
-
there. It costs one string and works for everyone.
|
|
1682
|
-
|
|
1683
|
-
The host is **not** the cropper: `/profile` mounts `studio/modals/_scoped_host`
|
|
1684
|
-
and never `studio/cropper_assets`, because the avatar is read-only on that page.
|
|
1685
|
-
|
|
1824
|
+
## 0.50.0 — 2026-08-15
|
|
1686
1825
|
|
|
1687
1826
|
### Added
|
|
1688
1827
|
|
|
@@ -1719,19 +1858,6 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1719
1858
|
the intended consequence — the sidebar always has at least the viewer's own
|
|
1720
1859
|
profile in it — but it is a visible change to a navbar every consumer inherits.
|
|
1721
1860
|
|
|
1722
|
-
### Fixed
|
|
1723
|
-
|
|
1724
|
-
- **`Studio.profile_sections`' `if:` gate no longer fails open on a Symbol.**
|
|
1725
|
-
`if: :some_predicate` — Rails' own `before_action ..., if: :method_name`
|
|
1726
|
-
spelling, and so the most natural thing a host will write — was coerced
|
|
1727
|
-
straight to `true`, because a Symbol does not answer `call`. The gate silently
|
|
1728
|
-
did nothing, with no signal to the host, in the same permissive direction as
|
|
1729
|
-
the bug `if:` was added to fix. Symbols and Strings now name a method on the
|
|
1730
|
-
view; a name the view does not answer drops the row rather than rendering it.
|
|
1731
|
-
Lambdas and plain booleans are unchanged.
|
|
1732
|
-
|
|
1733
|
-
### Added
|
|
1734
|
-
|
|
1735
1861
|
- **`/profile` splits into a read page and an edit page.** `/profile` is now "you
|
|
1736
1862
|
at a glance" — the identity header, and the rows you look at rather than type
|
|
1737
1863
|
into. `/profile/edit` (`edit_profile_path`) holds the fields, in ONE form with
|
|
@@ -1760,6 +1886,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1760
1886
|
adding a second copy for a screen reader to read out. Honors
|
|
1761
1887
|
`prefers-reduced-motion`.
|
|
1762
1888
|
|
|
1889
|
+
## 0.49.0 — 2026-08-15
|
|
1890
|
+
|
|
1891
|
+
### Added
|
|
1892
|
+
|
|
1763
1893
|
- **`/profile` gains the Email field — changeable from any signed-in session.**
|
|
1764
1894
|
It saves with the rest of the edit form through `PATCH /profile`.
|
|
1765
1895
|
|
|
@@ -1791,6 +1921,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1791
1921
|
not raise, it would quietly send TURF's copy, banner and `account_url` instead
|
|
1792
1922
|
of the engine's. The silent case is the dangerous one.
|
|
1793
1923
|
|
|
1924
|
+
## 0.48.0 — 2026-08-15
|
|
1925
|
+
|
|
1926
|
+
### Added
|
|
1927
|
+
|
|
1794
1928
|
- **`/profile` gains the Google account row.** Shows the linked identity with an
|
|
1795
1929
|
Unlink control, or a branded Connect button that POSTs to OmniAuth's own
|
|
1796
1930
|
`/auth/google_oauth2` (the engine does not draw a link route — the middleware
|
|
@@ -1984,6 +2118,21 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
1984
2118
|
mcritchie-industries and acquisition-studio gain a working link where they had
|
|
1985
2119
|
`href="#"`; every other consumer forks the partial and is untouched.
|
|
1986
2120
|
|
|
2121
|
+
### Fixed
|
|
2122
|
+
|
|
2123
|
+
- **`Studio.profile_sections`' `if:` gate no longer fails open on a Symbol.**
|
|
2124
|
+
`if: :some_predicate` — Rails' own `before_action ..., if: :method_name`
|
|
2125
|
+
spelling, and so the most natural thing a host will write — was coerced
|
|
2126
|
+
straight to `true`, because a Symbol does not answer `call`. The gate silently
|
|
2127
|
+
did nothing, with no signal to the host, in the same permissive direction as
|
|
2128
|
+
the bug `if:` was added to fix. Symbols and Strings now name a method on the
|
|
2129
|
+
view; a name the view does not answer drops the row rather than rendering it.
|
|
2130
|
+
Lambdas and plain booleans are unchanged.
|
|
2131
|
+
|
|
2132
|
+
## 0.47.2 — 2026-08-14
|
|
2133
|
+
|
|
2134
|
+
### Changed
|
|
2135
|
+
|
|
1987
2136
|
- **No pre-registered email seeds a logo — `magic_link` was the last one, and it
|
|
1988
2137
|
seeded the Studio wordmark onto the SIGN-IN email.** `STANDARD`'s `magic_link`
|
|
1989
2138
|
entry carried `logo: "emails/logo-horizontal.png"`, and
|
|
@@ -2033,32 +2182,7 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
2033
2182
|
that registers no mark, so the page says so rather than offering a choice that
|
|
2034
2183
|
changes nothing.
|
|
2035
2184
|
|
|
2036
|
-
|
|
2037
|
-
joins it.** `STANDARD` means "the emails EVERY Studio app sends", and only
|
|
2038
|
-
turf-monster sends an email-change confirmation — which it **already registers
|
|
2039
|
-
itself**, with its own artwork, in `config/initializers/studio_emails.rb`. No
|
|
2040
|
-
host loses a banner, and no host action is required for the removal.
|
|
2041
|
-
|
|
2042
|
-
What DOES break consumers is the ADDITION. Three suites assert against the
|
|
2043
|
-
whole shared catalogue, so a new standard email fails them:
|
|
2044
|
-
|
|
2045
|
-
| Consumer | Assertion | Fix | State |
|
|
2046
|
-
|---|---|---|---|
|
|
2047
|
-
| turf-monster | `EmailRegistrationTest` compares the full key list | McRitchie-Studio/turf-monster#292 | merged |
|
|
2048
|
-
| turf-monster | `AdminEmailsRenderTest` counts `tbody img` exactly | McRitchie-Studio/turf-monster#294 | merged |
|
|
2049
|
-
| mcritchie-studio | `StudioEmailsPageTest` pins the registry list, an `email_change_confirmation` link, and the upload-rejection wording | McRitchie-Studio/mcritchie-studio#813 | merged |
|
|
2050
|
-
|
|
2051
|
-
The second turf fix is the easy one to miss: a layered standard email renders
|
|
2052
|
-
its banner through an `<iframe>` rather than an `<img>`, so an exact image
|
|
2053
|
-
count comes up one short. #292 alone does NOT turn that lane green.
|
|
2054
|
-
|
|
2055
|
-
Consumer CI builds each app against its **default branch**, so all three had to
|
|
2056
|
-
reach `main` before this engine's consumer lanes could go green. All three now
|
|
2057
|
-
have, and the lanes pass. Nothing on this branch ever could have turned them
|
|
2058
|
-
green — a red consumer lane here is a consumer-side fix, not an engine defect.
|
|
2059
|
-
|
|
2060
|
-
The artwork `emails/email-change-confirmation.gif` still ships, so a host that
|
|
2061
|
-
wants the entry can register it.
|
|
2185
|
+
## 0.47.0 — 2026-08-13
|
|
2062
2186
|
|
|
2063
2187
|
### Added
|
|
2064
2188
|
|
|
@@ -2116,6 +2240,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
2116
2240
|
exact defect `isLive` exists to prevent. A turf-side follow-up, not part of
|
|
2117
2241
|
this change.
|
|
2118
2242
|
|
|
2243
|
+
## 0.46.0 — 2026-08-13
|
|
2244
|
+
|
|
2245
|
+
### Added
|
|
2246
|
+
|
|
2119
2247
|
- **The standard user profile columns — and the engine's first migration against
|
|
2120
2248
|
a host-owned table.** Every other engine migration creates a `studio_*` table
|
|
2121
2249
|
the engine owns outright; `users` belongs to the host. That boundary is crossed
|
|
@@ -2193,6 +2321,46 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
2193
2321
|
rather than raising on every signed-in request — so the gem can land there
|
|
2194
2322
|
before the migration does.
|
|
2195
2323
|
|
|
2324
|
+
## 0.44.1 — 2026-08-13
|
|
2325
|
+
|
|
2326
|
+
### Fixed
|
|
2327
|
+
|
|
2328
|
+
- **The newsletter's seed lent every host the McRitchie Studio wordmark.**
|
|
2329
|
+
`STANDARD`'s `newsletter_subscribed` entry seeded
|
|
2330
|
+
`logo: "emails/logo-horizontal.png"`, and `EmailCatalog.register` merges on
|
|
2331
|
+
`logo.presence` — so an omitted `logo:` INHERITS. A host registering that email
|
|
2332
|
+
for any ordinary reason (a relabel, its own preview builder) would have sent its
|
|
2333
|
+
branded newsletter under Studio's mark, and the alt text could not warn anyone:
|
|
2334
|
+
`Studio::Banner` sets it from `Studio.app_name`, so it always agrees with the
|
|
2335
|
+
host and never with the pixels. turf-monster hit the same mechanism on
|
|
2336
|
+
`magic_link`.
|
|
2337
|
+
|
|
2338
|
+
**No consumer was exposed.** No app registers `newsletter_subscribed` or sends
|
|
2339
|
+
it — turf's own `NewsletterMailer` sends its flat `newsletter_welcome` — so this
|
|
2340
|
+
was visible only on the `/admin/emails` preview. It was seeded identically in
|
|
2341
|
+
0.42 and 0.43; nothing changed it recently.
|
|
2342
|
+
|
|
2343
|
+
The seed now carries no logo. **Artwork still rides the gem** — a background is
|
|
2344
|
+
a picture any app can send, and inheriting one is what gives a new app
|
|
2345
|
+
good-looking mail on day one. A wordmark is somebody's identity, and the line is
|
|
2346
|
+
drawn there. An app that wants a mark registers a path IT ships
|
|
2347
|
+
(`register("newsletter_subscribed", logo: "emails/our-mark.png")`) or sets one
|
|
2348
|
+
on /admin/emails. Guards render the mail and resolve every `<img>` back to a
|
|
2349
|
+
file on the engine's own asset load path, comparing the wordmark by its BYTES,
|
|
2350
|
+
so renaming the file and re-seeding it is caught too.
|
|
2351
|
+
|
|
2352
|
+
**`magic_link` is unchanged and still seeds the wordmark.** A host that
|
|
2353
|
+
registers no mark of its own inherits it on the sign-in email. That is an
|
|
2354
|
+
**upgrade trap** rather than a live leak — the apps with no email initializer
|
|
2355
|
+
are pinned before this registry existed, so they inherit nothing today, but the
|
|
2356
|
+
bump that brings one forward would hand it another brand's mark with no code
|
|
2357
|
+
change of its own. Tracked separately, with the design question of whether
|
|
2358
|
+
`STANDARD` should seed a logo at all.
|
|
2359
|
+
|
|
2360
|
+
## 0.42.0 — 2026-08-12
|
|
2361
|
+
|
|
2362
|
+
### Added
|
|
2363
|
+
|
|
2196
2364
|
- **`Studio::NewsletterMailer` — a sendable "you're on the list" email.**
|
|
2197
2365
|
Namespaced under `Studio::` on purpose: a host that defines its own top-level
|
|
2198
2366
|
`UserMailer` (McRitchie Studio does) SHADOWS the engine's outright, so an
|
|
@@ -2245,39 +2413,36 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
2245
2413
|
host's own users with avatar and initials, degrading to a sample when an app has
|
|
2246
2414
|
none.
|
|
2247
2415
|
|
|
2248
|
-
###
|
|
2416
|
+
### Changed
|
|
2249
2417
|
|
|
2250
|
-
-
|
|
2251
|
-
`STANDARD`
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
branded newsletter under Studio's mark, and the alt text could not warn anyone:
|
|
2256
|
-
`Studio::Banner` sets it from `Studio.app_name`, so it always agrees with the
|
|
2257
|
-
host and never with the pixels. turf-monster hit the same mechanism on
|
|
2258
|
-
`magic_link`.
|
|
2418
|
+
- **`email_change_confirmation` leaves the pre-registered set; `newsletter_subscribed`
|
|
2419
|
+
joins it.** `STANDARD` means "the emails EVERY Studio app sends", and only
|
|
2420
|
+
turf-monster sends an email-change confirmation — which it **already registers
|
|
2421
|
+
itself**, with its own artwork, in `config/initializers/studio_emails.rb`. No
|
|
2422
|
+
host loses a banner, and no host action is required for the removal.
|
|
2259
2423
|
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
was visible only on the `/admin/emails` preview. It was seeded identically in
|
|
2263
|
-
0.42 and 0.43; nothing changed it recently.
|
|
2424
|
+
What DOES break consumers is the ADDITION. Three suites assert against the
|
|
2425
|
+
whole shared catalogue, so a new standard email fails them:
|
|
2264
2426
|
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
on /admin/emails. Guards render the mail and resolve every `<img>` back to a
|
|
2271
|
-
file on the engine's own asset load path, comparing the wordmark by its BYTES,
|
|
2272
|
-
so renaming the file and re-seeding it is caught too.
|
|
2427
|
+
| Consumer | Assertion | Fix | State |
|
|
2428
|
+
|---|---|---|---|
|
|
2429
|
+
| turf-monster | `EmailRegistrationTest` compares the full key list | McRitchie-Studio/turf-monster#292 | merged |
|
|
2430
|
+
| turf-monster | `AdminEmailsRenderTest` counts `tbody img` exactly | McRitchie-Studio/turf-monster#294 | merged |
|
|
2431
|
+
| mcritchie-studio | `StudioEmailsPageTest` pins the registry list, an `email_change_confirmation` link, and the upload-rejection wording | McRitchie-Studio/mcritchie-studio#813 | merged |
|
|
2273
2432
|
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2433
|
+
The second turf fix is the easy one to miss: a layered standard email renders
|
|
2434
|
+
its banner through an `<iframe>` rather than an `<img>`, so an exact image
|
|
2435
|
+
count comes up one short. #292 alone does NOT turn that lane green.
|
|
2436
|
+
|
|
2437
|
+
Consumer CI builds each app against its **default branch**, so all three had to
|
|
2438
|
+
reach `main` before this engine's consumer lanes could go green. All three now
|
|
2439
|
+
have, and the lanes pass. Nothing on this branch ever could have turned them
|
|
2440
|
+
green — a red consumer lane here is a consumer-side fix, not an engine defect.
|
|
2441
|
+
|
|
2442
|
+
The artwork `emails/email-change-confirmation.gif` still ships, so a host that
|
|
2443
|
+
wants the entry can register it.
|
|
2444
|
+
|
|
2445
|
+
### Fixed
|
|
2281
2446
|
|
|
2282
2447
|
- **The manager reported artwork that was not being sent.** A layered-native email
|
|
2283
2448
|
(no flat `default_asset`) drew an empty box and a "sends without a banner" badge
|
|
@@ -2291,6 +2456,8 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
2291
2456
|
existing guard was real and only ran on the index, so it never saw the page that
|
|
2292
2457
|
shipped it.
|
|
2293
2458
|
|
|
2459
|
+
## 0.41.0 — 2026-08-12
|
|
2460
|
+
|
|
2294
2461
|
### Added
|
|
2295
2462
|
|
|
2296
2463
|
- **A browser lane — the engine now runs Playwright against its own partials.**
|
|
@@ -2312,6 +2479,10 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
2312
2479
|
and covered instead by `test/views/at_time_zone_table_test.rb`. Full rationale,
|
|
2313
2480
|
limits and cost: `docs/E2E_LANE.md`.
|
|
2314
2481
|
|
|
2482
|
+
## 0.40.0 — 2026-08-12
|
|
2483
|
+
|
|
2484
|
+
### Added
|
|
2485
|
+
|
|
2315
2486
|
- **The "at" time stamp — `at_time_tag` + `studio/at_time_script`.** A shared
|
|
2316
2487
|
primitive for stamping WHEN something happened, on the READER's clock rather
|
|
2317
2488
|
than the app's: `at 3:53p`, gaining a date only when the stamp is not today and
|