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 +4 -4
- data/CHANGELOG.md +62 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +58 -0
- data/app/views/studio/modals/blocks/_digit_reel.html.erb +41 -0
- data/app/views/studio/modals/blocks/_entry_confirmed.html.erb +86 -0
- data/app/views/studio/modals/blocks/_free_entry_earned.html.erb +46 -0
- data/app/views/studio/modals/blocks/_seeds_bar.html.erb +125 -0
- data/app/views/studio/modals/blocks/_wallet_brand_sprite.html.erb +51 -0
- data/app/views/style/_modals.html.erb +88 -4
- data/app/views/style/modals/_wallet_connect.html.erb +33 -6
- data/lib/studio/version.rb +1 -1
- metadata +11 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3b84a5d11a22ad89cdbf077ba91c6d35374ba8a956c2fdc1207eb7033ec15c5
|
|
4
|
+
data.tar.gz: 510f09d35fb5dd3562c9435c1df1f5cf8001b9a34dd12678eb32822f91ded766
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
672
|
-
|
|
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'
|
|
19
|
-
{ name: 'Solflare', url: 'https://solflare.com/download'
|
|
20
|
-
{ name: 'Backpack', url: 'https://backpack.app/downloads'
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
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" />
|
data/lib/studio/version.rb
CHANGED
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.
|
|
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:
|
|
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:
|
|
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
|