studio-engine 0.19.0 → 0.21.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.
@@ -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,40 @@
1
+ <%#
2
+ Wallet brand-icon sprite — inline brand 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 the OFFICIAL brand asset embedded
19
+ inline as a base64 PNG data-URI <image> — byte-identical to the real logos each
20
+ wallet ships (matching Turf Monster's public/wallet-*.png), so no hand-drawn
21
+ approximation drifts from the brand. The letter tile stays the fallback for any
22
+ wallet 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 — official brand mark (light-purple tile, cream ghost). %>
27
+ <symbol id="se-wallet-phantom" viewBox="0 0 460 460">
28
+ <image width="460" height="460" href="data:image/png;base64,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"/>
29
+ </symbol>
30
+
31
+ <%# Solflare — official brand mark (yellow tile, black blackletter S). %>
32
+ <symbol id="se-wallet-solflare" viewBox="0 0 361 361">
33
+ <image width="361" height="361" href="data:image/png;base64,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"/>
34
+ </symbol>
35
+
36
+ <%# Backpack — official brand mark (red tile, white pack). %>
37
+ <symbol id="se-wallet-backpack" viewBox="0 0 128 128">
38
+ <image width="128" height="128" href="data:image/png;base64,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"/>
39
+ </symbol>
40
+ </svg>
@@ -14,11 +14,16 @@
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), Profile (crop / upload, opened
18
- through studio/cropper_assets), Web3 (wallet-connect / on-chain-tx / deposit,
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
21
+ through studio/cropper_assets), Web3 (wallet-connect with engine-owned inline
22
+ brand icons / on-chain-tx / deposit / the generic entry-confirmed celebration,
19
23
  gated by Studio.feature?(:web3) — disabled-but-present-yet-openable when off),
20
24
  System and status (the reusable card blocks), Templates (the copy-from
21
- archetypes), and Rewards (level-up, gated by :leveling).
25
+ archetypes), and Rewards (level-up + the entry-confirmed seeds bar + the
26
+ Free Entry Earned reward, gated by :leveling).
22
27
 
23
28
  The modal motion (modal-card-* / modal-backdrop-* keyframes + studio-modal-
24
29
  drain + body.modal-open) ships in engine-motion.css, so a consumer that
@@ -26,6 +31,7 @@
26
31
  <%
27
32
  web3_on = Studio.feature?(:web3)
28
33
  leveling_on = Studio.feature?(:leveling)
34
+ age_gate_on = Studio.feature?(:age_gate)
29
35
 
30
36
  # Auth method-toggle defaults — mirror the app's Studio.auth_method? config;
31
37
  # Solana Wallet also needs the web3 capability, so it defaults OFF on an app
@@ -49,8 +55,17 @@
49
55
  expr
50
56
  end
51
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. %>
52
66
  <section id="modals" class="space-y-10" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)"
53
- 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' })">
54
69
  <div class="space-y-1">
55
70
  <h2 class="text-2xl font-bold text-heading">Modals</h2>
56
71
  <p class="text-muted text-sm">
@@ -235,6 +250,11 @@
235
250
  defines the factory inline; no importmap change). %>
236
251
  <%= render "studio/cropper_assets" %>
237
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
+
238
258
  <%# ---- The overlay: one backdrop + card; every specimen's real content is a
239
259
  single-root <template x-if> gated on the current modal id. Faithful to the
240
260
  shared host — backdrop fade, scroll lock, escape + click-self dismissal
@@ -255,6 +275,14 @@
255
275
  <div><%= render "style/modals/auth" %></div>
256
276
  </template>
257
277
 
278
+ <%# --- Eligibility + entry --- %>
279
+ <template x-if="$store.dsModals.current().id === 'age-verify'">
280
+ <div><%= render "style/modals/age_verify" %></div>
281
+ </template>
282
+ <template x-if="$store.dsModals.current().id === 'entry-tokens'">
283
+ <div><%= render "style/modals/entry_tokens" %></div>
284
+ </template>
285
+
258
286
  <%# --- Profile (real engine partials, opened on dsModals) --- %>
259
287
  <template x-if="$store.dsModals.current().id === 'crop-photo'">
260
288
  <div><%= render "studio/modals/crop_photo", store: "dsModals" %></div>
@@ -273,6 +301,15 @@
273
301
  <template x-if="$store.dsModals.current().id === 'wallet-deposit'">
274
302
  <div><%= render "style/modals/wallet_deposit" %></div>
275
303
  </template>
304
+ <%# entry-confirmed — the engine celebration composed via _success_card's
305
+ yield. One registration serves the generic (web3) + seeds (leveling)
306
+ specimens; the seeds bar self-gates on Studio.feature?(:leveling). %>
307
+ <template x-if="$store.dsModals.current().id === 'entry-confirmed'">
308
+ <div><%= render "studio/modals/blocks/entry_confirmed",
309
+ modal_store: "dsModals",
310
+ cta_label: "Contest Lobby ⚔️",
311
+ free_entry_label: "Free Entry Earned 🎟️" %></div>
312
+ </template>
276
313
 
277
314
  <%# --- System and status (reusable card blocks) --- %>
278
315
  <template x-if="$store.dsModals.current().id === 'ds-processing'">
@@ -332,6 +369,12 @@
332
369
  </div>
333
370
  <% end %></div>
334
371
  </template>
372
+ <%# free-entry-earned — the standalone level-up reward modal (:leveling). %>
373
+ <template x-if="$store.dsModals.current().id === 'free-entry-earned'">
374
+ <div><%= render "studio/modals/blocks/free_entry_earned",
375
+ modal_store: "dsModals",
376
+ subtitle: "You leveled up! A Free Entry lands in your account soon. Keep leveling for more." %></div>
377
+ </template>
335
378
 
336
379
  </div>
337
380
  </div>
@@ -415,6 +458,68 @@
415
458
  </div>
416
459
  </section>
417
460
 
461
+ <%# ===================================================================== %>
462
+ <%# 1b. ELIGIBILITY + ENTRY %>
463
+ <%# ===================================================================== %>
464
+ <section class="space-y-5">
465
+ <div class="space-y-1">
466
+ <div class="flex flex-wrap items-center gap-3">
467
+ <h3 class="text-xl font-bold text-heading">Eligibility &amp; entry</h3>
468
+ <% if age_gate_on %>
469
+ <span class="badge" style="color: var(--color-success); border-color: var(--color-success)">age gate on</span>
470
+ <% else %>
471
+ <span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
472
+ <% end %>
473
+ </div>
474
+ <p class="text-muted text-sm">
475
+ The pre-entry gate and the entry-purchase flow. The <strong>age gate</strong>
476
+ (<code class="font-mono text-2xs">studio/modals/blocks/_age_verify</code>) is a
477
+ NEW engine primitive gated by
478
+ <code class="font-mono text-2xs">Studio.feature?(:age_gate)</code> &mdash; a DOB
479
+ modal whose minimum age, jurisdiction label, endpoint, and legal copy are all
480
+ <strong>app-supplied</strong> (the engine hardcodes no policy). It is the heavier
481
+ sibling of the <code class="font-mono text-2xs">shared/_age_attestation</code>
482
+ checkbox inside Sign in above &mdash; <strong>run one or the other</strong>. The
483
+ <strong>entry-tokens</strong> flow is <strong>app-specific</strong> (payments
484
+ product): documented here composing engine chrome, but the packs, rails, and
485
+ the on-chain mint stay app-owned.
486
+ </p>
487
+ </div>
488
+ <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
489
+ <%# Age gate — engine primitive, gated on :age_gate (disabled-but-present). %>
490
+ <%= render layout: "style/modal_specimen", locals: {
491
+ label: "Age gate (DOB)",
492
+ 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')),
493
+ open_expr: "$store.dsModals.open('age-verify')",
494
+ glow_when: ds_glow.call("age-verify"),
495
+ disabled: !age_gate_on, openable: true, disabled_label: "age gate off" } do %>
496
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
497
+ <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
498
+ <div class="grid grid-cols-3 gap-1">
499
+ <span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
500
+ <span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
501
+ <span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
502
+ </div>
503
+ <span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
504
+ </div>
505
+ <% end %>
506
+
507
+ <%# Entry tokens — app-specific flow, documented (not capability-gated). %>
508
+ <%= render layout: "style/modal_specimen", locals: {
509
+ label: "Entry tokens",
510
+ 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' })),
511
+ open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
512
+ glow_when: ds_glow.call("entry-tokens") } do %>
513
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
514
+ <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
515
+ <span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
516
+ <span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
517
+ <span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
518
+ </div>
519
+ <% end %>
520
+ </div>
521
+ </section>
522
+
418
523
  <%# ===================================================================== %>
419
524
  <%# 2. PROFILE — crop / upload, live via cropper_assets %>
420
525
  <%# ===================================================================== %>
@@ -494,6 +599,10 @@
494
599
  <code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
495
600
  <strong><%= web3_on ? "on" : "off" %></strong> here, so these render
496
601
  <%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
602
+ The Connect-wallet picker ships its <strong>Phantom / Solflare / Backpack
603
+ brand marks inline from the engine</strong>
604
+ (<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
605
+ so every app inherits them with no per-app icon files.
497
606
  </p>
498
607
  </div>
499
608
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -555,6 +664,25 @@
555
664
  <span class="block h-5 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
556
665
  </div>
557
666
  <% end %>
667
+
668
+ <%# Entry confirmed — the GENERIC web3 celebration (tx link + heading + drain
669
+ CTA). seedsEarned: 0 so no seeds bar even where leveling is on: this card
670
+ demos the clean success a web3-only app gets. The seeds variant lives in
671
+ Rewards below (gated by :leveling). %>
672
+ <%= render layout: "style/modal_specimen", locals: {
673
+ label: "Entry confirmed",
674
+ 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 })),
675
+ open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 0, seedsTotal: 40 })",
676
+ disabled: !web3_on, openable: true } do %>
677
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
678
+ <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)">
679
+ <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>
680
+ </div>
681
+ <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
682
+ <span class="block h-1.5 w-14 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
683
+ <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
684
+ </div>
685
+ <% end %>
558
686
  </div>
559
687
  </section>
560
688
 
@@ -668,9 +796,16 @@
668
796
  <% end %>
669
797
  </div>
670
798
  <p class="text-muted text-sm">
671
- The level-up celebration, gated by <code class="font-mono text-2xs">Studio.feature?(:leveling)</code>.
672
- Leveling is <strong><%= leveling_on ? "on" : "off" %></strong> here, so this renders
799
+ The seeds bar + level-up celebrations, gated by
800
+ <code class="font-mono text-2xs">Studio.feature?(:leveling)</code> an
801
+ app-level setting <strong>independent of :web3</strong> (some apps have
802
+ Solana but no leveling). Leveling is <strong><%= leveling_on ? "on" : "off" %></strong>
803
+ here, so these render
673
804
  <%= leveling_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
805
+ The seed counts, level, and free entry are <strong>app-supplied props</strong>
806
+ (<code class="font-mono text-2xs">seedsEarned</code>,
807
+ <code class="font-mono text-2xs">seedsTotal</code>, …) — the engine renders
808
+ the bar, the app feeds the numbers.
674
809
  </p>
675
810
  </div>
676
811
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -685,6 +820,43 @@
685
820
  <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
686
821
  </div>
687
822
  <% end %>
823
+
824
+ <%# Entry + seeds — the SAME _entry_confirmed card, now with seedsEarned
825
+ crossing a level so the :leveling seeds bar mounts in the _success_card
826
+ yield and plays the 4-phase level-up (fill → pop → drain → refill), then
827
+ swaps to the Free Entry Earned reward line. %>
828
+ <%= render layout: "style/modal_specimen", locals: {
829
+ label: "Entry + seeds level-up",
830
+ 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 })),
831
+ open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 30, seedsTotal: 100 })",
832
+ disabled: !leveling_on, openable: true } do %>
833
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
834
+ <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)">
835
+ <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>
836
+ </div>
837
+ <div class="flex gap-1">
838
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-cta)"></span>
839
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-cta); opacity:.6"></span>
840
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-cta); opacity:.25"></span>
841
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-text); opacity:.12"></span>
842
+ <span class="flex-1 h-2.5 rounded" style="background: var(--color-text); opacity:.12"></span>
843
+ </div>
844
+ <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
845
+ </div>
846
+ <% end %>
847
+
848
+ <%# Free Entry Earned — the standalone level-up reward modal (:leveling). %>
849
+ <%= render layout: "style/modal_specimen", locals: {
850
+ label: "Free Entry Earned",
851
+ 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 })),
852
+ open_expr: "$store.dsModals.open('free-entry-earned', { level: 3 })",
853
+ disabled: !leveling_on, openable: true } do %>
854
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
855
+ <div class="text-2xl leading-none">🎉</div>
856
+ <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
857
+ <span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
858
+ </div>
859
+ <% end %>
688
860
  </div>
689
861
  </section>
690
862
  </section>
@@ -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">&times;</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">&middot;</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>