studio-engine 0.46.0 → 0.47.0

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: 026c018bce499134d15e23ca2f9101e05da752380be210e5c6136babbac97a5a
4
- data.tar.gz: ff33bee4cbe01664a9f2385716107d0a7319e0b267cc9a1f6c4a47e0555c4c5a
3
+ metadata.gz: d1da7736255e9cf6690f07488e8758227bd6231e08502d123dcbefbdd6f6c685
4
+ data.tar.gz: a8e2e861599996fdb22203bb9ed42feae6f5343232a1c0578c73fd95f28b8b81
5
5
  SHA512:
6
- metadata.gz: 25dedda1d9c8f5c7cc2f485cfcea80e94ce3446952029015d5e53ab1dd41fdbfd014dc1c1251f9c637ab7fbd3605b7aa50e0593e4d4e85b1a81c6349536dd7ea
7
- data.tar.gz: 8f71ee45ddf77e12851991857aff2a72bfe3082719b7505c16b8fc7cd08d17c5bf502c07af2393aa315c22146067ef63af1d275d9566d327193d9fa935a94a72
6
+ metadata.gz: 6f1effe6341c220633e267f9382cf5dc2ca68565565489b95feceb744003aceaca151663a0cdba25c7c11753f99ac78369c4c4327232436628f55b30c40afcf1
7
+ data.tar.gz: 33526cb24c80fc6e26ada46d15fbbd5701f5681af7cd4a9feaab2bfcea6c92e8283b7d50d1408cc8f160c2624ed5038968509fbb37ae3783ae003f0381ef1500
data/CHANGELOG.md CHANGED
@@ -35,6 +35,60 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
35
35
 
36
36
  ### Added
37
37
 
38
+ - **`$store.modals.isLive(id)` — "is this card up?", the question `isOpen` cannot
39
+ answer.** `close()` flips `_closing` immediately and splices the entry only
40
+ after the exit animation, so for that whole ~220ms window `isOpen(id)` reports
41
+ a card that is already leaving the screen as open. Every consumer guard that
42
+ means *don't open this twice* wants the opposite answer, and turf-monster had
43
+ hand-rolled the same `_closing`-aware `.some()` over `.stack` to get it.
44
+
45
+ `isLive(id)` returns true when a card with that id is on the stack **and not on
46
+ its way out**. It also accepts an array — `isLive(['onboarding', 'age-verify',
47
+ 'wallet-setup'])` — so a caller can ask "is any card of this flow still up?"
48
+ without reaching into `.stack` and re-deriving the lifecycle rule. Shipped on
49
+ both the shared host and the page-scoped host.
50
+
51
+ **`isOpen(id)` is unchanged.** It still answers "on the stack, in any state",
52
+ which is what a caller asking "is the DOM node still mounted" depends on. This
53
+ is additive; no existing call site changes behavior. The proposal on the task
54
+ was `isOpen(id, { ignoreClosing: true })`, rejected because it reads as a double
55
+ negative at the call site (*is it open, ignoring that it is closing*) and
56
+ because two genuinely different questions are clearer as two names than as one
57
+ name with a mode flag.
58
+
59
+ The asymmetry to know, and the reason this is not simply "not animating":
60
+
61
+ | Transition | Flags on the entry | `isOpen` | `isLive` |
62
+ |---|---|---|---|
63
+ | `open()` | — | true | **true** |
64
+ | `close()` | `_closing` | true | **false** |
65
+ | `swap()`, leaving entry | `_swappingOut` + `_closing` | true | **false** |
66
+ | `advance()` | `_swappingOut` only | true | **true** |
67
+ | spliced / never opened | — | false | **false** |
68
+
69
+ `advance()` slides the SAME card between steps of its own flow, so it sets
70
+ `_swappingOut` and nothing else. `isLive` therefore tests `_closing` alone and
71
+ never consults `_swappingOut`: conflating the two reports a card as gone in the
72
+ middle of a step transition, which is how an in-flow hand-off starts looking to
73
+ a caller like a dismissal. Both halves are pinned by tests that execute the
74
+ store under node (`test/views/modal_host_store_behavior_test.rb`) — dropping the
75
+ `_closing` check and conflating it with `_swappingOut` redden *different*
76
+ assertions.
77
+
78
+ **Consumers that fork the host do not get this from a version bump.**
79
+ `mcritchie-studio` and `turf-monster` both ship their own
80
+ `app/views/studio/modals/_host.html.erb`, and a non-isolated engine lets the app
81
+ copy shadow the gem's. Those apps pick `isLive` up only by porting it into their
82
+ fork or by retiring the fork; `studio/modals/scoped_host` is unforked everywhere
83
+ and carries it today. turf-monster's two hand-rolled scans — the onboarding
84
+ chain driver's `open()` guard (`layouts/application.html.erb`) and
85
+ `selectionBoard#showWalletSetupModal` (`contests/_turf_totals_board.html.erb`)
86
+ — can then collapse onto this primitive, as can its two remaining bare
87
+ `isOpen()` idempotence guards (`app/javascript/solana_stores.js` for
88
+ `wallet-changed`, `cdp/returns/show.html.erb` for `cdp-ramp`), which are the
89
+ exact defect `isLive` exists to prevent. A turf-side follow-up, not part of
90
+ this change.
91
+
38
92
  - **The standard user profile columns — and the engine's first migration against
39
93
  a host-owned table.** Every other engine migration creates a `studio_*` table
40
94
  the engine owns outright; `users` belongs to the host. That boundary is crossed
data/README.md CHANGED
@@ -258,6 +258,14 @@ The scoped host takes its animations from `engine-motion.css` rather than an
258
258
  inline copy, so a consumer bundling that layer gets the same spring as the shared
259
259
  host.
260
260
 
261
+ It carries the store API below with three deliberate differences: there is no
262
+ `advance()`; `swap()` replaces the top entry immediately rather than running the
263
+ shared host's directional slide — so no entry is ever left mid-transition; and it
264
+ does not read the `window.ModalAnimations` registry, so the `enterAnim` /
265
+ `exitAnim` props documented under the table are ignored and the exit always plays
266
+ the 220ms unmount from `engine-motion.css`. `isOpen` and `isLive` behave
267
+ identically on both.
268
+
261
269
  Store API (`Alpine.store('modals')`):
262
270
 
263
271
  | Call | Behavior |
@@ -268,7 +276,17 @@ Store API (`Alpine.store('modals')`):
268
276
  | `close()` | Animated close of the current modal (no-op if already closing). |
269
277
  | `closeAll()` | Instant, unanimated clear (used by navigation cleanup). |
270
278
  | `closeAllDismissible()` | Clears all modals except those opened with `dismissible: false`. |
271
- | `isOpen(id)` / `current()` | Introspection. |
279
+ | `isOpen(id)` / `current()` | Introspection. `isOpen` is true while a card is ON THE STACK — including the whole exit animation, because `close()` flips `_closing` at once and splices only after it. |
280
+ | `isLive(id)` / `isLive([id, …])` | True when a card with that id is up and **not on its way out**. Pass an array to ask about a set of ids ("is any card of this flow still up?"). |
281
+
282
+ **`isOpen` or `isLive`?** A guard that means "don't open this twice" almost
283
+ always wants `isLive`. `isOpen` stays true for the ~220ms exit window, so an
284
+ idempotence check built on it refuses to reopen a card the user just dismissed.
285
+ The one asymmetry worth knowing: `advance()` slides the *same* card between
286
+ steps of its own flow and sets only `_swappingOut`, so a mid-advance card is
287
+ still **live** — only `close()` and the outgoing half of a `swap()` set
288
+ `_closing`. `isLive` tests `_closing` alone, deliberately; conflating it with
289
+ `_swappingOut` makes an in-flow hand-off look like a dismissal.
272
290
 
273
291
  Recognized props: `dismissible: false` disables escape/click-outside dismissal
274
292
  (e.g. an in-flight transaction); `enterAnim` / `exitAnim` pick a named
@@ -390,12 +390,51 @@
390
390
  });
391
391
  this._sync();
392
392
  },
393
+ // isOpen(id) — is a card with this id ON THE STACK, in any
394
+ // lifecycle state? A card mid-close still counts: close() flips
395
+ // _closing at once and splices only after the exit animation, so
396
+ // this answers TRUE for that whole exit window. Unchanged — callers
397
+ // asking "is the DOM node still mounted" depend on exactly this.
398
+ // If you mean "is it still up", you want isLive() below.
393
399
  isOpen: function(id) {
394
400
  for (var i = 0; i < this.stack.length; i++) {
395
401
  if (this.stack[i].id === id) return true;
396
402
  }
397
403
  return false;
398
404
  },
405
+ // isLive(id) / isLive([id, ...]) — is a card with this id on the
406
+ // stack and NOT on its way out? This is what a "don't open it
407
+ // twice" guard actually means, and it is the question isOpen
408
+ // cannot answer.
409
+ //
410
+ // The difference is a TIMING one, and the asymmetry is load-bearing:
411
+ //
412
+ // close() entry gets _closing, and is spliced
413
+ // CLOSE_ANIM_MS later. isOpen stays TRUE for
414
+ // that window; isLive is FALSE immediately.
415
+ // swap() / open(..., the LEAVING entry gets _swappingOut AND
416
+ // { replace: true }) _closing — a different card is taking the
417
+ // screen, so it is not live either.
418
+ // advance() the entry gets _swappingOut and NOTHING
419
+ // else. Same card, same id, sliding between
420
+ // steps of its own flow. It stays LIVE.
421
+ //
422
+ // So this tests _closing ALONE and deliberately never consults
423
+ // _swappingOut. Conflating the two reports a card as gone in the
424
+ // middle of a step transition, which is how an in-flow hand-off
425
+ // starts looking to a caller like a dismissal.
426
+ //
427
+ // Accepts an array so a caller can ask about a SET of ids ("is any
428
+ // card belonging to this flow still up?") without reaching into
429
+ // .stack and re-deriving the lifecycle rule for itself.
430
+ isLive: function(id) {
431
+ var ids = Array.isArray(id) ? id : [id];
432
+ for (var i = 0; i < this.stack.length; i++) {
433
+ var entry = this.stack[i];
434
+ if (entry && !entry._closing && ids.indexOf(entry.id) !== -1) return true;
435
+ }
436
+ return false;
437
+ },
399
438
  current: function() {
400
439
  return this.stack.length ? this.stack[this.stack.length - 1] : null;
401
440
  },
@@ -138,10 +138,26 @@
138
138
  this._sync();
139
139
  },
140
140
 
141
+ // isOpen(id) — on the stack in ANY lifecycle state, including a card
142
+ // mid-close (close() flips _closing at once and splices only after
143
+ // CLOSE_ANIM_MS). isLive() below is the "still up" question.
141
144
  isOpen: function(id) {
142
145
  return this.stack.some(function(entry) { return entry.id === id; });
143
146
  },
144
147
 
148
+ // isLive(id) / isLive([id, ...]) — on the stack and NOT on its way
149
+ // out. Mirrors the shared host's primitive so a page-scoped store
150
+ // answers the same question the same way; see studio/modals/_host
151
+ // for the full lifecycle rationale. Tests _closing ALONE: a leaving
152
+ // swap() entry carries _swappingOut AND _closing, so _swappingOut on
153
+ // its own must never count as leaving.
154
+ isLive: function(id) {
155
+ var ids = Array.isArray(id) ? id : [id];
156
+ return this.stack.some(function(entry) {
157
+ return entry && !entry._closing && ids.indexOf(entry.id) !== -1;
158
+ });
159
+ },
160
+
145
161
  current: function() {
146
162
  return this.stack.length ? this.stack[this.stack.length - 1] : null;
147
163
  },
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.46.0"
2
+ VERSION = "0.47.0"
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.46.0
4
+ version: 0.47.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie