studio-engine 0.60.0 → 0.60.1
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 +45 -0
- data/app/views/studio/{_age_verify_assets.html.erb → _birthday_assets.html.erb} +79 -13
- data/app/views/studio/_board_assets.html.erb +1 -1
- data/app/views/studio/_leveling_activity_assets.html.erb +2 -2
- data/app/views/studio/fields/_date_of_birth.html.erb +4 -3
- data/app/views/studio/modals/blocks/_age_gate.html.erb +225 -0
- data/app/views/studio/modals/blocks/_birthday.html.erb +134 -0
- data/app/views/studio/profiles/_birthday_fields.html.erb +1 -1
- data/app/views/style/_modals.html.erb +380 -114
- data/app/views/style/modals/_age_gate.html.erb +21 -0
- data/app/views/style/modals/_birthday.html.erb +59 -0
- data/app/views/style/modals/_cosign_rejected.html.erb +36 -0
- data/app/views/style/modals/_quest_success.html.erb +55 -0
- data/app/views/style/modals/_unsubscribe_confirm.html.erb +52 -0
- data/app/views/style/modals/_unsubscribe_goodbye.html.erb +30 -0
- data/app/views/style/modals/_wallet_setup.html.erb +112 -0
- data/lib/studio/version.rb +1 -1
- metadata +12 -5
- data/app/views/studio/modals/blocks/_age_verify.html.erb +0 -82
- data/app/views/style/modals/_age_verify.html.erb +0 -17
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen for the engine AGE GATE refusal card
|
|
3
|
+
(studio/modals/blocks/_age_gate). New 2026-08-24.
|
|
4
|
+
|
|
5
|
+
It is reached two ways, and both are worth seeing:
|
|
6
|
+
- directly, from its own card here
|
|
7
|
+
- as the HANDOFF from the birthday card's refused branch, which is how a
|
|
8
|
+
real person meets it
|
|
9
|
+
|
|
10
|
+
Demo values only. watch_url points at an anchor rather than a route because the
|
|
11
|
+
engine has no idea what a contest is — the target is app-supplied, and a host
|
|
12
|
+
passes its own contest path. The back link swaps to 'birthday', the id this
|
|
13
|
+
page registers for the DOB card, so the round trip works on this page exactly
|
|
14
|
+
as it does in an app.
|
|
15
|
+
%>
|
|
16
|
+
<%= render "studio/modals/blocks/age_gate",
|
|
17
|
+
min_age: 21,
|
|
18
|
+
state: "CA",
|
|
19
|
+
watch_url: "#demo-watch-contest",
|
|
20
|
+
birthday_modal_id: "birthday",
|
|
21
|
+
modal_store: "dsModals" %>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen for the engine BIRTHDAY DOB card
|
|
3
|
+
(studio/modals/blocks/_birthday, renamed from _age_verify on 2026-08-24).
|
|
4
|
+
Opened on the page-scoped dsModals host.
|
|
5
|
+
|
|
6
|
+
TWO MODES, ONE CARD, chosen by props.validates — the same choice a host makes
|
|
7
|
+
by passing min_age or leaving it out:
|
|
8
|
+
|
|
9
|
+
validates: true the app HAS a bar. min_age + state ride in, the card says
|
|
10
|
+
what the rule is, and submitting takes the refused branch
|
|
11
|
+
so the guide can walk the handoff to the age-gate card.
|
|
12
|
+
validates: false the app has NO bar. No min_age at all: the card is a plain
|
|
13
|
+
birthday asker, which is what a hub app wants when it
|
|
14
|
+
collects the date for a greeting rather than for entry.
|
|
15
|
+
|
|
16
|
+
The point of the seam: every legal value here is a DEMO the app feeds. The
|
|
17
|
+
engine primitive hardcodes no age and no state table, which is why the same
|
|
18
|
+
partial serves an app whose minimum is jurisdiction-dependent and an app with
|
|
19
|
+
no minimum at all.
|
|
20
|
+
%>
|
|
21
|
+
<%# ABSENT `validates` MEANS VALIDATING, and the default has exactly one home
|
|
22
|
+
here so the two branches cannot disagree about it.
|
|
23
|
+
|
|
24
|
+
Why absent is not "no bar": the only card that can REACH the age gate is the
|
|
25
|
+
validating one — the no-bar card carries no min_age, so the factory's _reject
|
|
26
|
+
never fires. So every arrival with no props is a return trip from the gate,
|
|
27
|
+
and `back()` swaps with EMPTY props by design (blocks/_age_gate.html.erb).
|
|
28
|
+
Reading undefined as falsy sent that return trip to the NO-BAR card, where
|
|
29
|
+
resubmitting the SAME under-age date was accepted — the guide demonstrating an
|
|
30
|
+
age gate being bypassed. Caught in review, 2026-08-25.
|
|
31
|
+
|
|
32
|
+
The engine primitive is right and is NOT changed: in a real host the mode
|
|
33
|
+
comes from ERB locals, not from a prop, so this is a two-mode SPECIMEN's
|
|
34
|
+
problem alone. %>
|
|
35
|
+
<div x-data="{
|
|
36
|
+
get props() { var c = $store.dsModals.current(); return (c && c.props) || {} },
|
|
37
|
+
get validates() { return this.props.validates !== false; }
|
|
38
|
+
}">
|
|
39
|
+
<template x-if="validates">
|
|
40
|
+
<div><%= render "studio/modals/blocks/birthday",
|
|
41
|
+
min_age: 21,
|
|
42
|
+
state: "CA",
|
|
43
|
+
submit_url: "#demo-birthday",
|
|
44
|
+
modal_store: "dsModals",
|
|
45
|
+
demo: true,
|
|
46
|
+
demo_underage: true,
|
|
47
|
+
fine_print: "Demo policy the app supplies: 21+ in CA. The engine hardcodes no age and no state table. Submitting from here takes the REFUSED branch, so you land on the age-gate card." %></div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<%# No min_age at all — not min_age: 0, which would be the app claiming a bar
|
|
51
|
+
of zero. Omitting it is how a host says "I do not gate on this". %>
|
|
52
|
+
<template x-if="!validates">
|
|
53
|
+
<div><%= render "studio/modals/blocks/birthday",
|
|
54
|
+
submit_url: "#demo-birthday",
|
|
55
|
+
modal_store: "dsModals",
|
|
56
|
+
demo: true,
|
|
57
|
+
fine_print: "No age bar on this app. The card asks for the date and nothing gates on it — the eligibility wording and the refusal handoff are both absent." %></div>
|
|
58
|
+
</template>
|
|
59
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the "we did not co-sign that" refusal.
|
|
3
|
+
|
|
4
|
+
APP-SPECIFIC FLOW, engine chrome only. What this card names is turf-monster's:
|
|
5
|
+
POST confirm_onchain_entry answered 422 with code `tx_rejected`, meaning the
|
|
6
|
+
server refused to co-sign because the submitted transaction did not match the
|
|
7
|
+
entry it had prepared. The engine owns no entry preparation, no co-signing and
|
|
8
|
+
no verdict — it lends `blocks/_card_header` and nothing else, exactly as
|
|
9
|
+
style/modals/_entry_tokens borrows chrome for a flow the engine does not own.
|
|
10
|
+
|
|
11
|
+
WHY THE COPY IS VAGUE, and why a reviewer should not "improve" it: good actors
|
|
12
|
+
never see this card. It fires on a tampered or mismatched transaction, so the
|
|
13
|
+
copy is deliberately reassuring and deliberately silent about WHICH
|
|
14
|
+
server-side check failed — naming the check would help an attacker probe the
|
|
15
|
+
validation. The reassurance ("nothing was submitted and your funds are safe")
|
|
16
|
+
is the part that matters to the one honest user who ever hits it.
|
|
17
|
+
|
|
18
|
+
Single root: the outer <div> is the host's required root.
|
|
19
|
+
%>
|
|
20
|
+
<div x-data="{}">
|
|
21
|
+
<%= render "studio/modals/blocks/card_header",
|
|
22
|
+
icon_emoji: "🛡️",
|
|
23
|
+
title: "Transaction not signed" do %>
|
|
24
|
+
<p class="text-sm text-body">
|
|
25
|
+
For your security we didn’t co-sign this transaction — it
|
|
26
|
+
didn’t match the entry we prepared, so nothing was submitted and your
|
|
27
|
+
funds are safe. Please close this and try entering again. If it keeps
|
|
28
|
+
happening, <span class="text-primary">contact support</span>.
|
|
29
|
+
</p>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<%# One button, because there is exactly one thing to do. The host also closes
|
|
33
|
+
on escape and click-outside; this card is dismissible by design. %>
|
|
34
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
35
|
+
class="btn btn-primary btn-lg w-full">Got it</button>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the off-page quest celebration.
|
|
3
|
+
|
|
4
|
+
WHY IT EXISTS ALONGSIDE "Great Username", which looks almost identical: they
|
|
5
|
+
are the same beat reached two different ways, and the difference is where the
|
|
6
|
+
user is standing. The leveling-activity celebration fires INSIDE the modal that
|
|
7
|
+
did the work. This card is what a host shows when the quest completed somewhere
|
|
8
|
+
with no quest card to advance in place — turf's /account username change — so
|
|
9
|
+
the celebration has to arrive on its own.
|
|
10
|
+
|
|
11
|
+
APP-SPECIFIC FLOW, engine chrome only. The engine lends `blocks/_card_header`
|
|
12
|
+
and `blocks/_seeds_bar` (a :leveling primitive — the bar is the engine's, the
|
|
13
|
+
NUMBERS are app-supplied Alpine expressions). What stays the host's: which
|
|
14
|
+
quests exist, what they pay, and where "Next Quest" goes.
|
|
15
|
+
|
|
16
|
+
THE FORK AT THE BOTTOM is the interesting part and the reason this is one card
|
|
17
|
+
with a toggle rather than two cards. A viewer who has NOT joined the newsletter
|
|
18
|
+
is offered it as the next quest — the celebration doubles as the funnel. One who
|
|
19
|
+
already has is sent to the contest, where the remaining quests live. In the real
|
|
20
|
+
card that gate is SERVER-rendered from current_user.subscribed_to_newsletter?,
|
|
21
|
+
precisely so the CTA never flashes the wrong option at someone already in.
|
|
22
|
+
|
|
23
|
+
Single root: the outer <div> is the host's required root.
|
|
24
|
+
%>
|
|
25
|
+
<div x-data="{ get props() { var c = $store.dsModals.current(); return (c && c.props) || {} } }">
|
|
26
|
+
<%= render "studio/modals/blocks/card_header",
|
|
27
|
+
size: :lg,
|
|
28
|
+
icon_emoji: "🌱",
|
|
29
|
+
title: "Great Username",
|
|
30
|
+
subtitle: "Nice — you earned 25 seeds by completing a quest. Complete more quests to earn a Free Entry Token." %>
|
|
31
|
+
|
|
32
|
+
<%# The engine's bar, fed demo numbers. A host passes its own live totals; the
|
|
33
|
+
bar decides on its own whether a level was crossed and runs the flourish. %>
|
|
34
|
+
<div class="mb-5">
|
|
35
|
+
<%= render "studio/modals/blocks/seeds_bar",
|
|
36
|
+
seeds_earned_key: "25",
|
|
37
|
+
seeds_total_key: "props.seedsTotal || 25",
|
|
38
|
+
free_entry_label: "Free Entry Token 🎟️" %>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<template x-if="props.subscribed">
|
|
42
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
43
|
+
class="btn btn-primary btn-lg w-full">Next Quest</button>
|
|
44
|
+
</template>
|
|
45
|
+
<template x-if="!props.subscribed">
|
|
46
|
+
<%# Not subscribed: the celebration hands off to the newsletter quest. %>
|
|
47
|
+
<button type="button" @click="$store.dsModals.swap('join-newsletter', { demo: true, leveling: true })"
|
|
48
|
+
class="btn btn-primary btn-lg w-full">Next Quest</button>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
52
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
53
|
+
Close
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the unsubscribe confirmation, and the handoff to
|
|
3
|
+
its goodbye beat.
|
|
4
|
+
|
|
5
|
+
APP-SPECIFIC FLOW, engine chrome only. The real card POSTs the host's
|
|
6
|
+
newsletter_unsubscribe_path and swaps to the goodbye card on success; the engine
|
|
7
|
+
owns no newsletter and no endpoint. Here the POST resolves locally so the guide
|
|
8
|
+
can walk the handoff with no backend — the SWAP is the thing worth reviewing,
|
|
9
|
+
and you cannot see a handoff by looking at either card alone.
|
|
10
|
+
|
|
11
|
+
THE BOTTOM LINK IS THE POINT OF THE CARD. "Close" is the never-mind out, and it
|
|
12
|
+
is deliberately the quiet option while the destructive action wears the primary
|
|
13
|
+
button. A confirmation dialog whose safe path is harder to find than its
|
|
14
|
+
destructive one is not a confirmation, it is a formality.
|
|
15
|
+
|
|
16
|
+
Single root: the outer <div> is the host's required root, and everything inside
|
|
17
|
+
the double-quoted x-data is single-quoted — an inner double quote closes the
|
|
18
|
+
attribute and mounts the component as a silent no-op.
|
|
19
|
+
%>
|
|
20
|
+
<div x-data="{
|
|
21
|
+
saving: false,
|
|
22
|
+
unsubscribe() {
|
|
23
|
+
if (this.saving) return;
|
|
24
|
+
this.saving = true;
|
|
25
|
+
var s = this;
|
|
26
|
+
// Demo: the real card awaits a POST. Hold briefly so the pending state
|
|
27
|
+
// is visible rather than skipped, then hand off exactly as it does.
|
|
28
|
+
setTimeout(function () {
|
|
29
|
+
s.saving = false;
|
|
30
|
+
$store.dsModals.swap('unsubscribe-goodbye');
|
|
31
|
+
}, 450);
|
|
32
|
+
}
|
|
33
|
+
}">
|
|
34
|
+
<%= render "studio/modals/blocks/card_header",
|
|
35
|
+
icon_emoji: "📭",
|
|
36
|
+
title: "Leave the newsletter?",
|
|
37
|
+
subtitle: "You'll stop getting sports news and contest updates. You can rejoin anytime." %>
|
|
38
|
+
|
|
39
|
+
<button type="button" @click="unsubscribe()" :disabled="saving"
|
|
40
|
+
class="btn btn-primary btn-lg w-full disabled:opacity-60">
|
|
41
|
+
<span x-show="!saving">Unsubscribe</span>
|
|
42
|
+
<span x-show="saving" class="inline-flex items-center justify-center gap-2" style="display: none;">
|
|
43
|
+
<span class="spinner" aria-hidden="true"></span>
|
|
44
|
+
Unsubscribing…
|
|
45
|
+
</span>
|
|
46
|
+
</button>
|
|
47
|
+
|
|
48
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
49
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
50
|
+
Close
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the final beat after a confirmed unsubscribe.
|
|
3
|
+
|
|
4
|
+
The smallest card in the guide, and it earns its place: it is the half of the
|
|
5
|
+
newsletter story the guide could not previously show. The Profile Leveling
|
|
6
|
+
section already walked the way IN (Join the Newsletter → Subscribed!); this and
|
|
7
|
+
its sibling walk the way OUT. A section that only ever demonstrates opt-in is
|
|
8
|
+
not showing the flow, it is showing the happy half of it.
|
|
9
|
+
|
|
10
|
+
APP-SPECIFIC, engine chrome only: the engine owns no newsletter. It lends
|
|
11
|
+
`blocks/_card_header` and the tone.
|
|
12
|
+
|
|
13
|
+
NOTE THE COPY IS NOT A GUILT TRIP — "Change your mind anytime from your
|
|
14
|
+
account" is a door left open, not a plea. Worth preserving if this is ever
|
|
15
|
+
reworded.
|
|
16
|
+
|
|
17
|
+
Single root: the outer <div> is the host's required root.
|
|
18
|
+
%>
|
|
19
|
+
<div x-data="{}">
|
|
20
|
+
<%= render "studio/modals/blocks/card_header",
|
|
21
|
+
size: :lg,
|
|
22
|
+
icon_emoji: "✌️",
|
|
23
|
+
title: "See you later",
|
|
24
|
+
subtitle: "You're off the list. Change your mind anytime from your account." %>
|
|
25
|
+
|
|
26
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
27
|
+
class="btn btn-primary btn-lg w-full">
|
|
28
|
+
Close
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the onboarding chain's LAST step, "Set up your
|
|
3
|
+
wallet" (chain step 3 of 3).
|
|
4
|
+
|
|
5
|
+
This documents an APP-SPECIFIC flow. The engine deliberately does NOT own it:
|
|
6
|
+
turf-monster's app/views/modals/_wallet_setup.html.erb is ~576 lines of
|
|
7
|
+
extension-presence polling, a hosted 90-second walkthrough video, a Detailed
|
|
8
|
+
Guide route that resolves at render time, and a managed-wallet fallback for
|
|
9
|
+
grandfathered web2 accounts. None of that is shareable — it is one app's
|
|
10
|
+
answer to "how do we get this person a wallet".
|
|
11
|
+
|
|
12
|
+
What IS shared, and all this specimen composes, is the CHROME:
|
|
13
|
+
studio/modals/blocks/shell — title row + close
|
|
14
|
+
studio/modals/blocks/progress_pill — the 3-of-3 chain position
|
|
15
|
+
studio/modals/blocks/wallet_brand_sprite — inline brand marks, no per-app PNG
|
|
16
|
+
plus the house wallet-row idiom (brand tile, the wallet's own name, status on
|
|
17
|
+
the right, chevron) and pulse-cta, so a wallet reads identically here, in the
|
|
18
|
+
connect picker, and on the web3 step-up card.
|
|
19
|
+
|
|
20
|
+
ONE card, ONE id, a `detected` TOGGLE for the two ends of the real progression
|
|
21
|
+
(install -> waiting -> Installed -> connect). Deliberately not two specimens
|
|
22
|
+
sharing a modal id: that shape is what left the two web3-step-up cards unable
|
|
23
|
+
to tell each other apart in the glow, fixed in the same pass that added this.
|
|
24
|
+
|
|
25
|
+
WHO SEES IT — the half worth remembering. This card and the web3 step-up card
|
|
26
|
+
are OPPOSITE populations and no user can ever meet both. WalletSetupPolicy
|
|
27
|
+
exits at its own step 1 for an account that already holds a wallet; the step-up
|
|
28
|
+
card is only ever shown to accounts that DO hold one. "Get a wallet" vs "prove
|
|
29
|
+
the wallet you have".
|
|
30
|
+
|
|
31
|
+
Self-contained: no install detection, no video embed, no real routes. The
|
|
32
|
+
walkthrough is represented as a still placeholder rather than an <iframe>,
|
|
33
|
+
because the video is the app's asset and an embed here would reach off-host.
|
|
34
|
+
|
|
35
|
+
Single root: the outer <div> is the host's required root.
|
|
36
|
+
%>
|
|
37
|
+
<div x-data="{
|
|
38
|
+
get props() { var c = Alpine.store('dsModals').current(); return (c && c.props) || {}; },
|
|
39
|
+
get detected() { return !!this.props.detected; }
|
|
40
|
+
}"
|
|
41
|
+
class="relative">
|
|
42
|
+
<%= render "studio/modals/blocks/wallet_brand_sprite" %>
|
|
43
|
+
|
|
44
|
+
<%= render layout: "studio/modals/blocks/shell",
|
|
45
|
+
locals: { title: "Set up your wallet", modal_store: "dsModals" } do %>
|
|
46
|
+
|
|
47
|
+
<%# Chain position — the HOST's, not this page's. It is the only one of the
|
|
48
|
+
three onboarding steps that fills the pill, which is why the pill is here
|
|
49
|
+
even though the card sits in the Web3 section beside the wallet cards. %>
|
|
50
|
+
<div class="mb-3">
|
|
51
|
+
<%= render "studio/modals/blocks/progress_pill", current: 3, total: 3 %>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<p class="text-sm text-body text-center mb-4">
|
|
55
|
+
This app runs on the Solana public blockchain; you will need a wallet to
|
|
56
|
+
enter contests and collect winnings.
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
<%# THE wallet row — the house idiom, glowing because it is the one thing to
|
|
60
|
+
press. Same shape the connect picker and the step-up card use. The right
|
|
61
|
+
side is the only part that moves: INSTALL before the extension exists,
|
|
62
|
+
an Installed badge once it answers. %>
|
|
63
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
64
|
+
style="--pulse-cta-color: var(--color-primary); --pulse-cta-strength: 0.4; --pulse-cta-scale: 1.02"
|
|
65
|
+
class="pulse-cta w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition text-left">
|
|
66
|
+
<span class="w-9 h-9 rounded-lg overflow-hidden flex items-center justify-center shrink-0">
|
|
67
|
+
<svg class="w-9 h-9" aria-hidden="true"><use href="#se-wallet-phantom"></use></svg>
|
|
68
|
+
</span>
|
|
69
|
+
<span class="font-semibold text-heading">Phantom</span>
|
|
70
|
+
<span class="ml-auto flex items-center gap-2">
|
|
71
|
+
<span x-show="!detected" class="text-xs text-muted uppercase tracking-wide">Install</span>
|
|
72
|
+
<span x-show="detected" class="badge border-primary text-primary">Installed</span>
|
|
73
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-muted">
|
|
74
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
75
|
+
</svg>
|
|
76
|
+
</span>
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<%# The teaching half. A brand-new player does not know what a wallet IS, so
|
|
80
|
+
the card carries its own explainer rather than sending them away to find
|
|
81
|
+
one. Represented, not embedded — see the header. %>
|
|
82
|
+
<div class="mt-4 pt-4 border-t border-strong">
|
|
83
|
+
<h4 class="text-heading font-bold text-sm mb-1">New to Solana 🤔</h4>
|
|
84
|
+
<p class="text-sm text-body mb-3">Set up your wallet in <strong>90 seconds</strong>.</p>
|
|
85
|
+
|
|
86
|
+
<div class="rounded-lg border border-subtle bg-inset aspect-video flex flex-col items-center justify-center gap-1">
|
|
87
|
+
<span class="text-2xl leading-none" aria-hidden="true">▶️</span>
|
|
88
|
+
<span class="text-2xs text-muted">walkthrough — the app supplies the video</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
92
|
+
class="btn btn-primary btn-lg w-full mt-3 flex items-center justify-center gap-2">
|
|
93
|
+
Detailed Guide
|
|
94
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
|
95
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
96
|
+
</svg>
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<%# Dismissible by design, and the fine print says what dismissing costs. The
|
|
101
|
+
chain is skippable; the entry gate is not, and the card is honest that
|
|
102
|
+
the two are different things. %>
|
|
103
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
104
|
+
class="block mx-auto mt-4 text-sm text-secondary hover:text-heading transition">
|
|
105
|
+
Maybe later
|
|
106
|
+
</button>
|
|
107
|
+
<p class="mt-2 text-center text-2xs text-muted">
|
|
108
|
+
Rather pay by card? Link a wallet first — an entry token needs
|
|
109
|
+
somewhere to land.
|
|
110
|
+
</p>
|
|
111
|
+
<% end %>
|
|
112
|
+
</div>
|
data/lib/studio/version.rb
CHANGED
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.60.
|
|
4
|
+
version: 0.60.1
|
|
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-08-
|
|
11
|
+
date: 2026-08-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -401,8 +401,8 @@ files:
|
|
|
401
401
|
- app/views/schema/index.html.erb
|
|
402
402
|
- app/views/sessions/_sso_continue.html.erb
|
|
403
403
|
- app/views/sessions/new.html.erb
|
|
404
|
-
- app/views/studio/_age_verify_assets.html.erb
|
|
405
404
|
- app/views/studio/_at_time_script.html.erb
|
|
405
|
+
- app/views/studio/_birthday_assets.html.erb
|
|
406
406
|
- app/views/studio/_board_assets.html.erb
|
|
407
407
|
- app/views/studio/_confirm_interstitial.html.erb
|
|
408
408
|
- app/views/studio/_cropper_assets.html.erb
|
|
@@ -446,7 +446,8 @@ files:
|
|
|
446
446
|
- app/views/studio/modals/_scoped_host.html.erb
|
|
447
447
|
- app/views/studio/modals/_web3_step_up.html.erb
|
|
448
448
|
- app/views/studio/modals/auth/_resend_footer.html.erb
|
|
449
|
-
- app/views/studio/modals/blocks/
|
|
449
|
+
- app/views/studio/modals/blocks/_age_gate.html.erb
|
|
450
|
+
- app/views/studio/modals/blocks/_birthday.html.erb
|
|
450
451
|
- app/views/studio/modals/blocks/_card_header.html.erb
|
|
451
452
|
- app/views/studio/modals/blocks/_change_username.html.erb
|
|
452
453
|
- app/views/studio/modals/blocks/_cta_redirect.html.erb
|
|
@@ -502,12 +503,18 @@ files:
|
|
|
502
503
|
- app/views/style/_tricks.html.erb
|
|
503
504
|
- app/views/style/board/_demo_card.html.erb
|
|
504
505
|
- app/views/style/index.html.erb
|
|
505
|
-
- app/views/style/modals/
|
|
506
|
+
- app/views/style/modals/_age_gate.html.erb
|
|
506
507
|
- app/views/style/modals/_auth.html.erb
|
|
508
|
+
- app/views/style/modals/_birthday.html.erb
|
|
509
|
+
- app/views/style/modals/_cosign_rejected.html.erb
|
|
507
510
|
- app/views/style/modals/_entry_tokens.html.erb
|
|
508
511
|
- app/views/style/modals/_onchain_tx.html.erb
|
|
512
|
+
- app/views/style/modals/_quest_success.html.erb
|
|
513
|
+
- app/views/style/modals/_unsubscribe_confirm.html.erb
|
|
514
|
+
- app/views/style/modals/_unsubscribe_goodbye.html.erb
|
|
509
515
|
- app/views/style/modals/_wallet_connect.html.erb
|
|
510
516
|
- app/views/style/modals/_wallet_deposit.html.erb
|
|
517
|
+
- app/views/style/modals/_wallet_setup.html.erb
|
|
511
518
|
- app/views/theme_settings/edit.html.erb
|
|
512
519
|
- app/views/user_mailer/magic_link.html.erb
|
|
513
520
|
- app/views/user_mailer/magic_link.text.erb
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
Age-gate DOB modal — engine primitive. Collects a real date of birth
|
|
3
|
-
(Month / Day / Year), shows a live "too young" hint, and submits. Homed in the
|
|
4
|
-
engine as the heavier sibling of studio/modals/shared/_age_attestation (the
|
|
5
|
-
lighter one-checkbox legal attestation). RUN ONE OR THE OTHER, not both: the
|
|
6
|
-
attestation is a signup-time "I confirm I'm of legal age" checkbox; this is an
|
|
7
|
-
entry-time DOB gate that a server can recompute and stamp.
|
|
8
|
-
|
|
9
|
-
CRITICAL — the engine hardcodes NO legal policy. It renders the modal UI, the
|
|
10
|
-
DOB fields, and the submit; everything legal is app-supplied:
|
|
11
|
-
|
|
12
|
-
min_age (Integer, REQUIRED) — the app's minimum age. There is NO engine
|
|
13
|
-
default (18 is itself a policy value); the app resolves it from
|
|
14
|
-
its own policy against its own server-detected jurisdiction.
|
|
15
|
-
submit_url (String, REQUIRED) — the app endpoint the DOB POSTs to. The
|
|
16
|
-
app owns the authoritative recompute + DOB persistence; the modal
|
|
17
|
-
only shows the response.
|
|
18
|
-
state (String, optional) — a PASSIVE jurisdiction label for the copy
|
|
19
|
-
(e.g. "CA"). Server-detected upstream; the modal never detects
|
|
20
|
-
geography and never offers an editable state field (a spoofable
|
|
21
|
-
client state must not be able to lower the bar). Blank => the
|
|
22
|
-
jurisdiction clause is dropped.
|
|
23
|
-
title (String, optional) — modal title. Default "Verify your age".
|
|
24
|
-
intro (String, optional) — the lead sentence after the age line.
|
|
25
|
-
fine_print (String, optional) — the app's legal / jurisdiction copy (e.g.
|
|
26
|
-
its per-state age table). The engine ships only a neutral,
|
|
27
|
-
policy-free default; the app passes its real copy here.
|
|
28
|
-
modal_store (String, optional) — Alpine store backing close(). Default "modals".
|
|
29
|
-
demo (Boolean, optional) — style-guide preview: resolve locally, no POST.
|
|
30
|
-
|
|
31
|
-
Requires window.ageVerifyModal — render studio/_age_verify_assets once at
|
|
32
|
-
layout level (the factory can't ship inside this template; a cloned script
|
|
33
|
-
never runs). Single root element (modal-host template mount rule).
|
|
34
|
-
%>
|
|
35
|
-
<%
|
|
36
|
-
min_age = local_assigns.fetch(:min_age)
|
|
37
|
-
submit_url = local_assigns.fetch(:submit_url)
|
|
38
|
-
state = local_assigns[:state].to_s.strip
|
|
39
|
-
title = local_assigns.fetch(:title, "Verify your age")
|
|
40
|
-
intro = local_assigns.fetch(:intro, "Enter your date of birth to confirm you're eligible — we only ask once.")
|
|
41
|
-
fine_print = local_assigns.fetch(:fine_print, "We use your date of birth only to confirm eligibility.")
|
|
42
|
-
modal_store = local_assigns.fetch(:modal_store, "modals")
|
|
43
|
-
demo = local_assigns.fetch(:demo, false)
|
|
44
|
-
%>
|
|
45
|
-
<div x-data="ageVerifyModal({ minAge: <%= min_age.to_i %>, state: '<%= j state %>', url: '<%= j submit_url %>', store: '<%= j modal_store %>', demo: <%= demo ? 'true' : 'false' %> })">
|
|
46
|
-
<%= render layout: "studio/modals/blocks/shell", locals: { title: title, modal_store: modal_store } do %>
|
|
47
|
-
<p class="text-sm text-muted mb-4">
|
|
48
|
-
You must be <span class="text-heading font-semibold"><%= min_age %>+</span><%= state.present? ? " in #{state}" : "" %>.
|
|
49
|
-
<%= intro %>
|
|
50
|
-
</p>
|
|
51
|
-
|
|
52
|
-
<%# Month / Day / Year — the engine's one DOB field, shared with the profile
|
|
53
|
-
page's birthday row. It lived here first and turf-monster forked a second
|
|
54
|
-
copy; extracting it is what stops the two drifting. %>
|
|
55
|
-
<div class="mb-4">
|
|
56
|
-
<%= render "studio/fields/date_of_birth" %>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<%# Live too-young hint — shown once a full date is picked and it fails the
|
|
60
|
-
app-supplied bar, so the user sees the problem before submitting. %>
|
|
61
|
-
<template x-if="complete && computedAge !== null && minAge && computedAge < minAge">
|
|
62
|
-
<p class="text-red-400 text-sm mb-3" x-text="'You must be ' + minAge + '+' + (stateCode ? ' in ' + stateCode : '') + '.'"></p>
|
|
63
|
-
</template>
|
|
64
|
-
|
|
65
|
-
<%# Server error (invalid date, or the authoritative too-young check). %>
|
|
66
|
-
<template x-if="error">
|
|
67
|
-
<p class="text-red-400 text-sm mb-3" x-text="error"></p>
|
|
68
|
-
</template>
|
|
69
|
-
|
|
70
|
-
<button type="button" @click="submit()"
|
|
71
|
-
class="btn btn-primary btn-lg w-full"
|
|
72
|
-
:disabled="!complete || submitting || (computedAge !== null && minAge && computedAge < minAge)">
|
|
73
|
-
<span x-show="!submitting">Confirm & Continue</span>
|
|
74
|
-
<span x-show="submitting" class="inline-flex items-center justify-center gap-2" style="display: none;">
|
|
75
|
-
<span class="cta-spinner" aria-hidden="true"></span>
|
|
76
|
-
Verifying…
|
|
77
|
-
</span>
|
|
78
|
-
</button>
|
|
79
|
-
|
|
80
|
-
<p class="text-[11px] text-muted text-center mt-3 leading-snug"><%= fine_print %></p>
|
|
81
|
-
<% end %>
|
|
82
|
-
</div>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
Living style guide specimen for the engine age-gate DOB modal
|
|
3
|
-
(studio/modals/blocks/_age_verify). Opened on the page-scoped dsModals host.
|
|
4
|
-
|
|
5
|
-
The whole point of this specimen is the SEAM: every legal value here is a DEMO
|
|
6
|
-
the app feeds — the engine primitive hardcodes none of it. So we pass an
|
|
7
|
-
illustrative policy (21+ in CA) and a demo fine_print that SAYS so, and run in
|
|
8
|
-
demo mode (no backend — submit resolves locally). A real app swaps these for
|
|
9
|
-
its own min_age / state / submit_url / legal copy resolved server-side.
|
|
10
|
-
%>
|
|
11
|
-
<%= render "studio/modals/blocks/age_verify",
|
|
12
|
-
min_age: 21,
|
|
13
|
-
state: "CA",
|
|
14
|
-
submit_url: "#demo-age-verify",
|
|
15
|
-
modal_store: "dsModals",
|
|
16
|
-
demo: true,
|
|
17
|
-
fine_print: "Demo policy the app supplies: 21+ in CA. The engine hardcodes no age and no state table — your app passes min_age, the jurisdiction label, this copy, and the endpoint that recomputes it server-side." %>
|