studio-engine 0.74.8 → 0.74.9

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: c8073dcfcec3579f3a3fa810e5b851ac3eb38a99e38bb184183b4d6a13d8da6e
4
- data.tar.gz: 3f7bc2747c4101c7785b3f3c22b673f80467e600b88af943900d1c3a4c79fdbd
3
+ metadata.gz: 54ff8e7d4414fba9258118e2e467a259121eaab850d8ec8cfb574b95acdf8b22
4
+ data.tar.gz: fdac442335eaa412bc6084ca82b9bc3b9bd7a89eb110a3857d528f89109835dd
5
5
  SHA512:
6
- metadata.gz: d57ba1e14f875f76dd2cc2e5d75c5db00decb0f5235222dfabec4501c9ae3f6b821720188f6c84aca67b68d3b2abb99d0a9022eaa8da205955820029d2e9a19b
7
- data.tar.gz: 1165eedf5e6bce8f56d60c83f0cdf96cd87bdd0c297c48906c88f782d20bbb8af102913272cf3663b73972cfd6d76474452e6900489ab2035dfee98d777673ca
6
+ metadata.gz: 5c1619cd191b89affc30ca2182e1cd2f09963afd2c822a9ef335fe67baa8064c21420451dcd046f5fb26258cc14caca45d5cab9e4800e2e64ba74682e8afc311
7
+ data.tar.gz: 24a428027cc14be60d49771a2721bc31b974ab456fd6c553d97c78463d23de1effa6aa2f56eb4231b4df0b6cff73ca0bc2c171d21f702f42e29de1e9375413a3
data/CHANGELOG.md CHANGED
@@ -1,9 +1,23 @@
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)). `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`.
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)). Write entries under `## Unreleased` and never write a version heading: when `prepare` allocates a version it rolls this file in the same commit, moving everything under `## Unreleased` beneath a new `## <version> — <date>` heading (written even when the bucket is empty) and leaving `## Unreleased` first and empty. It refuses the sweep, with nothing published, when the roll would lie or it cannot read this file; when it skips allocation, nothing rolls. [`docs/RELEASE.md`](./docs/RELEASE.md), *Rolling `Unreleased` into a version*, says when each happens and what to do by hand. `test/docs/changelog_structure_test.rb` guards the shape.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Fixed
8
+
9
+ - **The auth resend footer's error line failed WCAG AA in both themes; it now
10
+ uses the danger ink.** `studio/modals/auth/_resend_footer` painted its
11
+ `role="alert"` paragraph with the static `text-red-400`, which Tailwind v4
12
+ compiles to about `#FF6467`: 3.86:1 on the dark modal card and 2.89:1 on the
13
+ light one, both under the 4.5:1 that 12px text needs. It is now
14
+ `text-danger-ink`, the red `Studio::ThemeResolver` derives per theme to clear
15
+ 4.5:1 on every surface: 4.50:1 dark and 5.76:1 light on the default modal
16
+ card. The resend spinner stays the engine's `.spinner`, tuned to
17
+ turf-monster's currentColor ring (`--spinner-track: currentColor;
18
+ --spinner-color: transparent`), so a host that drops its fork sees no change.
19
+ Guarded by `test/views/resend_footer_error_contrast_test.rb`.
20
+
7
21
  ### Docs
8
22
 
9
23
  - **Thirty-five minor versions of shipped entries left `## Unreleased` and moved
@@ -32,8 +46,29 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
32
46
  fails loudly rather than passing vacuously, and — the one that would have caught
33
47
  this in week one — `Studio::VERSION` no more than two minor versions ahead of the
34
48
  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.
49
+ entries are still filed under `## Unreleased`". ~~Automating the roll inside
50
+ `bin/release prepare` (mcritchie-studio) is the real fix and is not done.~~ —
51
+ **superseded after 0.74.8:** mcritchie-studio#1344 made `prepare` roll this
52
+ file whenever it allocates a version, so "it never touches this file" above
53
+ describes the tooling as of 0.74.8. See the entry on the automatic roll below.
54
+
55
+ - **The docs called the `## Unreleased` roll a manual conductor step; `bin/release
56
+ prepare` does it now, and the docs say exactly when it does not.**
57
+ mcritchie-studio#1344 made prepare roll this file into the allocated version in
58
+ the commit that sets `lib/studio/version.rb` and `Gemfile.lock`, and REFUSE the
59
+ sweep, with nothing published, when the roll would lie. `docs/RELEASE.md`
60
+ (*Rolling `Unreleased` into a version*), this file's preamble and the README's
61
+ release paragraph still said the roll was yours, by hand. They now name the
62
+ three outcomes (ALLOCATE rolls, SKIP rolls nothing, REFUSE aborts), each
63
+ refusal and its fix, and the reach limit: a SKIP (a version set by hand, or a
64
+ re-run after an abort before the tag) rolls nothing. They also settle three
65
+ conventions. An entry-less release still gets its heading; a heading never
66
+ lands before its version does, because `accepted` lags `release`; and a merge
67
+ that crosses a roll gets checked, because git can merge a new bullet CLEAN
68
+ under a version that shipped without it. The manual procedure stays, as the
69
+ remedy for those cases. No runtime code changed;
70
+ `test/docs/changelog_structure_test.rb` changed only its comments and its
71
+ failure message.
37
72
 
38
73
  ## 0.74.3 — 2026-09-08
39
74
 
data/README.md CHANGED
@@ -240,35 +240,78 @@ the block:
240
240
 
241
241
  #### Writing a modal's content partial — two rules the host imposes
242
242
 
243
- Both of these fail SILENTLY. Nothing raises, nothing logs, and the card renders;
244
- it just does less than it looks like it does.
245
-
246
- **1. SINGLE ROOT.** A content partial's outer `<div>` is the host's required
247
- root. Alpine's `<template x-if>` clones only the FIRST root element of its
248
- content, so a second top-level sibling a stray `<span>`, a trailing `<style>`
249
- block, a comment-turned-node is dropped on the floor. Bake anything extra
250
- inside the wrapping `<div>`.
251
-
252
- **2. NO DOUBLE QUOTE INSIDE `x-data`.** The attribute is double-quoted, so an
253
- inner double quote CLOSES it. Alpine then mounts a component whose expression is
254
- truncated mid-statement: every element still renders and every handler does
255
- nothing. Single-quote everything inside `x-data`.
243
+ Both rules let the card render and then do less than it looks like it does.
244
+ They differ in whether anything reaches the CONSOLE, which is the first thing
245
+ to check when a registered partial misbehaves.
246
+
247
+ **1. SINGLE ROOT fails in total silence.** A content partial's outer `<div>`
248
+ is the host's required root. Alpine's `<template x-if>` clones only the FIRST
249
+ root element of its content, so a second top-level sibling a stray `<span>`,
250
+ a trailing `<style>` block, a comment-turned-node — is dropped on the floor.
251
+ Nothing raises and nothing logs: `x-if` takes `.firstElementChild` and asks no
252
+ questions. (Alpine DOES warn on a multi-root `x-for` template *"x-for
253
+ templates require a single root element, additional elements will be ignored"*
254
+ which is exactly why this one catches people out. The modal host is `x-if`,
255
+ and `x-if` ships no such check.) Bake anything extra inside the wrapping
256
+ `<div>`.
257
+
258
+ **2. NO DOUBLE QUOTE INSIDE `x-data` — this one LOGS.** The attribute is
259
+ double-quoted, so an inner double quote CLOSES it. Alpine then mounts a
260
+ component whose expression is truncated mid-statement: every element still
261
+ renders and every handler does nothing. It is NOT silent. The truncation
262
+ leaves a JavaScript syntax error, Alpine's evaluator catches it, and the
263
+ default handler prints `Alpine Expression Error: …` together with the
264
+ offending expression — a `console.warn`, followed by an async rethrow. That
265
+ warning is the best clue this rule was broken, so send a debugging engineer to
266
+ the console rather than to the markup. Single-quote everything inside
267
+ `x-data`.
268
+
269
+ Interpolated values are the same rule arriving from the server, and Rails
270
+ already guards the ordinary case: `<%= value %>` inside `x-data` renders a
271
+ double quote as `&quot;`, and the HTML tokenizer never reads a character
272
+ reference as the closing quote — it decodes it straight into the value — so
273
+ the attribute survives. The vector is a value that SKIPS that escaping —
274
+ `raw`, `.html_safe`, or a helper returning a SafeBuffer — which puts a bare
275
+ `"` into the attribute and closes it exactly as a typed one would.
276
+ `escape_javascript` (`j`) is NOT the guard here: it escapes for a JavaScript
277
+ string literal (`\"`) and PRESERVES the html_safe flag, so the raw quote still
278
+ reaches the attribute. Let Rails escape it.
279
+
280
+ That settles the ATTRIBUTE, not the JavaScript inside it. A value spliced into
281
+ a single-quoted JS string — the shape rule 2 steers you to — has a second way
282
+ out: a `'`. ERB escapes it to `&#39;`, the parser decodes that straight back
283
+ into the value, and the bare `'` ends the string: the same
284
+ `Alpine Expression Error`. Route such a value through
285
+ `Studio::JsLiteral.in_attribute(value)`, which runs both escapers in the order
286
+ that works; `lib/studio/js_literal.rb` carries the full contract, identifier
287
+ position (`Studio::JsIdentifier`) included.
256
288
 
257
289
  These are properties of the HOST, not of any one card, and they apply to every
258
- consumer partial an app registers — not only to the specimens in this gem. They
259
- are recorded here because they used to be recorded only in the style guide's
260
- specimen headers, which made them deletable by a change that removed a specimen:
261
- retiring the `style/modals/_network_guard` and `_wallet_deposit` mirrors on
262
- 2026-09-09 would otherwise have taken the single-root rule out of both this repo
263
- and the app that inherited those cards.
264
-
265
- **Wallet modals moved to `solana-studio`.** The Connect Wallet picker, the Web3
266
- step-up card and the Phantom deep link used to ship here as
267
- `studio/modals/wallet_connect`, `studio/modals/web3_step_up` and
268
- `studio/solana/phantom_deeplink`. They now live in the **solana-studio** gem as
269
- `solana_studio/modals/wallet_connect`, `solana_studio/modals/web3_step_up` and
270
- `solana_studio/phantom_deeplink`; render them from those paths and read that
271
- gem's README for their locals and hooks.
290
+ consumer partial an app registers — not only to the specimens in this gem.
291
+
292
+ They are recorded HERE because this is the host's CONTRACT, and nothing else
293
+ in the repo is. Plenty of files state one or both rules, but each of them is
294
+ describing ITSELF a specimen header explaining its own markup, an asset
295
+ partial explaining its own root. Someone writing a NEW partial has no specimen
296
+ to read; they read the host's documentation, so that is where the rules have
297
+ to live.
298
+
299
+ And a specimen could not carry rule 1 even in principle, which is what settles
300
+ it: the style guide wraps EVERY registration's partial in a `<div>` of its own
301
+ `<template x-if="…"><div><%= render … %></div></template>` — and that
302
+ wrapper supplies the single root the rule is about. A specimen that broke the
303
+ rule would still render correctly in the guide, so the specimens cannot
304
+ demonstrate the failure they would be documenting. Pinned by
305
+ `test/docs/modal_host_contract_docs_test.rb`.
306
+
307
+ #### Wallet modals moved to `solana-studio`
308
+
309
+ The Connect Wallet picker, the Web3 step-up card and the Phantom deep link used
310
+ to ship here as `studio/modals/wallet_connect`, `studio/modals/web3_step_up`
311
+ and `studio/solana/phantom_deeplink`. They now live in the **solana-studio**
312
+ gem as `solana_studio/modals/wallet_connect`, `solana_studio/modals/web3_step_up`
313
+ and `solana_studio/phantom_deeplink`; render them from those paths and read
314
+ that gem's README for their locals and hooks.
272
315
 
273
316
  This is the two-template split: **BASE** is studio-engine + mcritchie-studio,
274
317
  **WEB3 ADD** is solana-studio + turf-monster. The engine has no business
@@ -815,11 +858,14 @@ let it allocate the version — it derives the bump, commits `lib/studio/version
815
858
  with its `Gemfile.lock` onto **`origin/release`**, then publishes, tags, and bumps
816
859
  each consumer's lock. **Do not set the version by hand.** A hand-set number makes
817
860
  the allocation read the current version as already past the last tag and skip, so
818
- the hand number silently wins over the derived one. **`prepare` does not touch
819
- [`CHANGELOG.md`](./CHANGELOG.md)** renaming `## Unreleased` to the allocated
820
- version and opening a fresh empty one is a manual last step, and skipping it is
821
- how thirty-five minor versions of entries ended up filed as pending. Details and the
822
- exact commands are in [`docs/RELEASE.md`](./docs/RELEASE.md).
861
+ the hand number silently wins over the derived one, and the skip rolls no
862
+ changelog. **When it allocates, `prepare` also rolls
863
+ [`CHANGELOG.md`](./CHANGELOG.md)** in the same commit: everything under
864
+ `## Unreleased` moves beneath the new version's heading, written even when the
865
+ bucket is empty, and `## Unreleased` stays first and empty. It refuses the sweep,
866
+ with nothing published, when the roll would lie or it cannot read the file.
867
+ [`docs/RELEASE.md`](./docs/RELEASE.md), *Rolling `Unreleased` into a version*,
868
+ says exactly when it rolls, refuses or skips, and what to do by hand.
823
869
 
824
870
  **Semver guide** — the release *derives* the bump from its members (a `breaking`
825
871
  risk tag → major, a `feature` → minor, otherwise patch), so this is what those
@@ -191,6 +191,13 @@
191
191
  Usage: <span class="spinner"></span>
192
192
  <span class="spinner" style="--spinner-size: 2rem"></span>
193
193
  on a colored button: <span class="spinner" style="--spinner-color: currentColor"></span>
194
+ the currentColor RING the engine's own CTA blocks use, matching the
195
+ ring turf-monster's host-only .cta-spinner draws:
196
+ <span class="spinner" style="--spinner-track: currentColor; --spinner-color: transparent; opacity: 0.85"></span>
197
+ Mind the mapping, it reads backwards: --spinner-track paints the
198
+ WHOLE ring and --spinner-color only the leading segment. Untuned on a
199
+ .btn-primary, the head is --color-cta, the button's own fill, so the
200
+ spinner shows nothing but its faint track.
194
201
  Tune: --spinner-size / --spinner-thickness / --spinner-track /
195
202
  --spinner-color / --spinner-speed
196
203
  ========================================================================== */
@@ -32,7 +32,11 @@
32
32
  IMPOSES on the content partials registered in its block: a SINGLE ROOT
33
33
  element (a template x-if clones only the first one; later siblings are
34
34
  dropped) and NO double quote inside x-data (it closes the attribute and
35
- Alpine mounts a silent no-op). Both fail without raising.
35
+ Alpine mounts a no-op). NEITHER STOPS THE RENDER, BUT ONLY ONE IS SILENT:
36
+ the single-root miss logs nothing at all, while the truncated x-data prints
37
+ "Alpine Expression Error" to the console (and rethrows it async). That
38
+ warning is the clue rule one never gives you, so check the console before
39
+ you re-read the markup.
36
40
  %>
37
41
  <style>
38
42
  /* Scroll lock applied by $store.modals._sync() when the stack is non-empty.
@@ -41,11 +41,12 @@
41
41
  chaining (`current()?.id`): the outer template unmounts one tick AFTER the
42
42
  stack empties, so a bare `.id` throws on every close.
43
43
 
44
- THE CONTENT PARTIALS OBEY THE SAME TWO RULES AS THE SHARED HOST'S, and both
45
- fail silently: a SINGLE ROOT element (a template x-if clones only the first
46
- one) and NO double quote inside x-data (it closes the attribute and Alpine
47
- mounts a component that renders everything and does nothing). Stated in full
48
- in the gem README's "Modal host" section.
44
+ THE CONTENT PARTIALS OBEY THE SAME TWO RULES AS THE SHARED HOST'S, and they
45
+ fail DIFFERENTLY: a SINGLE ROOT element (a template x-if clones only the
46
+ first one, logging nothing) and NO double quote inside x-data (it closes the
47
+ attribute and Alpine mounts a component that renders everything and does
48
+ nothing -- but LOGS "Alpine Expression Error" on the way). Stated in full in
49
+ the gem README's "Modal host" section.
49
50
 
50
51
  AND EVERY ID REGISTERED HERE MUST BE REACHABLE, both ways round. A card that
51
52
  opens an id no template registers shows an EMPTY panel; a template no card
@@ -10,6 +10,29 @@
10
10
 
11
11
  Locals:
12
12
  modal_store — Alpine store name backing close(). Default "modals".
13
+
14
+ ERROR INK, not a static red. The error paragraph is text-danger-ink, the
15
+ per-theme red Studio::ThemeResolver derives to a 4.5:1 target. It replaced
16
+ text-red-400. Measured through the resolver on turf-monster's real palette
17
+ (primary 4BAF50), on the modal card, which is bg-surface: danger-ink is 4.50
18
+ on the dark card 3C3853 and 5.76 on the light card ffffff, clearing AA on
19
+ both; text-red-400 was 3.86 and 2.89, failing both. Those are Tailwind v4's
20
+ red-400, about FF6467, which is what these apps compile; the v3 hex F87171
21
+ gives 4.03 and 2.77 and fails the same way. No STATIC red clears AA
22
+ on BOTH themes — theme_resolver.rb carries the measured table and derives
23
+ --color-danger-ink precisely to be the text red. --color-danger is the brand
24
+ FILL and is free to be vivid; it is not this. Guarded, executably, by
25
+ test/views/resend_footer_error_contrast_test.rb.
26
+
27
+ SPINNER, tuned rather than re-named. This is the engine's canonical .spinner
28
+ primitive (engine-motion.css), tuned through its OWN documented custom
29
+ properties to the currentColor ring turf-monster paints with its host-only
30
+ .cta-spinner — so the block looks unchanged once turf renders it, and the
31
+ engine names no class it cannot define. Read the mapping carefully, because
32
+ it looks backwards: --spinner-track colours the WHOLE ring and
33
+ --spinner-color colours only the leading segment. A currentColor ring with a
34
+ transparent gap is therefore track currentColor plus color transparent, NOT
35
+ the reverse, which would draw a lone quarter-arc instead.
13
36
  %>
14
37
  <%
15
38
  # IDENTIFIER position ($store.<name>) — VALIDATED, never escaped: Studio::JsIdentifier.
@@ -19,9 +42,10 @@
19
42
  <button @click="resendMagicLink()" :disabled="props.submitting === 'magic-link' || (props.resendCooldown || 0) > 0"
20
43
  class="block mx-auto text-sm text-primary hover:text-primary-300 underline underline-offset-2 disabled:opacity-50 disabled:no-underline">
21
44
  <span class="inline-flex items-center justify-center gap-1.5">
22
- <span x-show="props.submitting === 'magic-link'" class="spinner" aria-hidden="true"></span>
45
+ <span x-show="props.submitting === 'magic-link'" class="spinner" aria-hidden="true"
46
+ style="--spinner-track: currentColor; --spinner-color: transparent; opacity: 0.85"></span>
23
47
  <span x-text="props.submitting === 'magic-link' ? 'Resending…' : ((props.resendCooldown || 0) > 0 ? ('Resend available in ' + props.resendCooldown + 's') : 'Resend link')"></span>
24
48
  </span>
25
49
  </button>
26
- <p role="alert" x-show="props.resendError" x-cloak class="text-xs text-red-400 mt-3 text-center" x-text="props.resendError"></p>
50
+ <p role="alert" x-show="props.resendError" x-cloak class="text-xs text-danger-ink mt-3 text-center" x-text="props.resendError"></p>
27
51
  <button @click="$store.<%= modal_store %>.close()" class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">Close</button>
@@ -131,7 +131,7 @@
131
131
  :disabled="!complete || submitting">
132
132
  <span x-show="!submitting">Confirm &amp; Continue</span>
133
133
  <span x-show="submitting" class="inline-flex items-center justify-center gap-2" style="display: none;">
134
- <span class="cta-spinner" aria-hidden="true"></span>
134
+ <span class="spinner" aria-hidden="true" style="--spinner-track: currentColor; --spinner-color: transparent; opacity: 0.85"></span>
135
135
  Verifying…
136
136
  </span>
137
137
  </button>
@@ -168,7 +168,7 @@
168
168
  <button type="submit" class="btn btn-primary btn-lg w-full" :disabled="saving || !changed">
169
169
  <span x-show="!saving" x-text="changed ? '<%= j cta_label %>' : '<%= j saved_label %>'"><%= cta_label %></span>
170
170
  <span x-show="saving" class="inline-flex items-center justify-center gap-2" style="display: none;">
171
- <span class="cta-spinner" aria-hidden="true"></span>
171
+ <span class="spinner" aria-hidden="true" style="--spinner-track: currentColor; --spinner-color: transparent; opacity: 0.85"></span>
172
172
  <span x-text="progressLabel || '<%= j saving_label %>'"><%= saving_label %></span>
173
173
  </span>
174
174
  </button>
@@ -377,7 +377,7 @@
377
377
  </template>
378
378
  <button type="submit" :disabled="submitting"
379
379
  class="btn btn-primary btn-lg w-full gap-2 disabled:cursor-wait">
380
- <span x-show="submitting" class="cta-spinner" aria-hidden="true"></span>
380
+ <span x-show="submitting" class="spinner" aria-hidden="true" style="--spinner-track: currentColor; --spinner-color: transparent; opacity: 0.85"></span>
381
381
  <span x-text="submitting ? 'Saving…' : 'Save and continue'"></span>
382
382
  </button>
383
383
  </form>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.74.8"
2
+ VERSION = "0.74.9"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.74.8
4
+ version: 0.74.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie