studio-engine 0.65.1 → 0.65.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3aa4604073fec8349d92e58a72bfb14d5c87fbaf03081f4bcb3e430a0ec35bf6
4
- data.tar.gz: 3653491831ad04c4cb7aad8f1950297655b42a0582864173c998cbf8ea005a06
3
+ metadata.gz: 8ed062df7a9840a9633eb1c4995bc878bee401f98d85086e7943c5ec9f6b55d4
4
+ data.tar.gz: df0c1c7dc4e13185b3c8e3fbe7cb9243f4b310398843c43913ef0a1aa7b03762
5
5
  SHA512:
6
- metadata.gz: 38cd8f8eedcf34cb0cac87d4c7737bb3fd29af673651e4593a1a93f0e91e822e582500e62ffdb7172c6de1d6f8673fe6502da4717376b789ee91f0e9b9ed2fc4
7
- data.tar.gz: 2ac51eaf964ad5ef2d9e026298dd0565466540de68ae68677c77eb2b802f17bc392691d63594794bd537282b2a9d315fc64c01e52800a72a82cc87cf5a1ed329
6
+ metadata.gz: ca97a89b5c1452bad931ec0a332e9fff228ea43b7ebe2249516b3f60b9bf79f03fcb3171a66c5b8c7692e25389ac15d24a8c70b0176a82921fee80b2dd80a06e
7
+ data.tar.gz: 97061141650afc6472e15bcaf40dc96c5175897a25f6d4aea5cfb318dd1d92100db6cb1aa52c3feb4a018fe93bb924485fdee7fe97d5989a7779dfb0959b03c0
data/CHANGELOG.md CHANGED
@@ -6,6 +6,44 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
6
6
 
7
7
  ### Added
8
8
 
9
+ - **A green comment-leak scan used to mean "not looked at".**
10
+ `test/views/erb_comment_leak_test.rb` guards the ERB comment form in
11
+ `app/views/**/*.erb`. It never looked inside `<script>`, and that is where this
12
+ gem keeps its browser programs — `studio/modals/_host`,
13
+ `studio/solana/_phantom_deeplink`, `layouts/studio/_head` and the modal blocks
14
+ between them hold **1_239 JavaScript comments, 73_913 bytes of comment body**,
15
+ none of it read by any guard. A review of a diff made almost entirely of those
16
+ comments came back green, and the reviewer's note was the honest reading of it:
17
+ *"don't read that green as clearance."*
18
+
19
+ `test/views/script_comment_leak_test.rb` reads them. The rule is the one the ERB
20
+ guard applies — no ERB open or close sequence inside a comment body — and the
21
+ mechanism it defends against is worse than the ERB one. A tag quoted inside an
22
+ ERB comment is inert, because the comment swallows it. A tag quoted inside a
23
+ **JavaScript** comment is not inside anything ERB knows about, so ERB opens a tag
24
+ right there and **runs** it: a complete tag evaluates at render, and an
25
+ incomplete one swallows every byte up to the next close sequence in the file,
26
+ deleting working JavaScript with no syntax error to show for it. Wrapping the
27
+ line in an HTML comment does not help — ERB runs inside those too. Describing the
28
+ tag in words stays legal, exactly as it is for ERB comments.
29
+
30
+ **Finding a comment is the hard half, and the first cut of this got it wrong.**
31
+ Anchoring `<script>` blocks on raw source let an ERB comment's *prose mention* of
32
+ a script tag open the block: `studio/_board_assets.html.erb` reported 19_512
33
+ bytes of markup as JavaScript and never saw its real program on its own terms.
34
+ Blocks are now anchored on a copy with ERB tags and HTML comments blanked out,
35
+ length preserved so offsets and line numbers still land. Inside a block the
36
+ walker tracks the states that can hide a comment opener — a string
37
+ (`"https://…"` is not a comment), a template literal, a regex (`/\/\//`) — and a
38
+ literal or comment that never closes is REPORTED rather than absorbed, because a
39
+ walker that quietly lost its place is the same green as a clean file. The
40
+ assertions check the input as well as the verdict: how many blocks, comments and
41
+ bytes the scan actually reached, and that every block it opened matches a real
42
+ closing tag.
43
+
44
+ Zero findings across the engine at `origin/accepted`, so this lands green with no
45
+ allowlist.
46
+
9
47
  - **`data-pin` — the pinned stack publishes itself.** `--nav-h` / `--nav-bottom`
10
48
  answer for one element. Everything else that pins has been re-deriving the
11
49
  same geometry by hand: on `mcritchie-studio`'s `/deployments` the app strip
@@ -119,6 +157,60 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
119
157
  guard on the read-before-write order.
120
158
 
121
159
 
160
+ ### Fixed
161
+
162
+ - **A toast with buttons could not be dismissed while a modal was open.** The
163
+ layer scale put `--z-banner` (500) above `--z-toast` (400), and `body.modal-open`
164
+ lifts the environment bar stack to that tier at `position: sticky; top: 0`.
165
+ `#toast-container` is fixed at top 0 with `1rem` of padding, so the two land on
166
+ the same pixels: measured in a browser, the bars own y0-47 while the toast's
167
+ Dismiss button runs y28-40 — an 8-14px overlap at every viewport tested.
168
+ `elementFromPoint` at that button returned the **banner**, and a real click left
169
+ the toast on the page. Because the toast manager gives any toast carrying
170
+ buttons `duration: 0`, that X was the toast's **only** exit: it was stuck for
171
+ the rest of the session.
172
+
173
+ **The tiers are reordered** — `--z-banner` 400, `--z-toast-blur` 499, `--z-toast`
174
+ 500 — and that is a semantic decision rather than a nudge around the geometry: a
175
+ toast is transient and demands interaction, a banner is persistent chrome that
176
+ will still be there afterwards. `--z-banner` still clears `--z-modal` (200), so
177
+ the property the lift exists for — DEV MODE and the email chip stay lit and
178
+ clickable over a modal — is unchanged and now has its own spec.
179
+
180
+ **One measured cost.** A banner button's tooltip is a descendant of the bar
181
+ stack, and the modal-open lift makes that stack a stacking context, so the
182
+ tooltip composites at `--z-banner` rather than at its own `--z-tooltip` (600).
183
+ At 1440x900 the tooltip and the toast card never meet; at 390x844 they overlap
184
+ by 260x45px, so a banner tooltip opened with both a modal and a toast up is
185
+ drawn under the toast. Hover/focus-only, purely visual, and recorded beside the
186
+ scale.
187
+
188
+ **Consumers that redefine these tokens in their own `:root` after the engine
189
+ import still win**, as they always have, so an app carrying a local copy of the
190
+ scale keeps the old order (and the bug) until it drops the copy.
191
+
192
+ Two literals moved with the tiers: `#toast-container` and `.toast-page-blur` in
193
+ `layouts/studio/_flash` carry `var(--studio-toast-z, var(--z-toast, …))`
194
+ fallbacks, and a fallback that disagreed with the scale would hand the bug to
195
+ any app rendering the partial without the engine sheet.
196
+
197
+ **The existing test could not have caught this and still cannot on its own.**
198
+ `layer_scale_contract_test` asserted `--z-toast > --z-modal`, which was true the
199
+ entire time the toast was unusable — both tiers cleared the modal, which says
200
+ nothing about which of *them* wins. It now names the banner, and pins the halo
201
+ directly beneath its own toast so no tier can settle between them. The property
202
+ is also asserted where the defect lives, in `e2e/toast_over_banner.spec.js`: a
203
+ hit test and a real mouse click at the Dismiss button, at desktop and phone
204
+ widths, at scroll-top and at an offset. And once more on the artifact a
205
+ consumer is actually served — `test/integration/layer_scale_build_test.rb`
206
+ runs the real Tailwind binary over the engine's entry point and reads the
207
+ tiers out of the COMPILED bundle, where `@import` resolution, layer ordering
208
+ and a shadowing `:root` are all in play and a source read sees none of them. One trap is recorded there — at 390px
209
+ the point under that button is the banner's Email link, so on the broken build a
210
+ click *navigated* and the toast count on the new page was zero; asserting the
211
+ count alone passes over the bug.
212
+
213
+
122
214
  ### Breaking
123
215
 
124
216
  - **The age-gate DOB modal is renamed, and the refusal moved to its own card.**
@@ -63,12 +63,51 @@
63
63
  --z-modal: 200; /* modal backdrop + card — THE app blocker */
64
64
  --z-lightbox: 210; /* a lightbox opened from inside a modal */
65
65
  --z-alert: 300; /* transient overlays that must not be missed */
66
- --z-toast-blur: 399; /* the frosted halo behind the toasts */
67
- --z-toast: 400; /* toasts */
68
- --z-banner: 500; /* environment / DEV MODE reachable mid-modal */
66
+ --z-banner: 400; /* environment / DEV MODE reachable mid-modal */
67
+ --z-toast-blur: 499; /* the frosted halo behind the toasts */
68
+ --z-toast: 500; /* toasts ABOVE the banner; see below */
69
69
  --z-tooltip: 600; /* tooltips on the banner, so they clear it */
70
70
  }
71
71
 
72
+ /* WHY THE TOAST OUTRANKS THE BANNER, and why it did not used to.
73
+
74
+ The first cut of this scale put --z-banner (500) over --z-toast (400) on the
75
+ reasoning that the environment bars must stay reachable mid-modal. They must
76
+ — but so must a toast, and the two claims collided the moment BOTH were true
77
+ at once, because they occupy the same corner of the viewport.
78
+
79
+ THE DEFECT, MEASURED on mcritchie-studio at 1440x900 and 390x844, dark and
80
+ light, scrollY 0 and 900. #toast-container is fixed at top 0 with 1rem of
81
+ padding, so a toast card sits at roughly y16-92. With a modal open, the rule
82
+ below lifts the bar stack to sticky top 0 at --z-banner, so it owns y0-47 —
83
+ overlapping the toast's top edge, which is where the Dismiss button lives.
84
+ `document.elementFromPoint` at that button returned the BANNER div, and a
85
+ real click left the toast count at 1. With no modal open it returned the
86
+ button's own SVG and the count dropped to 0.
87
+
88
+ That is not a cosmetic overlap. A toast carrying action buttons is given
89
+ `duration: 0` by the toast manager (layouts/studio/_flash), so it never
90
+ auto-dismisses — the Dismiss button is its ONLY exit, and under a lifted
91
+ banner the toast is stuck on the page for the rest of the session.
92
+
93
+ SO THE ORDER IS A SEMANTIC DECISION, not a workaround for the geometry: a
94
+ TOAST IS TRANSIENT AND DEMANDS INTERACTION; a BANNER IS PERSISTENT CHROME
95
+ that will still be there afterwards. The thing asking to be dealt with wins.
96
+ Two alternatives were considered and rejected — lowering the modal-open lift
97
+ below the toast (narrower blast radius, but leaves the scale saying something
98
+ nobody believes) and offsetting #toast-container while modal-open (treats the
99
+ symptom; the toast goes back under the banner the day either one grows).
100
+
101
+ WHAT THIS COSTS, MEASURED RATHER THAN ASSUMED. --z-tooltip (600) still clears
102
+ both tiers, but a banner button's tooltip is a DESCENDANT of the bar stack —
103
+ and while a modal is open the rule below gives that stack a z-index, which
104
+ makes it a stacking context and clamps every descendant inside it. So the
105
+ tooltip now composites at --z-banner rather than at 600. At 1440x900 the two
106
+ never meet (tooltip x1083-1343, toast card x496-944); at 390x844 they overlap
107
+ by 260x45px, so a banner tooltip opened with a modal AND a toast up is drawn
108
+ under the toast card. It is hover/focus-only, purely visual (the tooltip is
109
+ pointer-events:none), and it is the accepted cost of this ordering. */
110
+
72
111
  /* The environment / impersonation bars stay in NORMAL FLOW at rest
73
112
  (banners/_stack records why: pinning them in the RESTING state needs one
74
113
  pinned sibling to measure the other, and the measured version visibly
@@ -110,7 +149,7 @@ body.modal-open .studio-bar-stack,
110
149
  body.modal-open .studio-app-banner {
111
150
  position: sticky;
112
151
  top: 0;
113
- z-index: var(--z-banner, 500);
152
+ z-index: var(--z-banner, 400);
114
153
  }
115
154
 
116
155
  /* -- Surfaces -------------------------------------------------------------- */
@@ -17,8 +17,18 @@
17
17
  bare 60/55 that sat below every app navbar. A toast is the one thing that
18
18
  must surface over an open modal, so --z-toast is above --z-modal and an app
19
19
  that never overrides gets that for free. Apps that DO override should set
20
- the tier (--z-toast) instead, and drop the per-component name. */
21
- #toast-container { z-index: var(--studio-toast-z, var(--z-toast, 400)); }
20
+ the tier (--z-toast) instead, and drop the per-component name.
21
+
22
+ ABOVE THE BANNER TOO, and the fallbacks below have to say so. A toast and
23
+ the environment bars share the top of the viewport, and with a modal open
24
+ engine.css lifts those bars to --z-banner at top 0 — which used to sit at
25
+ 500, over the toast's 400, so the toast's Dismiss button was covered and a
26
+ click landed on the banner. A toast carrying buttons is given duration 0 by
27
+ the manager below, so that button is its only exit: covered, the toast was
28
+ stuck forever. The tiers were reordered (engine.css carries the argument);
29
+ these literals are what an app gets when the engine sheet is absent, so
30
+ they must carry the SAME order or that app keeps the bug. */
31
+ #toast-container { z-index: var(--studio-toast-z, var(--z-toast, 500)); }
22
32
  .toast-shadow-all {
23
33
  box-shadow: 0 0 30px rgba(0,0,0,0.4), 0 0 10px rgba(0,0,0,0.2);
24
34
  }
@@ -65,7 +75,7 @@
65
75
  mask-image: radial-gradient(ellipse 70% 80% at 50% 0%, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 40%, rgba(0,0,0,0) 100%);
66
76
  -webkit-mask-image: radial-gradient(ellipse 70% 80% at 50% 0%, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 40%, rgba(0,0,0,0) 100%);
67
77
  pointer-events: none;
68
- z-index: var(--studio-toast-blur-z, var(--z-toast-blur, 399));
78
+ z-index: var(--studio-toast-blur-z, var(--z-toast-blur, 499));
69
79
  transition: opacity 0.5s ease;
70
80
  background: rgba(255, 255, 255, 0.15);
71
81
  }
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.65.1"
2
+ VERSION = "0.65.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.65.1
4
+ version: 0.65.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails