studio-engine 0.19.0 → 0.20.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: f49b9ef9699144b785efbe7d4839261bba26035970a6c411f5cd54d0606ea97c
4
- data.tar.gz: 3705abb5871fcfaa48b7c55613b0bc430b275c8bf9eb4c1625e43fcf14cfed32
3
+ metadata.gz: e3b84a5d11a22ad89cdbf077ba91c6d35374ba8a956c2fdc1207eb7033ec15c5
4
+ data.tar.gz: 510f09d35fb5dd3562c9435c1df1f5cf8001b9a34dd12678eb32822f91ded766
5
5
  SHA512:
6
- metadata.gz: 584cbbcdeeb388b1a6b0115a790fb6fa3d1c4ce4c69f42e57f7f0985edb4f6338578db04dcfeb5c60bde65aada69509db87c07151a52aab9f9348739a332cdca
7
- data.tar.gz: dbbe745f30a632c21d528e336dd323a21224e71d39f97caf6fc43f68f958d1cb78da1a8660d735393847bdfeb67a79d15c1f12c199446f665c0267ff521c19fd
6
+ metadata.gz: 4f6da2882ad34a600e5855c659ba525bda3d191737389d1b8348e0eca8eb01b1da2a48d7df55dec5b5072879acf6ba236439c1ac99fac2c82216ac68b8df6af3
7
+ data.tar.gz: dbe6979e76badd522119308b4c459b1ebd5a246fa549364b2b78d7ca86e8b828c93523899aaa0813f874d1bb95be37e4c6cd32dab492c60988dbae84051d9ceb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,68 @@
2
2
 
3
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 with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
4
4
 
5
+ ## 0.20.0 — 2026-07-27
6
+
7
+ Phase 1 of the studio-engine modal convergence: the engine now OWNS the wallet
8
+ brand icons, homes the entry-confirmed celebration, and gates the web3/leveling
9
+ enrichments behind the two independent capability flags.
10
+
11
+ ### Added
12
+
13
+ - **Wallet brand icons ship inline from the engine** — new
14
+ `studio/modals/blocks/_wallet_brand_sprite` emits a hidden inline-SVG sprite
15
+ with the Phantom (purple ghost), Solflare (yellow S), and Backpack (red pack)
16
+ marks as `<symbol>`s. The Connect-wallet picker resolves a mark by name
17
+ (`brandIcon(name)` → `<use href="#se-wallet-…">`) and falls back to a letter
18
+ tile only for an unknown wallet. Every consumer inherits crisp brand icons
19
+ with **zero per-app asset files** — before this, the picker referenced
20
+ app-served PNGs (`/wallet-phantom.png`, …), so a fresh app that had not copied
21
+ those into its own `public/` showed 404 letter tiles.
22
+
23
+ - **Entry-confirmed celebration — `studio/modals/blocks/_entry_confirmed`.** The
24
+ canonical "you're in" card, homed in the engine and composed via the
25
+ `_success_card` **yield slot**: `_success_card` owns the web3 spine (the `:lg`
26
+ check header, branded Solana tx link, drain CTA with auto-redirect, confetti),
27
+ and `_entry_confirmed` injects the leveling enrichment into its block. All
28
+ numbers are **app-supplied** via Alpine expressions (default `props.*`):
29
+ `txSignature`, `lobbyUrl`, `seedsEarned`, `seedsTotal`. Turf-specific copy is
30
+ parameterized, not baked in.
31
+
32
+ - **Seeds progress bar — `studio/modals/blocks/_seeds_bar` + `_digit_reel`.**
33
+ Ported from Turf Monster and generalized: the animated 5-segment fill with a
34
+ 4-phase level-up sequence (fill → level pop → drain → refill) and the
35
+ rolling-digit counter. The `--bar-progress` registered custom property,
36
+ `.seeds-bar-continuous`, `.seeds-reel-track`, and the `seedsShimmer` keyframe
37
+ ship in `engine-motion.css`. Motion is class-owned (timing via a
38
+ `--seeds-fill-dur` knob) so `prefers-reduced-motion` actually cancels the
39
+ bar-fill, shimmer, and reel.
40
+
41
+ - **Free Entry Earned modal — `studio/modals/blocks/_free_entry_earned`.** The
42
+ standalone level-up reward modal, parameterized (store, icon, title, copy).
43
+
44
+ - **Living style guide** gains an engine-owned wallet-icon picker, an
45
+ "Entry confirmed" (web3) specimen, and "Entry + seeds level-up" +
46
+ "Free Entry Earned" (leveling) specimens, so the gated variants demo correctly
47
+ (generic-success vs seeds vs free-entry).
48
+
49
+ ### Capability gating
50
+
51
+ - **`:web3`** gates the generic entry-confirmed success (branded tx link +
52
+ heading + CTA), alongside the existing wallet / on-chain / deposit modals.
53
+ - **`:leveling`** — independent of `:web3` — gates the seeds bar and the Free
54
+ Entry Earned reward. `_entry_confirmed` self-gates its block on
55
+ `Studio.feature?(:leveling)`, so a **web3-only app (leveling off) gets the
56
+ clean success card automatically** — no seeds, no free entry.
57
+
58
+ ### Unchanged
59
+
60
+ - Backward compatible: every addition is additive/optional (new locals default
61
+ to today's behavior). The shared `_success_card`, `_card_header`,
62
+ `_cta_redirect`, and the other blocks are untouched in default behavior, so
63
+ existing McRitchie Studio and Turf Monster consumers render identically. No
64
+ engine view-primitive gains any signing / key material — signing stays an
65
+ app-supplied callback seam.
66
+
5
67
  ## 0.19.0 — 2026-07-27
6
68
 
7
69
  ### Added
@@ -27,6 +27,7 @@
27
27
  .level-up-pop level-up pop + glow burst (ported, TM)
28
28
  .nav-level-pop nav level-up bounce (ported, TM)
29
29
  .badge-with-sheen one-shot diagonal sheen wrapper (ported, TM)
30
+ .seeds-bar-continuous 5-segment lockstep progress fill (ported, TM)
30
31
 
31
32
  ─────────────────────────────────────────────────────────────────────────────
32
33
  OPT-IN — this layer does NOT auto-bundle.
@@ -927,6 +928,63 @@
927
928
  }
928
929
  }
929
930
 
931
+ /* =============================================================================
932
+ SEEDS BAR — the continuous 5-segment progress fill behind the leveling
933
+ entry-confirmed celebration (studio/modals/blocks/_seeds_bar). Ported from
934
+ turf-monster. ONE registered custom property (--bar-progress) drives all five
935
+ segment clip-paths in lockstep off a SINGLE transition curve, instead of five
936
+ chained ones. .seeds-bar-continuous parks it at 0 and eases changes;
937
+ seedsShimmer is the light sweep that rides across the filled bar. The whole
938
+ family is capability-gated at the view layer by Studio.feature?(:leveling), so
939
+ an app with leveling off never renders a bar and these classes idle unused.
940
+ ========================================================================== */
941
+
942
+ /* Registered custom property — makes --bar-progress interpolatable so a single
943
+ transition animates all five segment widths together (one ease, not five). */
944
+ @property --bar-progress {
945
+ syntax: "<number>";
946
+ initial-value: 0;
947
+ inherits: true;
948
+ }
949
+
950
+ /* The motion PROPERTIES (transition / animation) live on these classes — NOT on
951
+ the view's inline style — precisely so the reduced-motion override below can
952
+ reach and cancel them. An inline `style="transition: …"` beats a class rule on
953
+ specificity, so a class-based @media override could never win against it; the
954
+ view keeps only the dynamic values inline (the Alpine-bound transform /
955
+ --bar-progress, and the fill duration knob --seeds-fill-dur). */
956
+ .seeds-bar-continuous {
957
+ --bar-progress: 0;
958
+ transition: --bar-progress var(--seeds-fill-dur, 1.2s) cubic-bezier(0.16, 1, 0.3, 1);
959
+ }
960
+
961
+ /* The rolling digit reel's slide — same fill duration as the bar (via the shared
962
+ --seeds-fill-dur knob) so the counter tracks the fill. Class-based so
963
+ reduced-motion can cancel it (the view binds only the transform inline). */
964
+ .seeds-reel-track {
965
+ transition: transform var(--seeds-fill-dur, 1.2s) cubic-bezier(0.16, 1, 0.3, 1);
966
+ }
967
+
968
+ /* Light sweep across a filled segment. The five segments stagger a shared band
969
+ (via per-segment offsets in the view) so it reads as one continuous wave. */
970
+ .seeds-shimmer {
971
+ animation: seedsShimmer 2.5s ease-in-out 0.4s 1 both;
972
+ }
973
+
974
+ @keyframes seedsShimmer {
975
+ 0% { transform: translateX(-100%); }
976
+ 100% { transform: translateX(200%); }
977
+ }
978
+
979
+ /* Reduced-motion: drop the interpolation + shimmer; the bar, reel, and shimmer
980
+ all snap to their resting value. Reachable because the motion lives on the
981
+ classes above, not on inline styles. */
982
+ @media (prefers-reduced-motion: reduce) {
983
+ .seeds-bar-continuous { transition: none; }
984
+ .seeds-reel-track { transition: none; }
985
+ .seeds-shimmer { animation: none; }
986
+ }
987
+
930
988
  /* =============================================================================
931
989
  Reduced-motion: honor the user preference. Animated primitives settle into a
932
990
  sensible static resting state; the static primitives (fade-edge, text-gradient,
@@ -0,0 +1,41 @@
1
+ <%#
2
+ Rolling-digit reel — a vertical stack of the values 0..max clipped to a single
3
+ cell, translated by -(value * cell_em)em so it slides as `value` changes. The
4
+ shared core behind rotating counters (a single 0–100 reel for the seeds bar,
5
+ per-unit em cells for countdowns). Ported from turf-monster; app-agnostic (only
6
+ Alpine expressions + locals — no app models or helpers).
7
+
8
+ Renders INSIDE a parent Alpine x-data scope: `value` and `transition` are Alpine
9
+ *expressions* evaluated there (e.g. "displaySeeds"; "'transform 1.9s ...'"). This
10
+ owns only the clip window + reel mechanics — colour, font, and any unit letter
11
+ live on the caller's wrapper.
12
+
13
+ Cells are em-sized, so the reel scales with the inherited font size; cell_em is
14
+ both the cell height (em) and the translate multiplier, so a value of N lands
15
+ cell N exactly under the window.
16
+
17
+ Locals:
18
+ value (String) — Alpine expr for the current value (drives translate)
19
+ max (Integer) — highest cell; renders cells 0..max
20
+ transition (String) — Alpine expr for the CSS transition (or "'none'")
21
+ width (String) — CSS width of the clip window, e.g. "2ch" / "3.2ch"
22
+ cell_em (Numeric?) — cell height in em + translate multiplier (default 1)
23
+ pad (Bool?) — zero-pad cell numbers to 2 digits (default false)
24
+ transition (String?) — Alpine expr for the slide transition when track_class
25
+ is NOT used (default "'none'"). Inline, so a caller can
26
+ snap on rollover; NOT reachable by reduced-motion CSS.
27
+ track_class(String?) — a CSS class that owns the slide transition. When set,
28
+ the reel binds ONLY the transform inline and the class
29
+ supplies the transition — so a reduced-motion @media
30
+ rule can cancel it (the seeds bar uses this). %>
31
+ <%
32
+ cell_em = local_assigns.fetch(:cell_em, 1)
33
+ pad = local_assigns.fetch(:pad, false)
34
+ track_class = local_assigns[:track_class]
35
+ transition = local_assigns.fetch(:transition, "'none'")
36
+ %>
37
+ <span style="display:inline-block;overflow:hidden;height:<%= cell_em %>em;line-height:<%= cell_em %>em;width:<%= width %>;text-align:right;">
38
+ <span class="block<%= " #{track_class}" if track_class %>" :style="{ transform: 'translateY(-' + (<%= value %> * <%= cell_em %>) + 'em)'<%= track_class ? "" : ", transition: #{transition}" %> }">
39
+ <% (0..max).each do |n| %><span class="block" style="height:<%= cell_em %>em;line-height:<%= cell_em %>em;"><%= pad ? format("%02d", n) : n %></span><% end %>
40
+ </span>
41
+ </span>
@@ -0,0 +1,86 @@
1
+ <%#
2
+ Entry-confirmed celebration — the canonical "you're in" card for an on-chain
3
+ (or entry-token-funded) entry. Homed in the engine and composed via the
4
+ _success_card YIELD SLOT (its documented purpose): _success_card owns the web3
5
+ spine — the :lg check header, the branded Solana tx link, the drain CTA with
6
+ auto-redirect, and the confetti — and this partial injects the LEVELING
7
+ enrichment (the seeds bar + level-up reward) into its block.
8
+
9
+ Capability gating (the two flags are independent):
10
+ * The generic success (heading + branded tx link + CTA) is a web3 celebration;
11
+ a caller renders this only in a :web3 context.
12
+ * The seeds bar + "Free Entry Earned" reward render ONLY under
13
+ Studio.feature?(:leveling). A web3-only app (leveling OFF) gets the clean
14
+ success card — no seeds, no free entry — automatically, because this partial
15
+ self-gates the block on the flag.
16
+
17
+ All numbers are APP-SUPPLIED via Alpine expressions (defaulting to props.*): the
18
+ engine renders the card, the app feeds txSignature / lobbyUrl / seedsEarned /
19
+ seedsTotal. The reward copy + CTA label + seeds-per-level are parameters, so no
20
+ Turf-specific string is baked in.
21
+
22
+ Single root: the outer <div> is one element so a <template x-if> clones it whole.
23
+
24
+ Locals (all optional; sensible props.* / static defaults):
25
+ modal_store — Alpine store name backing props/close. Default "modals".
26
+ title_key — Alpine expr for the headline. Default "props.title || 'Good Luck'".
27
+ subtitle — static sub-headline. Default "Entry Confirmed".
28
+ subtitle_key — Alpine expr for the sub-headline (priority over subtitle).
29
+ tx_signature_key — Alpine expr for the Solana tx hash. Default "props.txSignature".
30
+ cluster_param — explorer cluster query (e.g. "?cluster=devnet"). Default "".
31
+ lobby_url_key — Alpine expr for the CTA href + auto-redirect. Default "props.lobbyUrl".
32
+ cta_label — primary CTA text. Default "Continue".
33
+ seeds_earned_key — Alpine expr: seeds awarded this entry. Default "props.seedsEarned".
34
+ seeds_total_key — Alpine expr: new seeds total. Default "props.seedsTotal".
35
+ seeds_per_level — Integer seeds per level. Default 100.
36
+ free_entry_label — reward copy on a level-up. Default "Free Entry Earned 🎟️".
37
+ %>
38
+ <%
39
+ modal_store = local_assigns.fetch(:modal_store, "modals")
40
+ title_key = local_assigns.fetch(:title_key, "props.title || 'Good Luck'")
41
+ subtitle = local_assigns.fetch(:subtitle, "Entry Confirmed")
42
+ subtitle_key = local_assigns[:subtitle_key]
43
+ tx_signature_key = local_assigns.fetch(:tx_signature_key, "props.txSignature")
44
+ cluster_param = local_assigns.fetch(:cluster_param, "")
45
+ lobby_url_key = local_assigns.fetch(:lobby_url_key, "props.lobbyUrl")
46
+ cta_label = local_assigns.fetch(:cta_label, "Continue")
47
+ seeds_earned_key = local_assigns.fetch(:seeds_earned_key, "props.seedsEarned")
48
+ seeds_total_key = local_assigns.fetch(:seeds_total_key, "props.seedsTotal")
49
+ seeds_per_level = local_assigns.fetch(:seeds_per_level, 100)
50
+ free_entry_label = local_assigns.fetch(:free_entry_label, "Free Entry Earned 🎟️")
51
+
52
+ sc_locals = {
53
+ large_title: true,
54
+ icon_color: "primary",
55
+ title_key: title_key,
56
+ tx_signature_key: tx_signature_key,
57
+ tx_solana: true,
58
+ cta_label: cta_label,
59
+ cta_href_key: lobby_url_key,
60
+ cta_drain: true,
61
+ auto_redirect_url_key: lobby_url_key,
62
+ confetti: true
63
+ }
64
+ if subtitle_key
65
+ sc_locals[:message_key] = subtitle_key
66
+ else
67
+ sc_locals[:message] = subtitle
68
+ end
69
+ %>
70
+ <%# props getter + clusterParam live on the root scope so _success_card's nested
71
+ Alpine expressions (props.txSignature, props.lobbyUrl, clusterParam) resolve up
72
+ the scope chain. clusterParam feeds _success_card's branded tx-link URL. %>
73
+ <div x-data="{ get props() { var c = $store.<%= modal_store %>.current(); return (c && c.props) || {}; }, clusterParam: '<%= cluster_param %>' }">
74
+ <%= render "studio/modals/blocks/success_card", sc_locals do %>
75
+ <% if Studio.feature?(:leveling) %>
76
+ <%# Leveling enrichment — mounts only when this entry awarded seeds. %>
77
+ <template x-if="(<%= seeds_earned_key %>) > 0">
78
+ <%= render "studio/modals/blocks/seeds_bar",
79
+ seeds_earned_key: seeds_earned_key,
80
+ seeds_total_key: seeds_total_key,
81
+ seeds_per_level: seeds_per_level,
82
+ free_entry_label: free_entry_label %>
83
+ </template>
84
+ <% end %>
85
+ <% end %>
86
+ </div>
@@ -0,0 +1,46 @@
1
+ <%#
2
+ Free-entry-earned — the standalone level-up celebration modal. Pops when a user
3
+ crosses a seed level (the reward for leveling). Ported from turf-monster and
4
+ generalized: the store, icon, title, and reward copy are all parameters, so no
5
+ app-specific string is baked in.
6
+
7
+ Capability: a :leveling primitive — a caller opens it only under
8
+ Studio.feature?(:leveling).
9
+
10
+ Single root: the outer <div> is one element so a <template x-if> clones it whole.
11
+
12
+ Locals (all optional):
13
+ modal_store — Alpine store name backing props/close. Default "modals".
14
+ icon_emoji — celebration glyph. Default "🎉".
15
+ title_key — Alpine expr for the headline. Default shows "Level N" from
16
+ props.level, else "Free Entry Earned".
17
+ subtitle — static reward copy. Default a generic level-up line.
18
+ subtitle_key — Alpine expr for the reward copy (priority over subtitle).
19
+ close_label — dismiss link text. Default "Close".
20
+ %>
21
+ <%
22
+ modal_store = local_assigns.fetch(:modal_store, "modals")
23
+ icon_emoji = local_assigns.fetch(:icon_emoji, "🎉")
24
+ title_key = local_assigns.fetch(:title_key, "props.level ? ('Level ' + props.level) : 'Free Entry Earned'")
25
+ subtitle = local_assigns.fetch(:subtitle, "You leveled up and earned a reward. Keep earning more with each level.")
26
+ subtitle_key = local_assigns[:subtitle_key]
27
+ close_label = local_assigns.fetch(:close_label, "Close")
28
+
29
+ ch_locals = { size: :lg, icon_emoji: icon_emoji, title_key: title_key }
30
+ if subtitle_key
31
+ ch_locals[:subtitle_key] = subtitle_key
32
+ else
33
+ ch_locals[:subtitle] = subtitle
34
+ end
35
+ %>
36
+ <%# Confetti on mount — prefer a free-entry-specific burst if the host defined one,
37
+ else the shared success confetti; both are optional (guarded). %>
38
+ <div x-data="{ get props() { var c = $store.<%= modal_store %>.current(); return (c && c.props) || {}; } }"
39
+ x-init="setTimeout(function () { try { (window.fireFreeEntryConfetti || window.fireSuccessConfetti || function(){})(); } catch (e) {} }, 120)">
40
+ <%= render "studio/modals/blocks/card_header", ch_locals %>
41
+
42
+ <button type="button" @click="$store.<%= modal_store %>.close()"
43
+ class="block mx-auto mt-4 text-sm text-secondary hover:text-heading transition">
44
+ <%= close_label %>
45
+ </button>
46
+ </div>
@@ -0,0 +1,125 @@
1
+ <%#
2
+ Seeds progress bar — the leveling celebration's animated 5-segment fill + rolling
3
+ digit reel, with a 4-phase level-up sequence (fill to full → level pop → drain to
4
+ empty → refill toward the next level). Ported from turf-monster's entry-confirmed
5
+ inline bar and generalized: seed counts / level are APP-SUPPLIED (Alpine
6
+ expressions), and Turf-specific copy is a parameter. The engine renders the bar;
7
+ the app feeds the numbers.
8
+
9
+ Capability: this is a :leveling primitive. Callers render it only under
10
+ Studio.feature?(:leveling) (the engine's _entry_confirmed does exactly that), so
11
+ a web3-only app (leveling off) never mounts it.
12
+
13
+ Single root: the outer <div> is one element so a <template x-if> clones it whole.
14
+
15
+ Locals:
16
+ seeds_earned_key — Alpine expr: seeds awarded this event (drives whether a
17
+ level was crossed). Required.
18
+ seeds_total_key — Alpine expr: the user's new total seeds. Required.
19
+ seeds_per_level — Integer: seeds per level (default 100). The bar fills to
20
+ (total % per_level) / per_level; a crossed multiple triggers
21
+ the level-up flourish.
22
+ free_entry_label — String shown when a level is crossed (default
23
+ "Free Entry Earned 🎟️"). Parameterized so the reward copy
24
+ is not hardcoded to any one app.
25
+
26
+ The co-located x-data owns its own _timers array; destroy() clears every pending
27
+ setTimeout so a mid-celebration modal close leaks nothing.
28
+ %>
29
+ <%
30
+ seeds_earned_key = local_assigns.fetch(:seeds_earned_key)
31
+ seeds_total_key = local_assigns.fetch(:seeds_total_key)
32
+ seeds_per_level = local_assigns.fetch(:seeds_per_level, 100)
33
+ free_entry_label = local_assigns.fetch(:free_entry_label, "Free Entry Earned 🎟️")
34
+ %>
35
+ <div x-data="{
36
+ displaySeeds: 0,
37
+ displayLevel: 1,
38
+ levelingUp: false,
39
+ showFreeEntryBadge: false,
40
+ _timers: [],
41
+ init() {
42
+ var self = this;
43
+ var perLevel = <%= seeds_per_level %>;
44
+ var total = (<%= seeds_total_key %>) || 0;
45
+ var earned = (<%= seeds_earned_key %>) || 0;
46
+ var oldSeeds = Math.max(0, total - earned);
47
+ var oldLevel = Math.floor(oldSeeds / perLevel) + 1;
48
+ var newLevel = Math.floor(total / perLevel) + 1;
49
+ var oldToward = oldSeeds % perLevel;
50
+ var newToward = total % perLevel;
51
+ self.displaySeeds = oldToward;
52
+ self.displayLevel = oldLevel;
53
+ var schedule = function (fn, ms) { self._timers.push(setTimeout(fn, ms)); };
54
+ schedule(function () {
55
+ if (newLevel > oldLevel) {
56
+ self.levelingUp = true;
57
+ self.displaySeeds = perLevel;
58
+ schedule(function () {
59
+ self.displayLevel = newLevel;
60
+ self.showFreeEntryBadge = true;
61
+ }, 1900);
62
+ schedule(function () { self.displaySeeds = 0; }, 3400);
63
+ schedule(function () {
64
+ self.displaySeeds = newToward;
65
+ self.levelingUp = false;
66
+ }, 3600);
67
+ } else {
68
+ self.displaySeeds = newToward;
69
+ }
70
+ }, 2000);
71
+ },
72
+ destroy() {
73
+ this._timers.forEach(clearTimeout);
74
+ this._timers = [];
75
+ }
76
+ }"
77
+ class="mx-auto mb-1" style="max-width: 100%;">
78
+
79
+ <%# The bar (hidden once the level-up swaps to the reward line). Single root. %>
80
+ <template x-if="!showFreeEntryBadge">
81
+ <%# --seeds-fill-dur cascades to the bar (.seeds-bar-continuous) AND the reel
82
+ (.seeds-reel-track) so both share the exact 1.9s fill; the transition
83
+ PROPERTY itself lives on the classes so reduced-motion can cancel it. %>
84
+ <div class="relative" style="--seeds-fill-dur: 1.9s;">
85
+ <div class="seeds-bar-continuous flex gap-1.5"
86
+ style="height: 22px;"
87
+ :style="{ '--bar-progress': (displaySeeds / <%= seeds_per_level %> * 100) }">
88
+ <template x-for="i in 5" :key="i">
89
+ <div class="flex-1 relative overflow-hidden rounded-md"
90
+ style="background: rgb(var(--color-primary-500-rgb) / 0.06); border: 1.5px solid rgb(var(--color-primary-500-rgb) / 0.2);">
91
+ <div class="absolute inset-0 pointer-events-none"
92
+ :style="{
93
+ backgroundPosition: ((i - 1) * 25) + '% 0',
94
+ clipPath: 'inset(0 calc(100% - clamp(0%, calc((var(--bar-progress) - ' + ((i - 1) * 20) + ') * 5%), 100%)) 0 0)'
95
+ }"
96
+ style="background-image: linear-gradient(90deg, color-mix(in srgb, var(--color-cta) 30%, transparent), color-mix(in srgb, var(--color-cta) 60%, transparent)); background-size: 500% 100%; background-repeat: no-repeat;"></div>
97
+ <div class="seeds-shimmer absolute top-0 bottom-0 pointer-events-none"
98
+ :style="{ left: -(i - 1) * 100 + '%', width: '500%' }"
99
+ style="background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.55) 50%, transparent 100%);"></div>
100
+ </div>
101
+ </template>
102
+ </div>
103
+ <div class="absolute inset-y-0 right-2 flex items-center pointer-events-none">
104
+ <span class="inline-flex items-baseline gap-1 font-mono font-bold text-sm" style="color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.7);">
105
+ <%# track_class hands the reel a class-based transition (reduced-motion
106
+ reachable) instead of an inline one; duration via --seeds-fill-dur. %>
107
+ <%= render "studio/modals/blocks/digit_reel",
108
+ value: "displaySeeds", max: seeds_per_level, width: "3.2ch", cell_em: 1.25,
109
+ track_class: "seeds-reel-track" %>
110
+ <span class="text-[10px] font-normal opacity-90">seeds</span>
111
+ </span>
112
+ </div>
113
+ </div>
114
+ </template>
115
+
116
+ <%# Level-up reward — a plain text line sized to the same 22px height as the bar
117
+ it replaces, so the card does not shift when the bar swaps to this. Single
118
+ root. Copy is caller-parameterized (free_entry_label). %>
119
+ <template x-if="showFreeEntryBadge">
120
+ <div class="level-up-pop flex items-center justify-center w-full font-bold text-sm"
121
+ style="height: 22px; color: var(--color-cta);">
122
+ <span><%= free_entry_label %></span>
123
+ </div>
124
+ </template>
125
+ </div>
@@ -0,0 +1,51 @@
1
+ <%#
2
+ Wallet brand-icon sprite — inline SVG marks for the wallets the Connect-Wallet
3
+ picker knows about (Phantom, Solflare, Backpack), OWNED BY THE ENGINE so every
4
+ consumer inherits crisp brand icons with ZERO per-app asset files. Before this,
5
+ the picker referenced app-served PNGs (/wallet-phantom.png, ...), so a fresh app
6
+ that had not copied those into its own public/ showed 404 letter tiles (P/S/B).
7
+
8
+ Render this ONCE inside a picker's single root, then reference a mark by name:
9
+
10
+ <svg class="w-9 h-9"><use href="#se-wallet-phantom"></use></svg>
11
+
12
+ or, in an Alpine x-for over detected wallets, resolve the id from the name:
13
+
14
+ <use :href="'#se-wallet-' + brandIcon(w.name)"></use> (brandIcon() lowercases
15
+ the name and returns it only for a known brand, else null → letter fallback).
16
+
17
+ The sprite is visually hidden (zero-box, absolute) and aria-hidden; only the
18
+ <use> references paint. Each <symbol> carries its own viewBox + brand fills, so
19
+ a <use> scales into whatever box the outer <svg> sizes. Marks are geometric
20
+ interpretations of the official brand icons (Phantom purple ghost, Solflare
21
+ yellow S, Backpack red pack); the letter tile stays the fallback for any wallet
22
+ not in this set.
23
+ %>
24
+ <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"
25
+ style="position:absolute;width:0;height:0;overflow:hidden" data-wallet-brand-sprite>
26
+ <%# Phantom — light-purple tile, cream ghost with two purple eye holes (the
27
+ official ghost path; fill-rule evenodd cuts the eyes so the tile shows). %>
28
+ <symbol id="se-wallet-phantom" viewBox="0 0 128 128">
29
+ <rect width="128" height="128" rx="28" fill="#AB9FF2"/>
30
+ <path fill="#FFFDF8" fill-rule="evenodd" d="M110.584 64.9142H99.142C99.142 41.7651 80.173 23 56.7724 23C33.6612 23 14.8716 41.3057 14.4118 64.0583C13.936 87.577 36.2456 108 60.0197 108H63.0263C83.9754 108 112.069 91.6259 116.459 71.7107C117.27 68.033 114.351 64.9142 110.584 64.9142ZM39.7689 65.9482C39.7689 69.0453 37.2384 71.5758 34.1413 71.5758C31.0442 71.5758 28.5137 69.0453 28.5137 65.9482V56.8554C28.5137 53.7583 31.0442 51.2278 34.1413 51.2278C37.2384 51.2278 39.7689 53.7583 39.7689 56.8554V65.9482ZM59.3312 65.9482C59.3312 69.0453 56.8007 71.5758 53.7036 71.5758C50.6065 71.5758 48.076 69.0453 48.076 65.9482V56.8554C48.076 53.7583 50.6065 51.2278 53.7036 51.2278C56.8007 51.2278 59.3312 53.7583 59.3312 56.8554V65.9482Z"/>
31
+ </symbol>
32
+
33
+ <%# Solflare — bright-yellow tile, bold black S crossed by a diagonal slash. %>
34
+ <symbol id="se-wallet-solflare" viewBox="0 0 100 100">
35
+ <rect width="100" height="100" rx="24" fill="#FFEC1F"/>
36
+ <path d="M67 37c-3-7-11-11-20-11-10 0-17 5-17 13 0 7 5 11 16 14l8 2c9 2 13 5 13 11 0 8-8 13-18 13-9 0-16-3-19-10"
37
+ fill="none" stroke="#131313" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
38
+ <path d="M27 76 73 24" fill="none" stroke="#131313" stroke-width="8" stroke-linecap="round"/>
39
+ </symbol>
40
+
41
+ <%# Backpack — red tile, white pack: handle arch, rounded body with a red buckle
42
+ ring, and a rounded lower pocket band. %>
43
+ <symbol id="se-wallet-backpack" viewBox="0 0 100 100">
44
+ <rect width="100" height="100" rx="24" fill="#E33E3F"/>
45
+ <path d="M39 32v-3a11 11 0 0 1 22 0v3" fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round"/>
46
+ <rect x="23" y="31" width="54" height="31" rx="15" fill="#fff"/>
47
+ <circle cx="50" cy="46" r="8" fill="#E33E3F"/>
48
+ <circle cx="50" cy="46" r="3.5" fill="#fff"/>
49
+ <rect x="23" y="66" width="54" height="13" rx="6.5" fill="#fff"/>
50
+ </symbol>
51
+ </svg>
@@ -15,10 +15,12 @@
15
15
  and registers all specimen content itself.
16
16
 
17
17
  Groups: Auth (the sign-in step machine), Profile (crop / upload, opened
18
- through studio/cropper_assets), Web3 (wallet-connect / on-chain-tx / deposit,
18
+ through studio/cropper_assets), Web3 (wallet-connect with engine-owned inline
19
+ brand icons / on-chain-tx / deposit / the generic entry-confirmed celebration,
19
20
  gated by Studio.feature?(:web3) — disabled-but-present-yet-openable when off),
20
21
  System and status (the reusable card blocks), Templates (the copy-from
21
- archetypes), and Rewards (level-up, gated by :leveling).
22
+ archetypes), and Rewards (level-up + the entry-confirmed seeds bar + the
23
+ Free Entry Earned reward, gated by :leveling).
22
24
 
23
25
  The modal motion (modal-card-* / modal-backdrop-* keyframes + studio-modal-
24
26
  drain + body.modal-open) ships in engine-motion.css, so a consumer that
@@ -273,6 +275,15 @@
273
275
  <template x-if="$store.dsModals.current().id === 'wallet-deposit'">
274
276
  <div><%= render "style/modals/wallet_deposit" %></div>
275
277
  </template>
278
+ <%# entry-confirmed — the engine celebration composed via _success_card's
279
+ yield. One registration serves the generic (web3) + seeds (leveling)
280
+ specimens; the seeds bar self-gates on Studio.feature?(:leveling). %>
281
+ <template x-if="$store.dsModals.current().id === 'entry-confirmed'">
282
+ <div><%= render "studio/modals/blocks/entry_confirmed",
283
+ modal_store: "dsModals",
284
+ cta_label: "Contest Lobby ⚔️",
285
+ free_entry_label: "Free Entry Earned 🎟️" %></div>
286
+ </template>
276
287
 
277
288
  <%# --- System and status (reusable card blocks) --- %>
278
289
  <template x-if="$store.dsModals.current().id === 'ds-processing'">
@@ -332,6 +343,12 @@
332
343
  </div>
333
344
  <% end %></div>
334
345
  </template>
346
+ <%# free-entry-earned — the standalone level-up reward modal (:leveling). %>
347
+ <template x-if="$store.dsModals.current().id === 'free-entry-earned'">
348
+ <div><%= render "studio/modals/blocks/free_entry_earned",
349
+ modal_store: "dsModals",
350
+ subtitle: "You leveled up! A Free Entry lands in your account soon. Keep leveling for more." %></div>
351
+ </template>
335
352
 
336
353
  </div>
337
354
  </div>
@@ -494,6 +511,10 @@
494
511
  <code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
495
512
  <strong><%= web3_on ? "on" : "off" %></strong> here, so these render
496
513
  <%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
514
+ The Connect-wallet picker ships its <strong>Phantom / Solflare / Backpack
515
+ brand marks inline from the engine</strong>
516
+ (<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
517
+ so every app inherits them with no per-app icon files.
497
518
  </p>
498
519
  </div>
499
520
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -555,6 +576,25 @@
555
576
  <span class="block h-5 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
556
577
  </div>
557
578
  <% end %>
579
+
580
+ <%# Entry confirmed — the GENERIC web3 celebration (tx link + heading + drain
581
+ CTA). seedsEarned: 0 so no seeds bar even where leveling is on: this card
582
+ demos the clean success a web3-only app gets. The seeds variant lives in
583
+ Rewards below (gated by :leveling). %>
584
+ <%= render layout: "style/modal_specimen", locals: {
585
+ label: "Entry confirmed",
586
+ reference: %(the Web3 "Entry confirmed" celebration (studio/modals/blocks/_entry_confirmed, composed via _success_card's yield — branded tx link + heading + drain CTA; the seeds bar is gated off by :leveling and stays hidden here) — open with $store.dsModals.open('entry-confirmed', { txSignature: '…', lobbyUrl: null, seedsEarned: 0 })),
587
+ open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 0, seedsTotal: 40 })",
588
+ disabled: !web3_on, openable: true } do %>
589
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
590
+ <div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-primary) 15%, transparent)">
591
+ <svg class="w-4 h-4" style="color: var(--color-primary)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
592
+ </div>
593
+ <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
594
+ <span class="block h-1.5 w-14 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
595
+ <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
596
+ </div>
597
+ <% end %>
558
598
  </div>
559
599
  </section>
560
600
 
@@ -668,9 +708,16 @@
668
708
  <% end %>
669
709
  </div>
670
710
  <p class="text-muted text-sm">
671
- The level-up celebration, gated by <code class="font-mono text-2xs">Studio.feature?(:leveling)</code>.
672
- Leveling is <strong><%= leveling_on ? "on" : "off" %></strong> here, so this renders
711
+ The seeds bar + level-up celebrations, gated by
712
+ <code class="font-mono text-2xs">Studio.feature?(:leveling)</code> an
713
+ app-level setting <strong>independent of :web3</strong> (some apps have
714
+ Solana but no leveling). Leveling is <strong><%= leveling_on ? "on" : "off" %></strong>
715
+ here, so these render
673
716
  <%= leveling_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
717
+ The seed counts, level, and free entry are <strong>app-supplied props</strong>
718
+ (<code class="font-mono text-2xs">seedsEarned</code>,
719
+ <code class="font-mono text-2xs">seedsTotal</code>, …) — the engine renders
720
+ the bar, the app feeds the numbers.
674
721
  </p>
675
722
  </div>
676
723
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -685,6 +732,43 @@
685
732
  <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
686
733
  </div>
687
734
  <% end %>
735
+
736
+ <%# Entry + seeds — the SAME _entry_confirmed card, now with seedsEarned
737
+ crossing a level so the :leveling seeds bar mounts in the _success_card
738
+ yield and plays the 4-phase level-up (fill → pop → drain → refill), then
739
+ swaps to the Free Entry Earned reward line. %>
740
+ <%= render layout: "style/modal_specimen", locals: {
741
+ label: "Entry + seeds level-up",
742
+ reference: %(the Rewards "Entry + seeds" celebration (studio/modals/blocks/_entry_confirmed — the :leveling seeds bar + Free Entry Earned reward injected into _success_card's yield) — open with $store.dsModals.open('entry-confirmed', { txSignature: '…', lobbyUrl: null, seedsEarned: 30, seedsTotal: 100 })),
743
+ open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 30, seedsTotal: 100 })",
744
+ disabled: !leveling_on, openable: true } do %>
745
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
746
+ <div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-primary) 15%, transparent)">
747
+ <svg class="w-4 h-4" style="color: var(--color-primary)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
748
+ </div>
749
+ <div class="flex gap-1">
750
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-cta)"></span>
751
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-cta); opacity:.6"></span>
752
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-cta); opacity:.25"></span>
753
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-text); opacity:.12"></span>
754
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-text); opacity:.12"></span>
755
+ </div>
756
+ <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
757
+ </div>
758
+ <% end %>
759
+
760
+ <%# Free Entry Earned — the standalone level-up reward modal (:leveling). %>
761
+ <%= render layout: "style/modal_specimen", locals: {
762
+ label: "Free Entry Earned",
763
+ reference: %(the Rewards "Free Entry Earned" level-up modal (studio/modals/blocks/_free_entry_earned) — open with $store.dsModals.open('free-entry-earned', { level: 3 })),
764
+ open_expr: "$store.dsModals.open('free-entry-earned', { level: 3 })",
765
+ disabled: !leveling_on, openable: true } do %>
766
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
767
+ <div class="text-2xl leading-none">🎉</div>
768
+ <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
769
+ <span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
770
+ </div>
771
+ <% end %>
688
772
  </div>
689
773
  </section>
690
774
  </section>
@@ -9,15 +9,19 @@
9
9
  wallet list; pick() runs a brief connecting state then closes (no real
10
10
  connect). Opened via open('wallet-connect', {...}) or swap()ed to from auth.
11
11
 
12
+ Brand icons ship inline from the engine (studio/modals/blocks/_wallet_brand_sprite,
13
+ rendered once below): brandIcon(name) resolves a known wallet to its <symbol>
14
+ id, and an unknown wallet falls back to a letter tile. No per-app PNG files.
15
+
12
16
  Single root: the outer <div> is the host's required root.
13
17
  %>
14
18
  <div x-data="{
15
19
  get props() { var c = Alpine.store('dsModals').current(); return (c && c.props) || {}; },
16
20
  wallets: [],
17
21
  installs: [
18
- { name: 'Phantom', url: 'https://phantom.app/download', icon: '/wallet-phantom.png' },
19
- { name: 'Solflare', url: 'https://solflare.com/download', icon: '/wallet-solflare.png' },
20
- { name: 'Backpack', url: 'https://backpack.app/downloads', icon: '/wallet-backpack.png' }
22
+ { name: 'Phantom', url: 'https://phantom.app/download' },
23
+ { name: 'Solflare', url: 'https://solflare.com/download' },
24
+ { name: 'Backpack', url: 'https://backpack.app/downloads' }
21
25
  ],
22
26
  connecting: false,
23
27
  picking: '',
@@ -38,6 +42,10 @@
38
42
  refresh() {
39
43
  this.wallets = (window.walletProvider && window.walletProvider.available && window.walletProvider.available()) || [];
40
44
  },
45
+ brandIcon(name) {
46
+ var n = ('' + name).toLowerCase();
47
+ return ['phantom', 'solflare', 'backpack'].indexOf(n) >= 0 ? n : null;
48
+ },
41
49
  get isMobile() { return !!(window.walletProvider && window.walletProvider.isMobile && window.walletProvider.isMobile()); },
42
50
  hasWallet(name) {
43
51
  var n = ('' + name).toLowerCase();
@@ -71,6 +79,9 @@
71
79
  }"
72
80
  class="relative">
73
81
 
82
+ <%# Engine-owned brand-icon sprite — one copy; the <use> refs below paint it. %>
83
+ <%= render "studio/modals/blocks/wallet_brand_sprite" %>
84
+
74
85
  <div class="relative mb-4">
75
86
  <h3 class="text-heading font-bold text-lg text-center pt-1">Connect Wallet</h3>
76
87
  <button @click="$store.dsModals.close()"
@@ -89,7 +100,14 @@
89
100
  <template x-for="w in wallets" :key="w.name">
90
101
  <button type="button" @click="pick(w.name)" :disabled="connecting"
91
102
  class="w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition text-left disabled:opacity-60 disabled:cursor-wait">
92
- <span class="w-9 h-9 rounded-lg bg-inset flex items-center justify-center text-sm font-bold text-heading" x-text="w.name.slice(0,1)"></span>
103
+ <template x-if="brandIcon(w.name)">
104
+ <span class="w-9 h-9 rounded-lg overflow-hidden flex items-center justify-center shrink-0">
105
+ <svg class="w-9 h-9" aria-hidden="true"><use :href="'#se-wallet-' + brandIcon(w.name)"></use></svg>
106
+ </span>
107
+ </template>
108
+ <template x-if="!brandIcon(w.name)">
109
+ <span class="w-9 h-9 rounded-lg bg-inset flex items-center justify-center text-sm font-bold text-heading shrink-0" x-text="w.name.slice(0,1)"></span>
110
+ </template>
93
111
  <span class="font-semibold text-heading" x-text="w.name"></span>
94
112
  <span class="ml-auto flex items-center gap-2">
95
113
  <span x-show="picking === w.name" class="inline-flex items-center gap-1.5 text-xs text-secondary"><span class="spinner" aria-hidden="true"></span>Connecting…</span>
@@ -105,7 +123,14 @@
105
123
  <template x-for="i in missingInstalls" :key="i.name">
106
124
  <a :href="i.url" target="_blank" rel="noopener noreferrer"
107
125
  class="w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition no-underline">
108
- <span class="w-9 h-9 rounded-lg bg-inset flex items-center justify-center text-sm font-bold text-muted" x-text="i.name.slice(0,1)"></span>
126
+ <template x-if="brandIcon(i.name)">
127
+ <span class="w-9 h-9 rounded-lg overflow-hidden flex items-center justify-center shrink-0">
128
+ <svg class="w-9 h-9" aria-hidden="true"><use :href="'#se-wallet-' + brandIcon(i.name)"></use></svg>
129
+ </span>
130
+ </template>
131
+ <template x-if="!brandIcon(i.name)">
132
+ <span class="w-9 h-9 rounded-lg bg-inset flex items-center justify-center text-sm font-bold text-muted shrink-0" x-text="i.name.slice(0,1)"></span>
133
+ </template>
109
134
  <span class="font-semibold text-heading" x-text="i.name"></span>
110
135
  <span class="ml-auto flex items-center gap-2">
111
136
  <span class="text-xs text-muted uppercase tracking-wide">Install</span>
@@ -119,7 +144,9 @@
119
144
  <%# Mobile (no extension) — deep-link into the Phantom app. %>
120
145
  <button x-show="isMobile" type="button" @click="deepLink()"
121
146
  class="w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition text-left">
122
- <span class="w-9 h-9 rounded-lg bg-inset flex items-center justify-center text-sm font-bold text-heading">P</span>
147
+ <span class="w-9 h-9 rounded-lg overflow-hidden flex items-center justify-center shrink-0">
148
+ <svg class="w-9 h-9" aria-hidden="true"><use href="#se-wallet-phantom"></use></svg>
149
+ </span>
123
150
  <span class="font-semibold text-heading">Open in Phantom app</span>
124
151
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="ml-auto w-4 h-4 text-muted">
125
152
  <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.19.0"
2
+ VERSION = "0.20.0"
3
3
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-07-28 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rails
@@ -264,14 +265,19 @@ files:
264
265
  - app/views/studio/modals/auth/_resend_footer.html.erb
265
266
  - app/views/studio/modals/blocks/_card_header.html.erb
266
267
  - app/views/studio/modals/blocks/_cta_redirect.html.erb
268
+ - app/views/studio/modals/blocks/_digit_reel.html.erb
269
+ - app/views/studio/modals/blocks/_entry_confirmed.html.erb
267
270
  - app/views/studio/modals/blocks/_error_card.html.erb
271
+ - app/views/studio/modals/blocks/_free_entry_earned.html.erb
268
272
  - app/views/studio/modals/blocks/_onchain_success.html.erb
269
273
  - app/views/studio/modals/blocks/_processing_card.html.erb
270
274
  - app/views/studio/modals/blocks/_progress_countdown.html.erb
271
275
  - app/views/studio/modals/blocks/_progress_pill.html.erb
276
+ - app/views/studio/modals/blocks/_seeds_bar.html.erb
272
277
  - app/views/studio/modals/blocks/_shell.html.erb
273
278
  - app/views/studio/modals/blocks/_solana_tx_link.html.erb
274
279
  - app/views/studio/modals/blocks/_success_card.html.erb
280
+ - app/views/studio/modals/blocks/_wallet_brand_sprite.html.erb
275
281
  - app/views/studio/modals/shared/_age_attestation.html.erb
276
282
  - app/views/studio/modals/shared/_email_field.html.erb
277
283
  - app/views/studio/modals/templates/_action.html.erb
@@ -326,6 +332,7 @@ metadata:
326
332
  source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
327
333
  bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
328
334
  changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
335
+ post_install_message:
329
336
  rdoc_options: []
330
337
  require_paths:
331
338
  - lib
@@ -340,7 +347,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
340
347
  - !ruby/object:Gem::Version
341
348
  version: '0'
342
349
  requirements: []
343
- rubygems_version: 4.0.9
350
+ rubygems_version: 3.5.22
351
+ signing_key:
344
352
  specification_version: 4
345
353
  summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
346
354
  image caching