studio-engine 0.20.0 → 0.22.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 +145 -0
- data/app/assets/javascripts/studio/canvas_confetti.js +17 -0
- data/app/assets/javascripts/studio/studio_confetti.js +93 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +47 -0
- data/app/views/layouts/studio/_head.html.erb +7 -1
- data/app/views/studio/_age_verify_assets.html.erb +120 -0
- data/app/views/studio/_leveling_activity_assets.html.erb +174 -0
- data/app/views/studio/modals/blocks/_age_verify.html.erb +106 -0
- data/app/views/studio/modals/blocks/_change_username.html.erb +46 -0
- data/app/views/studio/modals/blocks/_leveling_activity.html.erb +159 -0
- data/app/views/studio/modals/blocks/_wallet_brand_sprite.html.erb +15 -26
- data/app/views/style/_modals.html.erb +231 -2
- data/app/views/style/_tricks.html.erb +46 -0
- data/app/views/style/modals/_age_verify.html.erb +17 -0
- data/app/views/style/modals/_entry_tokens.html.erb +123 -0
- data/lib/studio/engine.rb +2 -0
- data/lib/studio/version.rb +1 -1
- metadata +10 -1
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
card behind ours. So this section owns a tiny, self-contained store + overlay
|
|
15
15
|
and registers all specimen content itself.
|
|
16
16
|
|
|
17
|
-
Groups: Auth (the sign-in step machine),
|
|
17
|
+
Groups: Auth (the sign-in step machine), Eligibility and entry (the
|
|
18
|
+
app-supplied-policy age-gate DOB primitive, gated by Studio.feature?(:age_gate),
|
|
19
|
+
plus the app-specific entry-tokens purchase flow documented via engine chrome),
|
|
20
|
+
Profile (crop / upload, opened
|
|
18
21
|
through studio/cropper_assets), Web3 (wallet-connect with engine-owned inline
|
|
19
22
|
brand icons / on-chain-tx / deposit / the generic entry-confirmed celebration,
|
|
20
23
|
gated by Studio.feature?(:web3) — disabled-but-present-yet-openable when off),
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
<%
|
|
29
32
|
web3_on = Studio.feature?(:web3)
|
|
30
33
|
leveling_on = Studio.feature?(:leveling)
|
|
34
|
+
age_gate_on = Studio.feature?(:age_gate)
|
|
31
35
|
|
|
32
36
|
# Auth method-toggle defaults — mirror the app's Studio.auth_method? config;
|
|
33
37
|
# Solana Wallet also needs the web3 capability, so it defaults OFF on an app
|
|
@@ -51,8 +55,17 @@
|
|
|
51
55
|
expr
|
|
52
56
|
end
|
|
53
57
|
%>
|
|
58
|
+
<%# DEMO flow wiring (style-page only): the age-gate primitive fires an
|
|
59
|
+
'age-verified' window event on a successful confirm — its designed handoff
|
|
60
|
+
hook, the same one a production app listens for to resume the entry. Here the
|
|
61
|
+
style page plays that "app": on age-verified it opens the entry-token purchase
|
|
62
|
+
modal, so the DS walks eligibility to purchase in sequence. The engine
|
|
63
|
+
primitive is untouched; its real submit still posts to the app-supplied
|
|
64
|
+
submit_url. Kept on the section (always mounted) so it fires even as the
|
|
65
|
+
age-gate card closes. %>
|
|
54
66
|
<section id="modals" class="space-y-10" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)"
|
|
55
|
-
x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
|
|
67
|
+
x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
|
|
68
|
+
@age-verified.window="$store.dsModals.open('entry-tokens', { step: 'picker' })">
|
|
56
69
|
<div class="space-y-1">
|
|
57
70
|
<h2 class="text-2xl font-bold text-heading">Modals</h2>
|
|
58
71
|
<p class="text-muted text-sm">
|
|
@@ -237,6 +250,16 @@
|
|
|
237
250
|
defines the factory inline; no importmap change). %>
|
|
238
251
|
<%= render "studio/cropper_assets" %>
|
|
239
252
|
|
|
253
|
+
<%# ageVerifyModal factory — rendered here at page level (never inside the
|
|
254
|
+
overlay template, where a cloned script would not run) so the age-gate
|
|
255
|
+
specimen opens live. A consumer app renders this in its layout the same way. %>
|
|
256
|
+
<%= render "studio/age_verify_assets" %>
|
|
257
|
+
|
|
258
|
+
<%# levelingActionModal factory — page level (never inside the overlay template,
|
|
259
|
+
where a cloned script would not run) so the change-username + quest-activity
|
|
260
|
+
specimens open live. A consumer app renders this in its layout the same way. %>
|
|
261
|
+
<%= render "studio/leveling_activity_assets" %>
|
|
262
|
+
|
|
240
263
|
<%# ---- The overlay: one backdrop + card; every specimen's real content is a
|
|
241
264
|
single-root <template x-if> gated on the current modal id. Faithful to the
|
|
242
265
|
shared host — backdrop fade, scroll lock, escape + click-self dismissal
|
|
@@ -257,6 +280,14 @@
|
|
|
257
280
|
<div><%= render "style/modals/auth" %></div>
|
|
258
281
|
</template>
|
|
259
282
|
|
|
283
|
+
<%# --- Eligibility + entry --- %>
|
|
284
|
+
<template x-if="$store.dsModals.current().id === 'age-verify'">
|
|
285
|
+
<div><%= render "style/modals/age_verify" %></div>
|
|
286
|
+
</template>
|
|
287
|
+
<template x-if="$store.dsModals.current().id === 'entry-tokens'">
|
|
288
|
+
<div><%= render "style/modals/entry_tokens" %></div>
|
|
289
|
+
</template>
|
|
290
|
+
|
|
260
291
|
<%# --- Profile (real engine partials, opened on dsModals) --- %>
|
|
261
292
|
<template x-if="$store.dsModals.current().id === 'crop-photo'">
|
|
262
293
|
<div><%= render "studio/modals/crop_photo", store: "dsModals" %></div>
|
|
@@ -350,6 +381,49 @@
|
|
|
350
381
|
subtitle: "You leveled up! A Free Entry lands in your account soon. Keep leveling for more." %></div>
|
|
351
382
|
</template>
|
|
352
383
|
|
|
384
|
+
<%# --- Leveling activities: change username + quest activity --- %>
|
|
385
|
+
<%# Each id renders the SAME engine primitive with an EXPLICIT leveling:
|
|
386
|
+
flag, so BOTH modes stay visible here regardless of this app's real
|
|
387
|
+
:leveling setting. All demo:true — the save resolves locally (no
|
|
388
|
+
backend), and the on-chain-accurate copy is passed as an app-authored
|
|
389
|
+
description (the engine default stays neutral). %>
|
|
390
|
+
<template x-if="$store.dsModals.current().id === 'change-username'">
|
|
391
|
+
<div><%= render "studio/modals/blocks/change_username",
|
|
392
|
+
modal_store: "dsModals", demo: true, leveling: true,
|
|
393
|
+
current_username: "turfmonster",
|
|
394
|
+
submit_url: "#demo-change-username",
|
|
395
|
+
description: "Your username is stored on-chain. Changes are saved via your managed wallet.",
|
|
396
|
+
quest_label: "Quest 1 of 4" %></div>
|
|
397
|
+
</template>
|
|
398
|
+
<template x-if="$store.dsModals.current().id === 'change-username-plain'">
|
|
399
|
+
<div><%= render "studio/modals/blocks/change_username",
|
|
400
|
+
modal_store: "dsModals", demo: true, leveling: false,
|
|
401
|
+
current_username: "mcritchie",
|
|
402
|
+
submit_url: "#demo-change-username",
|
|
403
|
+
description: "Choose the name shown across your account." %></div>
|
|
404
|
+
</template>
|
|
405
|
+
<template x-if="$store.dsModals.current().id === 'quest-activity'">
|
|
406
|
+
<div><%= render "studio/modals/blocks/leveling_activity",
|
|
407
|
+
modal_store: "dsModals", demo: true, leveling: true,
|
|
408
|
+
submit_url: "#demo-quest",
|
|
409
|
+
title: "Join the Newsletter",
|
|
410
|
+
description: "Sports news and contest updates — complete the quest for seeds.",
|
|
411
|
+
input: true, input_type: "email", placeholder: "you@example.com",
|
|
412
|
+
cta_label: "Subscribe", saved_label: "Subscribed", saving_label: "Subscribing…",
|
|
413
|
+
quest_label: "Quest 2 of 4",
|
|
414
|
+
icon_emoji: "📬", celebrate_title: "You're in",
|
|
415
|
+
celebrate_subtitle: "Nice — you earned seeds for joining. Keep completing quests to earn a Free Entry." %></div>
|
|
416
|
+
</template>
|
|
417
|
+
<template x-if="$store.dsModals.current().id === 'quest-activity-plain'">
|
|
418
|
+
<div><%= render "studio/modals/blocks/leveling_activity",
|
|
419
|
+
modal_store: "dsModals", demo: true, leveling: false,
|
|
420
|
+
submit_url: "#demo-quest",
|
|
421
|
+
title: "Join the Newsletter",
|
|
422
|
+
description: "Sports news and contest updates.",
|
|
423
|
+
input: true, input_type: "email", placeholder: "you@example.com",
|
|
424
|
+
cta_label: "Subscribe", saved_label: "Subscribed", saving_label: "Subscribing…" %></div>
|
|
425
|
+
</template>
|
|
426
|
+
|
|
353
427
|
</div>
|
|
354
428
|
</div>
|
|
355
429
|
</template>
|
|
@@ -432,6 +506,68 @@
|
|
|
432
506
|
</div>
|
|
433
507
|
</section>
|
|
434
508
|
|
|
509
|
+
<%# ===================================================================== %>
|
|
510
|
+
<%# 1b. ELIGIBILITY + ENTRY %>
|
|
511
|
+
<%# ===================================================================== %>
|
|
512
|
+
<section class="space-y-5">
|
|
513
|
+
<div class="space-y-1">
|
|
514
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
515
|
+
<h3 class="text-xl font-bold text-heading">Eligibility & entry</h3>
|
|
516
|
+
<% if age_gate_on %>
|
|
517
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">age gate on</span>
|
|
518
|
+
<% else %>
|
|
519
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
|
|
520
|
+
<% end %>
|
|
521
|
+
</div>
|
|
522
|
+
<p class="text-muted text-sm">
|
|
523
|
+
The pre-entry gate and the entry-purchase flow. The <strong>age gate</strong>
|
|
524
|
+
(<code class="font-mono text-2xs">studio/modals/blocks/_age_verify</code>) is a
|
|
525
|
+
NEW engine primitive gated by
|
|
526
|
+
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code> — a DOB
|
|
527
|
+
modal whose minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
528
|
+
<strong>app-supplied</strong> (the engine hardcodes no policy). It is the heavier
|
|
529
|
+
sibling of the <code class="font-mono text-2xs">shared/_age_attestation</code>
|
|
530
|
+
checkbox inside Sign in above — <strong>run one or the other</strong>. The
|
|
531
|
+
<strong>entry-tokens</strong> flow is <strong>app-specific</strong> (payments
|
|
532
|
+
product): documented here composing engine chrome, but the packs, rails, and
|
|
533
|
+
the on-chain mint stay app-owned.
|
|
534
|
+
</p>
|
|
535
|
+
</div>
|
|
536
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
537
|
+
<%# Age gate — engine primitive, gated on :age_gate (disabled-but-present). %>
|
|
538
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
539
|
+
label: "Age gate (DOB)",
|
|
540
|
+
reference: %(the "Age gate" DOB modal (studio-engine studio/modals/blocks/_age_verify) — a NEW engine primitive; the app supplies min_age / state / submit_url / fine_print and the engine hardcodes no legal policy. In this demo, Confirm & Continue advances to the Entry tokens purchase (page-level dsModals wiring on the primitive's 'age-verified' hook; the primitive's real submit posts to submit_url). Open with $store.dsModals.open('age-verify')),
|
|
541
|
+
open_expr: "$store.dsModals.open('age-verify')",
|
|
542
|
+
glow_when: ds_glow.call("age-verify"),
|
|
543
|
+
disabled: !age_gate_on, openable: true, disabled_label: "age gate off" } do %>
|
|
544
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
545
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
546
|
+
<div class="grid grid-cols-3 gap-1">
|
|
547
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
548
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
549
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
550
|
+
</div>
|
|
551
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
552
|
+
</div>
|
|
553
|
+
<% end %>
|
|
554
|
+
|
|
555
|
+
<%# Entry tokens — app-specific flow, documented (not capability-gated). %>
|
|
556
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
557
|
+
label: "Entry tokens",
|
|
558
|
+
reference: %(the "Entry tokens" purchase flow (app-specific — documented in the engine style guide composing studio/modals/blocks chrome). The packs, pricing, rails, and on-chain mint stay app-owned. Open with $store.dsModals.open('entry-tokens', { step: 'picker' })),
|
|
559
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
|
|
560
|
+
glow_when: ds_glow.call("entry-tokens") } do %>
|
|
561
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
562
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
563
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
564
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
|
|
565
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
566
|
+
</div>
|
|
567
|
+
<% end %>
|
|
568
|
+
</div>
|
|
569
|
+
</section>
|
|
570
|
+
|
|
435
571
|
<%# ===================================================================== %>
|
|
436
572
|
<%# 2. PROFILE — crop / upload, live via cropper_assets %>
|
|
437
573
|
<%# ===================================================================== %>
|
|
@@ -771,4 +907,97 @@
|
|
|
771
907
|
<% end %>
|
|
772
908
|
</div>
|
|
773
909
|
</section>
|
|
910
|
+
|
|
911
|
+
<%# ===================================================================== %>
|
|
912
|
+
<%# 7. LEVELING ACTIVITIES — change username + quest activities %>
|
|
913
|
+
<%# ===================================================================== %>
|
|
914
|
+
<section class="space-y-5">
|
|
915
|
+
<div class="space-y-1">
|
|
916
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
917
|
+
<h3 class="text-xl font-bold text-heading">Leveling activities</h3>
|
|
918
|
+
<% if leveling_on %>
|
|
919
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">leveling on</span>
|
|
920
|
+
<% else %>
|
|
921
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">leveling off</span>
|
|
922
|
+
<% end %>
|
|
923
|
+
</div>
|
|
924
|
+
<p class="text-muted text-sm">
|
|
925
|
+
The quest/leveling action modals
|
|
926
|
+
(<code class="font-mono text-2xs">studio/modals/blocks/_change_username</code>,
|
|
927
|
+
<code class="font-mono text-2xs">studio/modals/blocks/_leveling_activity</code>),
|
|
928
|
+
migrated from Turf Monster. ONE primitive, <strong>two modes</strong>, decided
|
|
929
|
+
by <code class="font-mono text-2xs">Studio.feature?(:leveling)</code>:
|
|
930
|
+
<strong>leveling on</strong> adds the quest pill + the seeds level-up
|
|
931
|
+
celebration; <strong>leveling off</strong> is the plain action modal (just
|
|
932
|
+
the input + Save + a Saved state — the McRitchie Studio shape). Each modal is
|
|
933
|
+
shown <strong>both ways</strong> below (the specimens pass an explicit
|
|
934
|
+
<code class="font-mono text-2xs">leveling:</code> so both render regardless of
|
|
935
|
+
this app's flag). The actual save is an <strong>app-supplied callback</strong>:
|
|
936
|
+
the modal POSTs to <code class="font-mono text-2xs">submit_url</code> and, for
|
|
937
|
+
an app that needs a wallet step, delegates it opaquely to an app-supplied
|
|
938
|
+
<code class="font-mono text-2xs">finalize_hook</code> — <strong>no wallet,
|
|
939
|
+
signing, or on-chain write lives in the engine</strong>. (These demos resolve
|
|
940
|
+
the save locally.)
|
|
941
|
+
</p>
|
|
942
|
+
</div>
|
|
943
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
944
|
+
<%# Change username — leveling ON: quest pill + seeds celebration on save. %>
|
|
945
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
946
|
+
label: "Change username (leveling)",
|
|
947
|
+
reference: %(the "Change username" modal WITH leveling (studio-engine studio/modals/blocks/_change_username, leveling: true) — quest pill + seeds level-up celebration on save. The on-chain save is an app-supplied callback (submit_url + an opaque finalize_hook); the engine is UI only. Open with $store.dsModals.open('change-username')),
|
|
948
|
+
open_expr: "$store.dsModals.open('change-username')",
|
|
949
|
+
glow_when: ds_glow.call("change-username") } do %>
|
|
950
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
951
|
+
<div class="flex items-center justify-between gap-2">
|
|
952
|
+
<span class="block h-2 w-16 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
953
|
+
<span class="text-[8px] font-semibold uppercase rounded-full px-1.5 py-0.5" style="color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 12%, transparent)">Quest 1/4</span>
|
|
954
|
+
</div>
|
|
955
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
956
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
957
|
+
</div>
|
|
958
|
+
<% end %>
|
|
959
|
+
|
|
960
|
+
<%# Change username — leveling OFF: the plain MS-style action + Save. %>
|
|
961
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
962
|
+
label: "Change username (plain)",
|
|
963
|
+
reference: %(the "Change username" modal WITHOUT leveling (studio-engine studio/modals/blocks/_change_username, leveling: false) — the plain McRitchie Studio shape: input + Save, then a Saved state. No quest pill, no seeds. Same primitive, flag off. Open with $store.dsModals.open('change-username-plain')),
|
|
964
|
+
open_expr: "$store.dsModals.open('change-username-plain')",
|
|
965
|
+
glow_when: ds_glow.call("change-username-plain") } do %>
|
|
966
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
967
|
+
<span class="block h-2 w-16 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
968
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
969
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
970
|
+
</div>
|
|
971
|
+
<% end %>
|
|
972
|
+
|
|
973
|
+
<%# Quest activity — leveling ON: generic quest (newsletter join) + seeds. %>
|
|
974
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
975
|
+
label: "Quest activity (leveling)",
|
|
976
|
+
reference: %(a generic quest activity WITH leveling (studio-engine studio/modals/blocks/_leveling_activity, leveling: true) — here a "Join the Newsletter" quest: quest pill + seeds celebration on complete. Covers join-newsletter / send-message and the rest as ONE parameterized primitive. Open with $store.dsModals.open('quest-activity')),
|
|
977
|
+
open_expr: "$store.dsModals.open('quest-activity')",
|
|
978
|
+
glow_when: ds_glow.call("quest-activity") } do %>
|
|
979
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
980
|
+
<div class="flex items-center justify-between gap-2">
|
|
981
|
+
<span class="text-lg leading-none">📬</span>
|
|
982
|
+
<span class="text-[8px] font-semibold uppercase rounded-full px-1.5 py-0.5" style="color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 12%, transparent)">Quest 2/4</span>
|
|
983
|
+
</div>
|
|
984
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
985
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
986
|
+
</div>
|
|
987
|
+
<% end %>
|
|
988
|
+
|
|
989
|
+
<%# Quest activity — leveling OFF: the plain action (no quest, no seeds). %>
|
|
990
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
991
|
+
label: "Quest activity (plain)",
|
|
992
|
+
reference: %(the SAME generic activity WITHOUT leveling (studio-engine studio/modals/blocks/_leveling_activity, leveling: false) — a plain "Join the Newsletter" action: input + Subscribe, then Subscribed. No quest pill, no seeds. Open with $store.dsModals.open('quest-activity-plain')),
|
|
993
|
+
open_expr: "$store.dsModals.open('quest-activity-plain')",
|
|
994
|
+
glow_when: ds_glow.call("quest-activity-plain") } do %>
|
|
995
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
996
|
+
<span class="text-lg leading-none">📬</span>
|
|
997
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
998
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
999
|
+
</div>
|
|
1000
|
+
<% end %>
|
|
1001
|
+
</div>
|
|
1002
|
+
</section>
|
|
774
1003
|
</section>
|
|
@@ -259,6 +259,52 @@
|
|
|
259
259
|
</div>
|
|
260
260
|
</section>
|
|
261
261
|
|
|
262
|
+
<!-- Confetti & pulse -->
|
|
263
|
+
<section class="space-y-5">
|
|
264
|
+
<div class="space-y-1">
|
|
265
|
+
<h3 class="text-xl font-bold text-heading">Confetti & pulse</h3>
|
|
266
|
+
<p class="text-muted text-sm max-w-2xl">
|
|
267
|
+
Celebration effects lifted from Turf Monster into the engine. Confetti is
|
|
268
|
+
<code class="font-mono text-2xs">window.studioConfetti</code> - a zero-dependency,
|
|
269
|
+
no-CDN effect (canvas-confetti is vendored into the engine and shipped through the
|
|
270
|
+
asset pipeline). Two callable variants, both honoring
|
|
271
|
+
<code class="font-mono text-2xs">prefers-reduced-motion</code>. The pulse is the
|
|
272
|
+
<code class="font-mono text-2xs">.pulse-cta</code> attention beat. Fire the confetti
|
|
273
|
+
with the buttons below.
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
277
|
+
<%= render layout: "style/specimen",
|
|
278
|
+
locals: { klass: "window.studioConfetti.burst(el)",
|
|
279
|
+
usage: %(window.studioConfetti.burst(cardEl)\n// radial burst from behind a card/modal, from its center) } do %>
|
|
280
|
+
<div x-data="{}" class="flex flex-col items-center gap-3 py-2">
|
|
281
|
+
<div x-ref="burstCard" class="card px-5 py-4 text-sm font-semibold text-heading text-center">Success!</div>
|
|
282
|
+
<button type="button" class="btn btn-primary btn-sm"
|
|
283
|
+
@click="window.studioConfetti && window.studioConfetti.burst($refs.burstCard)">Fire burst</button>
|
|
284
|
+
</div>
|
|
285
|
+
<% end %>
|
|
286
|
+
|
|
287
|
+
<%= render layout: "style/specimen",
|
|
288
|
+
locals: { klass: "window.studioConfetti.cannons()",
|
|
289
|
+
usage: %(window.studioConfetti.cannons()\n// left + right side-cannons, full-screen celebration) } do %>
|
|
290
|
+
<div x-data="{}" class="flex flex-col items-center gap-3 py-2">
|
|
291
|
+
<p class="text-sm font-semibold text-heading text-center">You joined a contest!</p>
|
|
292
|
+
<button type="button" class="btn btn-success btn-sm"
|
|
293
|
+
@click="window.studioConfetti && window.studioConfetti.cannons()">Fire cannons</button>
|
|
294
|
+
</div>
|
|
295
|
+
<% end %>
|
|
296
|
+
|
|
297
|
+
<%= render layout: "style/specimen",
|
|
298
|
+
locals: { klass: ".pulse-cta (knobs: --pulse-cta-color / -speed / -scale)",
|
|
299
|
+
usage: %(<button class="btn btn-primary pulse-cta">Change Username</button>\n<button class="btn btn-success pulse-cta" style="--pulse-cta-color: var(--color-success)">Go</button>) } do %>
|
|
300
|
+
<div class="flex flex-wrap items-center justify-center gap-4">
|
|
301
|
+
<button type="button" class="btn btn-primary pulse-cta">Change Username</button>
|
|
302
|
+
<button type="button" class="btn btn-success pulse-cta" style="--pulse-cta-color: var(--color-success)">Go</button>
|
|
303
|
+
</div>
|
|
304
|
+
<% end %>
|
|
305
|
+
</div>
|
|
306
|
+
</section>
|
|
307
|
+
|
|
262
308
|
<!-- Leveling (capability-gated: Studio.feature?(:leveling)) -->
|
|
263
309
|
<section class="space-y-5">
|
|
264
310
|
<div class="space-y-1">
|
|
@@ -0,0 +1,17 @@
|
|
|
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." %>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the entry-token purchase flow. This documents an
|
|
3
|
+
APP-SPECIFIC flow (Turf Monster's "Get Entry Tokens"); the engine deliberately
|
|
4
|
+
does NOT own the packs, pricing, or payment rails. The specimen composes only
|
|
5
|
+
engine CHROME (studio/modals/blocks/shell, card_header, progress_pill) and
|
|
6
|
+
represents the packs as an illustrative demo — the app owns the payment logic
|
|
7
|
+
and the on-chain mint.
|
|
8
|
+
|
|
9
|
+
Opened on the page-scoped dsModals host with an internal step machine on
|
|
10
|
+
props.step: picker -> confirming -> minted. Single-root templates throughout.
|
|
11
|
+
|
|
12
|
+
On-chain model (documented, not owned here): an entry token is a PROGRAM-OWNED
|
|
13
|
+
prepaid entry credit recorded on-chain (a per-token account), NOT a fungible
|
|
14
|
+
SPL token held in the user's wallet. Two distinct on-chain moments: (1) MINT at
|
|
15
|
+
purchase, server-signed after the payment clears — not on the buyer's click;
|
|
16
|
+
(2) CONSUME at entry, when the app's hold-to-confirm gesture spends one token to
|
|
17
|
+
create the contest entry.
|
|
18
|
+
%>
|
|
19
|
+
<%
|
|
20
|
+
# Illustrative packs only — a real app reads these from its own config
|
|
21
|
+
# (e.g. StripePurchase.available_packs), and they vary by funding mode.
|
|
22
|
+
demo_packs = [
|
|
23
|
+
{ tokens: 1, price: "$5", note: nil },
|
|
24
|
+
{ tokens: 3, price: "$12", note: "Popular" },
|
|
25
|
+
{ tokens: 10, price: "$35", note: "Best value" }
|
|
26
|
+
]
|
|
27
|
+
%>
|
|
28
|
+
<div x-data="{
|
|
29
|
+
get props() { var c = Alpine.store('dsModals').current(); return (c && c.props) || {}; },
|
|
30
|
+
buy(count, balance) {
|
|
31
|
+
Alpine.store('dsModals').advance({ step: 'confirming', mintedCount: count, mintedBalance: balance });
|
|
32
|
+
setTimeout(function () {
|
|
33
|
+
var c = Alpine.store('dsModals').current();
|
|
34
|
+
if (c && c.id === 'entry-tokens') Alpine.store('dsModals').advance({ step: 'minted' });
|
|
35
|
+
}, 1300);
|
|
36
|
+
}
|
|
37
|
+
}" class="relative">
|
|
38
|
+
|
|
39
|
+
<%# Shared close — top-right, above whichever step is active. %>
|
|
40
|
+
<button @click="$store.dsModals.close()"
|
|
41
|
+
class="absolute top-0 right-0 -mt-2 -mr-2 text-secondary hover:text-heading text-xl leading-none z-10"
|
|
42
|
+
aria-label="Close">×</button>
|
|
43
|
+
|
|
44
|
+
<%# ===== PICKER ===== %>
|
|
45
|
+
<template x-if="(props.step || 'picker') === 'picker'">
|
|
46
|
+
<div>
|
|
47
|
+
<div class="text-center pt-1 mb-3">
|
|
48
|
+
<h3 class="text-heading font-bold text-lg leading-tight">Get Entry Tokens</h3>
|
|
49
|
+
</div>
|
|
50
|
+
<%= render "studio/modals/blocks/progress_pill", current: 2, total: 3 %>
|
|
51
|
+
<p class="text-xs text-secondary mb-4 text-center">
|
|
52
|
+
1 token = 1 contest entry. Pick a pack to finish.
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<div class="space-y-2.5">
|
|
56
|
+
<% demo_packs.each do |pack| %>
|
|
57
|
+
<button type="button"
|
|
58
|
+
@click="buy(<%= pack[:tokens] %>, '<%= pack[:tokens] %>')"
|
|
59
|
+
class="w-full flex items-center justify-between gap-3 rounded-lg border border-strong bg-inset px-4 py-3 text-left hover:border-primary transition">
|
|
60
|
+
<span class="flex items-center gap-2">
|
|
61
|
+
<span class="text-heading font-semibold"><%= pack[:tokens] %> token<%= "s" unless pack[:tokens] == 1 %></span>
|
|
62
|
+
<% if pack[:note] %>
|
|
63
|
+
<span class="badge" style="color: var(--color-primary); border-color: var(--color-primary)"><%= pack[:note] %></span>
|
|
64
|
+
<% end %>
|
|
65
|
+
</span>
|
|
66
|
+
<span class="text-heading font-mono font-bold"><%= pack[:price] %></span>
|
|
67
|
+
</button>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<%# Rails — token-funded only. The picker documents which rails mint tokens.
|
|
72
|
+
The USDC-direct path is intentionally NOT a pack here (it's a separate
|
|
73
|
+
rail that transfers USDC and mints no token). %>
|
|
74
|
+
<div class="mt-4 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted">
|
|
75
|
+
<span>Pay by card</span>
|
|
76
|
+
<span aria-hidden="true">·</span>
|
|
77
|
+
<span>PayPal / Venmo</span>
|
|
78
|
+
</div>
|
|
79
|
+
<p class="text-center text-2xs text-muted mt-1">USDC-direct entry is a separate rail — it funds an entry with no token.</p>
|
|
80
|
+
|
|
81
|
+
<%# The seam callout — what the engine owns vs what the app owns. %>
|
|
82
|
+
<div class="mt-4 rounded-lg border border-subtle bg-surface p-3 text-2xs text-secondary leading-snug">
|
|
83
|
+
<p class="text-heading font-semibold mb-1">App-specific flow, engine chrome</p>
|
|
84
|
+
<p class="mb-1">
|
|
85
|
+
The engine supplies only the shell, header, and progress pill. The packs,
|
|
86
|
+
pricing, payment rails, and the on-chain mint are <strong>app-owned</strong> —
|
|
87
|
+
this specimen represents them as a demo.
|
|
88
|
+
</p>
|
|
89
|
+
<p>
|
|
90
|
+
On-chain, an entry token is a <strong>prepaid entry credit recorded on-chain</strong>
|
|
91
|
+
(not a transferable token held in your wallet): minted server-side once payment
|
|
92
|
+
clears, then <strong>consumed</strong> when the app's hold-to-confirm creates the entry.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<%# ===== CONFIRMING (mint-at-purchase) ===== %>
|
|
99
|
+
<template x-if="props.step === 'confirming'">
|
|
100
|
+
<div x-init="setTimeout(function () { var c = Alpine.store('dsModals').current(); if (c && c.id === 'entry-tokens') Alpine.store('dsModals').advance({ step: 'minted' }); }, 1300)">
|
|
101
|
+
<%= render "studio/modals/blocks/card_header",
|
|
102
|
+
spinner: true,
|
|
103
|
+
title: "Confirming your purchase…",
|
|
104
|
+
subtitle: "Payment cleared — a server job mints one prepaid entry credit per token on-chain." %>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<%# ===== MINTED (ready to consume at entry) ===== %>
|
|
109
|
+
<template x-if="props.step === 'minted'">
|
|
110
|
+
<div>
|
|
111
|
+
<%= render "studio/modals/blocks/card_header",
|
|
112
|
+
icon_color: 'primary',
|
|
113
|
+
title_key: "(props.mintedCount === 1 ? 'Entry Token Minted' : 'Entry Tokens Minted')" do %>
|
|
114
|
+
You now have <span class="font-bold text-primary font-mono" x-text="props.mintedBalance"></span> available.
|
|
115
|
+
<% end %>
|
|
116
|
+
<p class="text-xs text-secondary text-center mb-4 leading-snug">
|
|
117
|
+
In the app, holding to confirm now <strong>consumes one token on-chain</strong> to
|
|
118
|
+
create your contest entry, then resolves to the shared Entry Confirmed card.
|
|
119
|
+
</p>
|
|
120
|
+
<button @click="$store.dsModals.close()" class="btn btn-primary btn-lg w-full">Done</button>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
</div>
|
data/lib/studio/engine.rb
CHANGED
data/lib/studio/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -170,7 +170,9 @@ files:
|
|
|
170
170
|
- README.md
|
|
171
171
|
- app/assets/images/resend-favicon.png
|
|
172
172
|
- app/assets/images/ses-favicon.png
|
|
173
|
+
- app/assets/javascripts/studio/canvas_confetti.js
|
|
173
174
|
- app/assets/javascripts/studio/sticky_table_header.js
|
|
175
|
+
- app/assets/javascripts/studio/studio_confetti.js
|
|
174
176
|
- app/assets/stylesheets/studio/sticky_table_header.css
|
|
175
177
|
- app/assets/tailwind/studio_engine/engine-motion.css
|
|
176
178
|
- app/assets/tailwind/studio_engine/engine.css
|
|
@@ -243,8 +245,10 @@ files:
|
|
|
243
245
|
- app/views/schema/index.html.erb
|
|
244
246
|
- app/views/sessions/_sso_continue.html.erb
|
|
245
247
|
- app/views/sessions/new.html.erb
|
|
248
|
+
- app/views/studio/_age_verify_assets.html.erb
|
|
246
249
|
- app/views/studio/_confirm_interstitial.html.erb
|
|
247
250
|
- app/views/studio/_cropper_assets.html.erb
|
|
251
|
+
- app/views/studio/_leveling_activity_assets.html.erb
|
|
248
252
|
- app/views/studio/admin_models/_arenas_table.html.erb
|
|
249
253
|
- app/views/studio/admin_models/_teams_table.html.erb
|
|
250
254
|
- app/views/studio/admin_models/index.html.erb
|
|
@@ -263,12 +267,15 @@ files:
|
|
|
263
267
|
- app/views/studio/modals/_image_upload.html.erb
|
|
264
268
|
- app/views/studio/modals/_saving.html.erb
|
|
265
269
|
- app/views/studio/modals/auth/_resend_footer.html.erb
|
|
270
|
+
- app/views/studio/modals/blocks/_age_verify.html.erb
|
|
266
271
|
- app/views/studio/modals/blocks/_card_header.html.erb
|
|
272
|
+
- app/views/studio/modals/blocks/_change_username.html.erb
|
|
267
273
|
- app/views/studio/modals/blocks/_cta_redirect.html.erb
|
|
268
274
|
- app/views/studio/modals/blocks/_digit_reel.html.erb
|
|
269
275
|
- app/views/studio/modals/blocks/_entry_confirmed.html.erb
|
|
270
276
|
- app/views/studio/modals/blocks/_error_card.html.erb
|
|
271
277
|
- app/views/studio/modals/blocks/_free_entry_earned.html.erb
|
|
278
|
+
- app/views/studio/modals/blocks/_leveling_activity.html.erb
|
|
272
279
|
- app/views/studio/modals/blocks/_onchain_success.html.erb
|
|
273
280
|
- app/views/studio/modals/blocks/_processing_card.html.erb
|
|
274
281
|
- app/views/studio/modals/blocks/_progress_countdown.html.erb
|
|
@@ -293,7 +300,9 @@ files:
|
|
|
293
300
|
- app/views/style/_theme.html.erb
|
|
294
301
|
- app/views/style/_tricks.html.erb
|
|
295
302
|
- app/views/style/index.html.erb
|
|
303
|
+
- app/views/style/modals/_age_verify.html.erb
|
|
296
304
|
- app/views/style/modals/_auth.html.erb
|
|
305
|
+
- app/views/style/modals/_entry_tokens.html.erb
|
|
297
306
|
- app/views/style/modals/_onchain_tx.html.erb
|
|
298
307
|
- app/views/style/modals/_wallet_connect.html.erb
|
|
299
308
|
- app/views/style/modals/_wallet_deposit.html.erb
|