studio-engine 0.18.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 +96 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +58 -0
- data/app/controllers/concerns/studio/magic_link_issuing.rb +39 -0
- data/app/controllers/magic_links_controller.rb +5 -14
- data/app/controllers/studio/local_emails_controller.rb +1 -1
- data/app/controllers/studio/local_reviews_controller.rb +50 -0
- data/app/mailers/user_mailer.rb +6 -9
- 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
- data/lib/studio.rb +16 -0
- metadata +9 -2
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,102 @@
|
|
|
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
|
+
|
|
67
|
+
## 0.19.0 — 2026-07-27
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- **`GET /_studio/local_review` — the local half of the board's WAITING APPROVAL
|
|
72
|
+
button.** A magic link signs the recipient into the app that MINTED it: the
|
|
73
|
+
token lives in that app's store, and `return_to` is sanitized to a same-origin
|
|
74
|
+
PATH. So a link minted by the production task board could only ever land on
|
|
75
|
+
production — the operator clicked "review this locally" and arrived, signed in,
|
|
76
|
+
on `mcritchie.studio`. The board now redirects here instead, to the local
|
|
77
|
+
server named by the task's `local_url`, which mints in its OWN database and
|
|
78
|
+
lands the operator signed-in on the page under review
|
|
79
|
+
(`?email=<who>&return_to=<path>`).
|
|
80
|
+
|
|
81
|
+
Like the local email inbox it sits beside, this is a developer-desk tool that
|
|
82
|
+
hands out sign-in material without authenticating anyone, so it is bound to the
|
|
83
|
+
same floor: the route is drawn outside production only, and every request is
|
|
84
|
+
re-checked against `Studio.local_tool_enabled?` — production or non-loopback
|
|
85
|
+
gets a bare 404.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **`Studio.local_tool_enabled?(request_local:)`** — one spelling of the
|
|
90
|
+
developer-desk floor (not production, loopback only), now shared by
|
|
91
|
+
`Studio::LocalEmailsController` and `Studio::LocalReviewsController` so a tool
|
|
92
|
+
added later cannot quietly ship a weaker gate.
|
|
93
|
+
|
|
94
|
+
- **`Studio::MagicLinkIssuing`** — the mint (`issue_magic_link`) and the URL that
|
|
95
|
+
consumes it (`magic_link_url_for`) are two halves of one decision
|
|
96
|
+
(`Studio.magic_link_store`), extracted from `MagicLinksController` and
|
|
97
|
+
`UserMailer` into a shared concern. Handing out the wrong URL for the store
|
|
98
|
+
yields "invalid or expired" on a link that was valid; they can no longer drift
|
|
99
|
+
apart. No behavior change for either existing caller.
|
|
100
|
+
|
|
5
101
|
## 0.18.0 — 2026-07-27
|
|
6
102
|
|
|
7
103
|
### 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,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# Minting a magic link, and building the URL that consumes it — the two halves
|
|
5
|
+
# of one decision (Studio.magic_link_store), kept in one place so they can
|
|
6
|
+
# never drift apart. A token minted in the :database store is only consumable
|
|
7
|
+
# at /l/<token>; a :signed token only at /magic_link/<token>. Handing out the
|
|
8
|
+
# wrong URL for the store yields an "invalid or expired link" on a link that
|
|
9
|
+
# was valid — the failure mode this concern exists to prevent.
|
|
10
|
+
#
|
|
11
|
+
# Included by every issuer: MagicLinksController (the request-a-link flow),
|
|
12
|
+
# UserMailer (the emailed link), and Studio::LocalReviewsController (the
|
|
13
|
+
# dev-only local-review link). An app that overrides MagicLinksController
|
|
14
|
+
# wholesale brings its own issuing and is unaffected.
|
|
15
|
+
module MagicLinkIssuing
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Mint a token in the configured store. Default :signed keeps the legacy
|
|
21
|
+
# stateless MessageVerifier link; :database mints a Studio::Link row (the
|
|
22
|
+
# short, unified scheme). `return_to` is sanitized by both stores.
|
|
23
|
+
def issue_magic_link(email, return_to)
|
|
24
|
+
if Studio.magic_link_store == :database
|
|
25
|
+
Studio::Link.create_magic_link(email: email, return_to: return_to).token
|
|
26
|
+
else
|
|
27
|
+
MagicLink.generate(email: email, return_to: return_to)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The URL that CONSUMES the token this app mints: the short /l/<token> for
|
|
32
|
+
# the :database scheme, the legacy /magic_link/<token> for :signed (and for
|
|
33
|
+
# a :database app that keeps its own /magic_link route, e.g. turf-monster,
|
|
34
|
+
# whose /l is already its landing-page namespace).
|
|
35
|
+
def magic_link_url_for(token)
|
|
36
|
+
Studio.magic_link_via_l_route? ? link_url(token: token) : magic_link_url(token: token)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# sign_in_existing / sign_up_new building blocks.
|
|
16
16
|
class MagicLinksController < ApplicationController
|
|
17
17
|
include Studio::LinkConsumption
|
|
18
|
+
include Studio::MagicLinkIssuing
|
|
18
19
|
|
|
19
20
|
skip_before_action :require_authentication
|
|
20
21
|
layout false, only: :confirm
|
|
@@ -57,18 +58,8 @@ class MagicLinksController < ApplicationController
|
|
|
57
58
|
redirect_to login_path, alert: "That sign-in link is invalid or has expired. Request a fresh one below."
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
# Studio::Link row (URL: /l/<token>) — the short, unified scheme. The mailer
|
|
65
|
-
# builds the matching URL (see UserMailer#magic_link). sign_in_existing /
|
|
66
|
-
# sign_up_new / safe_path come from Studio::LinkConsumption.
|
|
67
|
-
def issue_magic_link(email, return_to)
|
|
68
|
-
if Studio.magic_link_store == :database
|
|
69
|
-
Studio::Link.create_magic_link(email: email, return_to: return_to).token
|
|
70
|
-
else
|
|
71
|
-
MagicLink.generate(email: email, return_to: return_to)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
61
|
+
# issue_magic_link (mint in the configured store) comes from
|
|
62
|
+
# Studio::MagicLinkIssuing, shared with UserMailer — which builds the URL that
|
|
63
|
+
# consumes it — so the mint and its landing URL cannot drift apart.
|
|
64
|
+
# sign_in_existing / sign_up_new / safe_path come from Studio::LinkConsumption.
|
|
74
65
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# GET /_studio/local_review?email=<who>&return_to=<path> — the LOCAL half of
|
|
5
|
+
# the task board's WAITING APPROVAL button.
|
|
6
|
+
#
|
|
7
|
+
# Why it must live here, on the local app. A magic link signs the recipient
|
|
8
|
+
# into the app that MINTED it: the token is a row (or a signed blob) in that
|
|
9
|
+
# app's own store, and Studio::LinkToken.sanitize_path keeps `return_to` a
|
|
10
|
+
# same-origin PATH. So a link minted by the production board can only ever
|
|
11
|
+
# land on production — no matter what host the path belongs to. The board
|
|
12
|
+
# therefore stops minting and REDIRECTS here, to the local server named by the
|
|
13
|
+
# task's local_url, which mints in its own database and lands the operator
|
|
14
|
+
# signed-in on the page under review.
|
|
15
|
+
#
|
|
16
|
+
# It mints for whatever email it is handed, unauthenticated — so it is bound
|
|
17
|
+
# to the same floor as the local email inbox (Studio::LocalEmailsController):
|
|
18
|
+
# never in production, and only for a loopback request. That is the same
|
|
19
|
+
# exposure the inbox already carries, which hands every captured sign-in link
|
|
20
|
+
# to any local reader. It is a development desk convenience; it is not a
|
|
21
|
+
# sign-in path.
|
|
22
|
+
class LocalReviewsController < ApplicationController
|
|
23
|
+
include Studio::MagicLinkIssuing
|
|
24
|
+
|
|
25
|
+
skip_before_action :require_authentication, raise: false
|
|
26
|
+
|
|
27
|
+
before_action :require_local_development!
|
|
28
|
+
|
|
29
|
+
def show
|
|
30
|
+
email = Studio::LinkToken.normalize_email(params[:email])
|
|
31
|
+
return redirect_to(login_path, alert: MISSING_EMAIL) unless email.match?(URI::MailTo::EMAIL_REGEXP)
|
|
32
|
+
|
|
33
|
+
# return_to is passed through raw: BOTH stores sanitize it to a same-origin
|
|
34
|
+
# path on the way in (Studio::Link.create_magic_link and MagicLink.generate
|
|
35
|
+
# each call Studio::LinkToken.sanitize_path). Re-sanitizing here would be a
|
|
36
|
+
# second spelling of a rule that already has one owner — and a mutation run
|
|
37
|
+
# confirmed it guards nothing the store does not already guard.
|
|
38
|
+
token = issue_magic_link(email, params[:return_to])
|
|
39
|
+
redirect_to magic_link_url_for(token), allow_other_host: false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
MISSING_EMAIL = "Add ?email=<your address> to mint a local review link."
|
|
45
|
+
|
|
46
|
+
def require_local_development!
|
|
47
|
+
head :not_found unless Studio.local_tool_enabled?(request_local: request.local?)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/app/mailers/user_mailer.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
class UserMailer < ApplicationMailer
|
|
2
|
+
# magic_link_url_for — the URL that consumes the token, matched to
|
|
3
|
+
# Studio.magic_link_store. Shared with the issuers that MINT the token
|
|
4
|
+
# (MagicLinksController, Studio::LocalReviewsController) so the two halves
|
|
5
|
+
# cannot drift apart.
|
|
6
|
+
include Studio::MagicLinkIssuing
|
|
7
|
+
|
|
2
8
|
# Branded shell (banner + card) for engine-sent UserMailer emails. An app with
|
|
3
9
|
# its own UserMailer + branded_mailer layout (e.g. turf-monster) overrides both.
|
|
4
10
|
layout "branded_mailer"
|
|
@@ -18,13 +24,4 @@ class UserMailer < ApplicationMailer
|
|
|
18
24
|
@banner_alt = "Your #{@app_name} sign-in link"
|
|
19
25
|
mail(to: email, subject: "Your #{@app_name} sign-in link")
|
|
20
26
|
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
# Match the emailed URL to Studio.magic_link_store: the short /l/<token> for
|
|
25
|
-
# the :database scheme, the legacy /magic_link/<token> for :signed. The
|
|
26
|
-
# request side (MagicLinksController#issue_magic_link) mints the matching token.
|
|
27
|
-
def magic_link_url_for(token)
|
|
28
|
-
Studio.magic_link_via_l_route? ? link_url(token: token) : magic_link_url(token: token)
|
|
29
|
-
end
|
|
30
27
|
end
|
|
@@ -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
data/lib/studio.rb
CHANGED
|
@@ -205,6 +205,18 @@ module Studio
|
|
|
205
205
|
magic_link_store == :database && draw_link_routes
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
+
# The floor every developer-desk tool sits on: the local email inbox
|
|
209
|
+
# (Studio::LocalEmailsController) and the local-review mint
|
|
210
|
+
# (Studio::LocalReviewsController). Both hand out sign-in material without
|
|
211
|
+
# authenticating anyone, so both are OFF in production and OFF for any request
|
|
212
|
+
# that did not come from the loopback interface. One spelling, so a tool added
|
|
213
|
+
# later cannot quietly ship a weaker gate. Pass request.local? in.
|
|
214
|
+
def self.local_tool_enabled?(request_local:)
|
|
215
|
+
return false if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
|
|
216
|
+
|
|
217
|
+
!!request_local
|
|
218
|
+
end
|
|
219
|
+
|
|
208
220
|
def self.local_email_capture?
|
|
209
221
|
return false if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
|
|
210
222
|
return !!local_email_capture unless local_email_capture.nil?
|
|
@@ -299,8 +311,12 @@ module Studio
|
|
|
299
311
|
get "auth/:provider/callback", to: "omniauth_callbacks#create"
|
|
300
312
|
get "auth/failure", to: "omniauth_callbacks#failure"
|
|
301
313
|
|
|
314
|
+
# Developer-desk tools. Drawn outside production, and each controller
|
|
315
|
+
# re-checks Studio.local_tool_enabled? per request (loopback only) — the
|
|
316
|
+
# route being absent is the outer gate, not the only one.
|
|
302
317
|
unless defined?(Rails) && Rails.env.production?
|
|
303
318
|
get "_studio/local_emails", to: "studio/local_emails#index", as: :studio_local_emails
|
|
319
|
+
get "_studio/local_review", to: "studio/local_reviews#show", as: :studio_local_review
|
|
304
320
|
end
|
|
305
321
|
|
|
306
322
|
# Passwordless email (magic link). Helpers: magic_link_request_path (POST
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -179,6 +179,7 @@ files:
|
|
|
179
179
|
- app/controllers/concerns/studio/error_handling.rb
|
|
180
180
|
- app/controllers/concerns/studio/impersonation.rb
|
|
181
181
|
- app/controllers/concerns/studio/link_consumption.rb
|
|
182
|
+
- app/controllers/concerns/studio/magic_link_issuing.rb
|
|
182
183
|
- app/controllers/error_logs_controller.rb
|
|
183
184
|
- app/controllers/magic_links_controller.rb
|
|
184
185
|
- app/controllers/navbar_controller.rb
|
|
@@ -190,6 +191,7 @@ files:
|
|
|
190
191
|
- app/controllers/studio/email_images_controller.rb
|
|
191
192
|
- app/controllers/studio/links_controller.rb
|
|
192
193
|
- app/controllers/studio/local_emails_controller.rb
|
|
194
|
+
- app/controllers/studio/local_reviews_controller.rb
|
|
193
195
|
- app/controllers/studio/models_controller.rb
|
|
194
196
|
- app/controllers/style_controller.rb
|
|
195
197
|
- app/controllers/theme_settings_controller.rb
|
|
@@ -263,14 +265,19 @@ files:
|
|
|
263
265
|
- app/views/studio/modals/auth/_resend_footer.html.erb
|
|
264
266
|
- app/views/studio/modals/blocks/_card_header.html.erb
|
|
265
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
|
|
266
270
|
- app/views/studio/modals/blocks/_error_card.html.erb
|
|
271
|
+
- app/views/studio/modals/blocks/_free_entry_earned.html.erb
|
|
267
272
|
- app/views/studio/modals/blocks/_onchain_success.html.erb
|
|
268
273
|
- app/views/studio/modals/blocks/_processing_card.html.erb
|
|
269
274
|
- app/views/studio/modals/blocks/_progress_countdown.html.erb
|
|
270
275
|
- app/views/studio/modals/blocks/_progress_pill.html.erb
|
|
276
|
+
- app/views/studio/modals/blocks/_seeds_bar.html.erb
|
|
271
277
|
- app/views/studio/modals/blocks/_shell.html.erb
|
|
272
278
|
- app/views/studio/modals/blocks/_solana_tx_link.html.erb
|
|
273
279
|
- app/views/studio/modals/blocks/_success_card.html.erb
|
|
280
|
+
- app/views/studio/modals/blocks/_wallet_brand_sprite.html.erb
|
|
274
281
|
- app/views/studio/modals/shared/_age_attestation.html.erb
|
|
275
282
|
- app/views/studio/modals/shared/_email_field.html.erb
|
|
276
283
|
- app/views/studio/modals/templates/_action.html.erb
|