studio-engine 0.26.1 → 0.28.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 +49 -0
- data/app/views/studio/_board_assets.html.erb +54 -1
- data/app/views/studio/board/_board.html.erb +28 -2
- data/app/views/studio/board/_column.html.erb +37 -3
- data/app/views/studio/modals/_host.html.erb +8 -18
- data/app/views/studio/modals/_load_convention.html.erb +55 -0
- data/app/views/studio/modals/_saving.html.erb +10 -3
- data/app/views/studio/modals/blocks/_processing_card.html.erb +20 -6
- data/app/views/style/_modals.html.erb +201 -88
- data/app/views/style/_tasks.html.erb +67 -2
- data/app/views/style/modals/_entry_tokens.html.erb +97 -32
- data/app/views/style/modals/_onchain_tx.html.erb +24 -0
- data/app/views/style/modals/_wallet_connect.html.erb +12 -1
- data/lib/studio/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79aac649d7403deadeaa9f055762802f111b7082865398432753dfdd93b998e0
|
|
4
|
+
data.tar.gz: 80307a1b7077886c9853ad5a6ab7f2dc79a9d7bf6d4db7aa883f5eb6162a77bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fb8f96918656e88d2d8cc3379716d544afd461bdaa19205c480e273ab71199c25cede8c7b729f9b6cf3400388a7976ac34f4135d96fd60b3e8e4083969f360e
|
|
7
|
+
data.tar.gz: 2f329ab8c0c240cf56e11e2e9d7bc1c3c6711e24dcb52ee82a2de2785c1697cf333e3bcc17f056db22400e4e606d6c30108565502bd8a6119e2d7b50e18a1a64
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
|
|
4
4
|
|
|
5
|
+
## 0.27.0 — 2026-07-28
|
|
6
|
+
|
|
7
|
+
**Two coordinated modal flows on the `/admin/style` Design System page — a walked
|
|
8
|
+
"Web3 Contest" wallet → on-chain flow and an extended "Contest entry & eligibility"
|
|
9
|
+
flow — plus a reusable minimum-visible-duration convention for load modals.** All
|
|
10
|
+
specimens are DEMO chrome (`demo: true`, no real signing); the engine gains only
|
|
11
|
+
view/chrome primitives and keeps custodial keys, wallet/multisig signing,
|
|
12
|
+
`Solana::Config`, and every on-chain write app-side.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Minimum-visible-duration convention** — `studio/modals/_load_convention`
|
|
17
|
+
ships `window.StudioModals.holdAtLeast(minMs)` + a standard
|
|
18
|
+
`MIN_LOAD_MS` (1400) as a SINGLE definition, rendered by both the shared modal
|
|
19
|
+
host (`studio/modals/_host`, which drops its inline copy) and the style page.
|
|
20
|
+
A load spinner holds for at least `min_duration`, longer if the real async runs
|
|
21
|
+
longer — `resolveAt = max(min_duration, actual_completion)`; in demo mode
|
|
22
|
+
`min_duration` doubles as the auto-resolve timer. `_processing_card` gains
|
|
23
|
+
`min_duration` + `resolve_expr` locals so a demo load card self-advances.
|
|
24
|
+
- **Web3 Contest** section (renamed from "Web3") — a walked flow with
|
|
25
|
+
glow-follows-the-flow continuity: Connect Wallet → Processing on-chain
|
|
26
|
+
transaction → On-chain success. The Processing card is a load modal with a demo
|
|
27
|
+
success/error toggle (unchecked resolves to success, checked to error), both
|
|
28
|
+
resolved states built. Picking a wallet swaps to Processing, which auto-resolves
|
|
29
|
+
after the min-load duration.
|
|
30
|
+
- **Contest entry & eligibility** section (renamed from "Eligibility & entry",
|
|
31
|
+
moved directly under Web3 Contest) — the entry flow extended to
|
|
32
|
+
Entry tokens → Payment processing → Entry Tokens Minted → Contest enter
|
|
33
|
+
processing → Contest entered, with the glow following the step machine. Keeps
|
|
34
|
+
the Age gate + Entry tokens cards. The section states the honest web2/web3 map:
|
|
35
|
+
the token mint is web2-only (web3 funds USDC on-chain directly, no token); the
|
|
36
|
+
paths diverge at the funding front-end and the entry-submit endpoint, then
|
|
37
|
+
converge on the same on-chain Entry PDA and the same "Entry Confirmed" card.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **`studio/modals/_host`** — the `holdAtLeast` helper now lives in the shared
|
|
42
|
+
`_load_convention` partial (same API, byte-identical behavior) so the host and
|
|
43
|
+
the DS page share one definition instead of drifting copies.
|
|
44
|
+
- **`style/modals/_entry_tokens`** — the confirming step uses the min-load
|
|
45
|
+
convention instead of a hardcoded timeout, and the step machine gains
|
|
46
|
+
`entering` + `entered` so hold-to-confirm walks through the on-chain consume to
|
|
47
|
+
the shared Entry Confirmed finish.
|
|
48
|
+
- **`style/modals/_onchain_tx`** — a processing modal opened with `demoResolve`
|
|
49
|
+
auto-resolves to success (or error when `demoError`) after the min-load
|
|
50
|
+
duration, honoring the convention.
|
|
51
|
+
- The Modals-section glow helper discriminates the on-chain-tx `state` and a
|
|
52
|
+
configurable step default, so the glow tracks a walked flow across modal ids.
|
|
53
|
+
|
|
5
54
|
## 0.26.1 — 2026-07-28
|
|
6
55
|
|
|
7
56
|
**Fix the `/admin/style` Design System page breaking when reached via Turbo Drive
|
|
@@ -48,9 +48,16 @@
|
|
|
48
48
|
onMoveHook: opts.onMoveHook || null,
|
|
49
49
|
onDropHook: opts.onDropHook || null,
|
|
50
50
|
labels: opts.labels || {},
|
|
51
|
+
// gap 3: the column key a data-exit-action="archive" remove re-parents INTO
|
|
52
|
+
// (its #dropzone-<key>) rather than deleting the card. null ⇒ 0.27.0 removal.
|
|
53
|
+
archiveZone: opts.archiveZone || null,
|
|
51
54
|
|
|
52
55
|
// --- state ---------------------------------------------------------
|
|
53
56
|
toasts: [],
|
|
57
|
+
// gap 1: board-level chrome state lives HERE, on the studioBoard scope, so a
|
|
58
|
+
// board's header_slot controls + column show_expr bind to it directly — no
|
|
59
|
+
// bespoke outer Alpine wrapper. Seeded from board_state; {} when omitted.
|
|
60
|
+
state: (opts.state && typeof opts.state === "object") ? opts.state : {},
|
|
54
61
|
|
|
55
62
|
// --- init ----------------------------------------------------------
|
|
56
63
|
init: function () {
|
|
@@ -75,6 +82,19 @@
|
|
|
75
82
|
zoneKey: function (el) { return el ? el.getAttribute("data-" + this.zoneAttr) : null; },
|
|
76
83
|
label: function (zone) { return this.labels[zone] || zone; },
|
|
77
84
|
|
|
85
|
+
// --- board-chrome state helpers (gap 1) ----------------------------
|
|
86
|
+
// Generic, domain-free operations over `state` so a board's header_slot /
|
|
87
|
+
// column show_expr drive filters + toggles without a bespoke wrapper.
|
|
88
|
+
toggleState: function (key) { this.state[key] = !this.state[key]; },
|
|
89
|
+
listHas: function (key, val) {
|
|
90
|
+
return Array.isArray(this.state[key]) && this.state[key].indexOf(val) !== -1;
|
|
91
|
+
},
|
|
92
|
+
toggleInState: function (key, val) {
|
|
93
|
+
if (!Array.isArray(this.state[key])) this.state[key] = [];
|
|
94
|
+
var i = this.state[key].indexOf(val);
|
|
95
|
+
if (i === -1) { this.state[key].push(val); } else { this.state[key].splice(i, 1); }
|
|
96
|
+
},
|
|
97
|
+
|
|
78
98
|
// --- SortableJS wiring ---------------------------------------------
|
|
79
99
|
initSortables: function () {
|
|
80
100
|
if (typeof Sortable === "undefined") return;
|
|
@@ -271,6 +291,21 @@
|
|
|
271
291
|
var card = document.getElementById(target);
|
|
272
292
|
if (!card) { renderNow(el); return; }
|
|
273
293
|
self.animateCardExit(card, exitAction).then(function () {
|
|
294
|
+
// gap 3: an `archive` exit RE-PARENTS the card into the archive
|
|
295
|
+
// column (#dropzone-<archiveZone>) instead of vanishing, when the
|
|
296
|
+
// board named one and it exists. Everything else falls through to
|
|
297
|
+
// Turbo's remove (the 0.27.0 behavior).
|
|
298
|
+
if (exitAction === "archive" && self.archiveZone) {
|
|
299
|
+
var zone = document.getElementById("dropzone-" + self.archiveZone);
|
|
300
|
+
if (zone) {
|
|
301
|
+
var anchor = self.emptySelector ? zone.querySelector(self.emptySelector) : null;
|
|
302
|
+
zone.insertBefore(card, anchor);
|
|
303
|
+
self.setCardZone(card, self.archiveZone);
|
|
304
|
+
self.resetCardExit(card);
|
|
305
|
+
self.updateCounts();
|
|
306
|
+
return; // keep the card in the archive column — do NOT remove it
|
|
307
|
+
}
|
|
308
|
+
}
|
|
274
309
|
renderNow(el);
|
|
275
310
|
self.updateCounts();
|
|
276
311
|
});
|
|
@@ -279,7 +314,13 @@
|
|
|
279
314
|
},
|
|
280
315
|
|
|
281
316
|
animateCardExit: function (card, kind) {
|
|
282
|
-
if (!card
|
|
317
|
+
if (!card) return Promise.resolve();
|
|
318
|
+
// gap 4: stamp an observable marker for WHICH exit is running (archive vs
|
|
319
|
+
// delete). Set before the reduced-motion guard so the marker is present
|
|
320
|
+
// even when the animation itself is skipped — the re-parent branch and any
|
|
321
|
+
// e2e both key off it.
|
|
322
|
+
card.dataset.exitAction = kind;
|
|
323
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return Promise.resolve();
|
|
283
324
|
card.style.pointerEvents = "none";
|
|
284
325
|
var frames = kind === "archive"
|
|
285
326
|
? [{ opacity: 1, transform: "translateY(0) scale(1)" }, { opacity: 0, transform: "translateY(18px) scale(0.86)" }]
|
|
@@ -292,6 +333,18 @@
|
|
|
292
333
|
return anim.finished.catch(function () {});
|
|
293
334
|
},
|
|
294
335
|
|
|
336
|
+
// Clear an exit animation + its marker so a re-parented card is fully
|
|
337
|
+
// visible again in the archive column (used by the gap-3 re-parent path).
|
|
338
|
+
resetCardExit: function (card) {
|
|
339
|
+
if (!card) return;
|
|
340
|
+
card.getAnimations({ subtree: false }).forEach(function (a) { a.cancel(); });
|
|
341
|
+
delete card.dataset.exitAction;
|
|
342
|
+
card.style.pointerEvents = "";
|
|
343
|
+
card.style.opacity = "";
|
|
344
|
+
card.style.transform = "";
|
|
345
|
+
card.style.filter = "";
|
|
346
|
+
},
|
|
347
|
+
|
|
295
348
|
// --- toasts --------------------------------------------------------
|
|
296
349
|
toast: function (message, type) {
|
|
297
350
|
if (!this.toastsEnabled) {
|
|
@@ -19,10 +19,27 @@
|
|
|
19
19
|
studio/board/_column (the `.kanban-dropzone`) and studio/board/_card_shell (the
|
|
20
20
|
`.kanban-card`) — an app may hand-roll a bespoke card as long as it matches.
|
|
21
21
|
|
|
22
|
+
CHROME COMPOSITION (the recipe that replaces a bespoke outer Alpine wrapper) —
|
|
23
|
+
a board with filters / toggles no longer needs to wrap this primitive in its own
|
|
24
|
+
x-data component. Everything the chrome needs lives ON the studioBoard scope:
|
|
25
|
+
1. `board_state:` seeds a reactive bag on the factory (`state`). Pass the
|
|
26
|
+
chrome's flags/lists, e.g. { showArchived: false, hiddenApps: [] }.
|
|
27
|
+
2. `header_slot:` (and `above_board_slot:`) render INSIDE the studioBoard
|
|
28
|
+
section, so their controls bind straight to that state and the generic
|
|
29
|
+
helpers the factory exposes: `toggleState('showArchived')`,
|
|
30
|
+
`toggleInState('hiddenApps', repo)`, `listHas('hiddenApps', repo)`.
|
|
31
|
+
3. A toggle-able column reads the same state via its `show_expr:`
|
|
32
|
+
(e.g. show_expr: "state.showArchived"); cards x-show off `listHas(...)`.
|
|
33
|
+
4. An Archived column that a stage→archived broadcast should re-parent INTO
|
|
34
|
+
(not delete) is named by the board's `archive_zone:`.
|
|
35
|
+
|
|
22
36
|
locals:
|
|
23
37
|
columns: (req) [{ key:, label:, cards:, count:, badge_class:,
|
|
24
|
-
collapsible:, hidden:,
|
|
25
|
-
|
|
38
|
+
count_class:, collapsible:, hidden:, show_expr:, min_width:,
|
|
39
|
+
kickoff:, header_extra: }]. `cards` are records passed to
|
|
40
|
+
`card_partial`; `count` defaults to cards.size. See
|
|
41
|
+
studio/board/_column for the additive per-column hooks
|
|
42
|
+
(count_class / show_expr / min_width / kickoff).
|
|
26
43
|
card_partial: (req) partial rendered per card (e.g. "tasks/task_card").
|
|
27
44
|
card_as: local the record binds to in card_partial. Default :card.
|
|
28
45
|
card_locals: per-card extra locals — a Proc ->(record){ Hash } or a Hash.
|
|
@@ -50,6 +67,11 @@
|
|
|
50
67
|
empty_label: empty-zone text. Default "Drop here".
|
|
51
68
|
header_slot: raw HTML rendered above the columns (pass html_safe content).
|
|
52
69
|
above_board_slot: raw HTML rendered between the header and the columns.
|
|
70
|
+
board_state: Hash seeding the factory's reactive `state` bag (board-level
|
|
71
|
+
chrome: filter/toggle flags + lists). nil ⇒ empty {}.
|
|
72
|
+
archive_zone: column key a `remove` exit-stream marked data-exit-action=
|
|
73
|
+
"archive" RE-PARENTS the card into (its #dropzone-<key>),
|
|
74
|
+
instead of deleting it. nil ⇒ the card is removed (0.27.0).
|
|
53
75
|
fx: window[<name>] module the factory defers add/exit anims to.
|
|
54
76
|
on_move_hook: window[<name>] opaque fn, called with { record, from, to }.
|
|
55
77
|
on_drop_hook: window[<name>] opaque fn, called with { ids, zone }.
|
|
@@ -79,6 +101,8 @@
|
|
|
79
101
|
empty_label = local_assigns.fetch(:empty_label, "Drop here")
|
|
80
102
|
header_slot = local_assigns.fetch(:header_slot, nil)
|
|
81
103
|
above_board_slot = local_assigns.fetch(:above_board_slot, nil)
|
|
104
|
+
board_state = local_assigns.fetch(:board_state, nil) # gap 1: chrome-state home
|
|
105
|
+
archive_zone = local_assigns.fetch(:archive_zone, nil) # gap 3: exit re-parent target
|
|
82
106
|
fx = local_assigns.fetch(:fx, nil)
|
|
83
107
|
on_move_hook = local_assigns.fetch(:on_move_hook, nil)
|
|
84
108
|
on_drop_hook = local_assigns.fetch(:on_drop_hook, nil)
|
|
@@ -102,6 +126,8 @@
|
|
|
102
126
|
toasts: !!toasts,
|
|
103
127
|
live: live_channel.present?,
|
|
104
128
|
demo: !!demo,
|
|
129
|
+
state: (board_state.is_a?(Hash) ? board_state : nil),
|
|
130
|
+
archiveZone: (archive_zone ? archive_zone.to_s : nil),
|
|
105
131
|
fx: fx,
|
|
106
132
|
onMoveHook: on_move_hook,
|
|
107
133
|
onDropHook: on_drop_hook,
|
|
@@ -12,10 +12,25 @@
|
|
|
12
12
|
|
|
13
13
|
locals:
|
|
14
14
|
column: (req) { key:, label:, cards:, count:, badge_class:,
|
|
15
|
-
collapsible:, hidden:,
|
|
15
|
+
count_class:, collapsible:, hidden:, show_expr:,
|
|
16
|
+
min_width:, kickoff:, header_extra: }. `cards` are the
|
|
16
17
|
records passed to the card partial; `count` defaults to
|
|
17
18
|
cards.size; `badge_class` picks a components/_badge scheme
|
|
18
19
|
for the count (else a plain neutral count chip).
|
|
20
|
+
Additive board-chrome hooks (all opt-in, all default to
|
|
21
|
+
the 0.27.0 rendering when omitted):
|
|
22
|
+
count_class: raw class string colouring the neutral
|
|
23
|
+
count chip (e.g. a task_stage_count_classes value).
|
|
24
|
+
Ignored when badge_class is set. Default neutral chip.
|
|
25
|
+
show_expr: Alpine x-show expression for a toggle-able
|
|
26
|
+
column (binds against the board's studioBoard scope,
|
|
27
|
+
e.g. "state.showArchived"). Adds x-cloak so it can't
|
|
28
|
+
flash before Alpine boots. Independent of `hidden:`.
|
|
29
|
+
min_width: the column's sm min-width utility class.
|
|
30
|
+
Default "sm:min-w-[190px]".
|
|
31
|
+
kickoff: raw HTML rendered in a fixed-height slot
|
|
32
|
+
under the header row (a per-column kickoff chip);
|
|
33
|
+
nil renders nothing.
|
|
19
34
|
zone_attr: data-attr name for the zone. Default "stage".
|
|
20
35
|
dom_id_prefix: card dom-id prefix, forwarded to the cards. Default "card-".
|
|
21
36
|
zone_selector_class: the drop-zone class. Default "kanban-dropzone".
|
|
@@ -41,9 +56,20 @@
|
|
|
41
56
|
cards = column[:cards] || []
|
|
42
57
|
count = column.fetch(:count) { cards.size }
|
|
43
58
|
badge_class = column[:badge_class]
|
|
59
|
+
# gap 2: an opt-in raw class colouring the neutral count chip; nil keeps the
|
|
60
|
+
# exact 0.27.0 neutral chip classes.
|
|
61
|
+
count_class = column[:count_class]
|
|
44
62
|
collapsible = column[:collapsible]
|
|
45
63
|
hidden = column[:hidden]
|
|
64
|
+
# gap 5: a bindable visibility expr (x-show), a per-board min-width, and a
|
|
65
|
+
# per-column kickoff slot — each nil/absent renders identically to 0.27.0.
|
|
66
|
+
show_expr = column[:show_expr]
|
|
67
|
+
min_width = column[:min_width] || "sm:min-w-[190px]"
|
|
68
|
+
kickoff = column[:kickoff]
|
|
46
69
|
header_extra = column[:header_extra]
|
|
70
|
+
# The neutral count chip's colour classes: an opt-in raw class, else the 0.27.0
|
|
71
|
+
# neutral default. The layout classes are shared; only the colour band varies.
|
|
72
|
+
count_color = count_class || "bg-surface-alt text-secondary border border-subtle"
|
|
47
73
|
|
|
48
74
|
# Merge the per-card hook (Proc or Hash) with the record binding.
|
|
49
75
|
resolve_locals = lambda do |record|
|
|
@@ -51,21 +77,29 @@
|
|
|
51
77
|
{ card_as => record }.merge(extra || {})
|
|
52
78
|
end
|
|
53
79
|
%>
|
|
54
|
-
<div class="studio-board-column w-full flex-none sm:flex-1
|
|
80
|
+
<div class="studio-board-column w-full flex-none sm:flex-1 <%= min_width %>"
|
|
55
81
|
data-board-column="<%= key %>"
|
|
56
82
|
<% if collapsible %>data-board-collapsible="true"<% end %>
|
|
83
|
+
<% if show_expr %>x-show="<%= show_expr %>" x-cloak<% end %>
|
|
57
84
|
<% if hidden %>x-cloak style="display:none"<% end %>>
|
|
58
85
|
<div class="mb-3 px-1 flex items-center gap-2">
|
|
59
86
|
<h3 class="text-sm font-bold text-heading uppercase tracking-wider"><%= label %></h3>
|
|
60
87
|
<% if badge_class %>
|
|
61
88
|
<%= render "components/badge", text: count.to_s, scheme: badge_class, data_board_count: key %>
|
|
62
89
|
<% else %>
|
|
63
|
-
<span class="stage-count inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full text-xs font-bold
|
|
90
|
+
<span class="stage-count inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full text-xs font-bold <%= count_color %>"
|
|
64
91
|
data-board-count="<%= key %>"><%= count %></span>
|
|
65
92
|
<% end %>
|
|
66
93
|
<% if header_extra %><%= header_extra %><% end %>
|
|
67
94
|
</div>
|
|
68
95
|
|
|
96
|
+
<%# gap 5: per-column kickoff slot — a fixed-height row under the header so every
|
|
97
|
+
column's dropzone starts at the same y (no height jog) when only some carry a
|
|
98
|
+
chip. Renders nothing when the column passes no kickoff. %>
|
|
99
|
+
<% if kickoff %>
|
|
100
|
+
<div class="studio-board-kickoff mt-2 min-h-[26px]"><%= kickoff %></div>
|
|
101
|
+
<% end %>
|
|
102
|
+
|
|
69
103
|
<div id="dropzone-<%= key %>"
|
|
70
104
|
data-<%= zone_attr %>="<%= key %>"
|
|
71
105
|
class="<%= zone_selector_class %> rounded-xl border-2 border-dashed border-subtle min-h-40 p-2 space-y-2 transition-colors duration-150 sm:min-h-[calc(100vh-220px)]">
|
|
@@ -153,24 +153,6 @@
|
|
|
153
153
|
|
|
154
154
|
<script>
|
|
155
155
|
(function() {
|
|
156
|
-
// === StudioModals helpers ===========================================
|
|
157
|
-
//
|
|
158
|
-
// holdAtLeast(ms) — pair with the _processing_card block. Stamp the
|
|
159
|
-
// moment a loading view becomes visible, then await the minimum
|
|
160
|
-
// before swapping to success so the spinner doesn't flash past the
|
|
161
|
-
// user when the operation finishes fast.
|
|
162
|
-
window.StudioModals = window.StudioModals || {};
|
|
163
|
-
window.StudioModals.holdAtLeast = function(minMs) {
|
|
164
|
-
var startedAt = Date.now();
|
|
165
|
-
return {
|
|
166
|
-
then: function(callback) {
|
|
167
|
-
var remaining = Math.max(0, minMs - (Date.now() - startedAt));
|
|
168
|
-
if (remaining === 0) { callback(); return; }
|
|
169
|
-
setTimeout(callback, remaining);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
};
|
|
173
|
-
|
|
174
156
|
// === Modal animation registry ======================================
|
|
175
157
|
//
|
|
176
158
|
// The single place to define + tune named entrance / exit animations
|
|
@@ -490,6 +472,14 @@
|
|
|
490
472
|
})();
|
|
491
473
|
</script>
|
|
492
474
|
|
|
475
|
+
<%# The minimum-visible-duration convention (window.StudioModals.holdAtLeast +
|
|
476
|
+
MIN_LOAD_MS) — the SINGLE source, shared with the /admin/style demo page.
|
|
477
|
+
Pair it with the _processing_card block so a load spinner never flashes past
|
|
478
|
+
the user. Rendered AFTER the store <script> above so the store stays the
|
|
479
|
+
host's first <script> (a Node behavior harness extracts that first script).
|
|
480
|
+
See studio/modals/_load_convention for the full contract. %>
|
|
481
|
+
<%= render "studio/modals/load_convention" %>
|
|
482
|
+
|
|
493
483
|
<template x-if="$store.modals.current()">
|
|
494
484
|
<%# Backdrop + card use CSS keyframe animations (NOT Alpine
|
|
495
485
|
x-transition), all shipped in the inline style block above. On
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Minimum-visible-duration convention for load / processing modals.
|
|
3
|
+
|
|
4
|
+
A load modal shows its spinner for AT LEAST a minimum duration so it never
|
|
5
|
+
flashes past the user (registering "did anything happen?" instead of
|
|
6
|
+
"processing → done"), but LONGER whenever the real async takes longer:
|
|
7
|
+
|
|
8
|
+
resolveAt = max(min_duration, actual_completion)
|
|
9
|
+
|
|
10
|
+
This ships the convention on window.StudioModals, defined ONCE here and
|
|
11
|
+
rendered by BOTH the shared modal host (studio/modals/_host) and the living
|
|
12
|
+
style guide (style/_modals) — so every consumer app and the /admin/style demo
|
|
13
|
+
page share a single definition:
|
|
14
|
+
|
|
15
|
+
MIN_LOAD_MS — the standard default minimum (1400 ms). A caller may pass its
|
|
16
|
+
own, but reach for this so every load modal feels consistent.
|
|
17
|
+
|
|
18
|
+
holdAtLeast(minMs) — returns a thenable whose .then(cb) fires cb at
|
|
19
|
+
max(minMs, elapsed-since-this-call). Two usages:
|
|
20
|
+
|
|
21
|
+
REAL async — stamp the moment the loading view appears, then
|
|
22
|
+
resolve in the async completion, so a slow op is never cut
|
|
23
|
+
short and a fast one still honors the spinner:
|
|
24
|
+
var hold = StudioModals.holdAtLeast(StudioModals.MIN_LOAD_MS);
|
|
25
|
+
doWork().then(function () { hold.then(showSuccess); });
|
|
26
|
+
|
|
27
|
+
DEMO (no real work) — call .then immediately and minMs
|
|
28
|
+
DOUBLES as the auto-resolve timer: the demo advances after
|
|
29
|
+
min_duration exactly:
|
|
30
|
+
StudioModals.holdAtLeast(StudioModals.MIN_LOAD_MS).then(advance);
|
|
31
|
+
|
|
32
|
+
Idempotent (|| guards): never clobber a definition an app already shipped, and
|
|
33
|
+
safe to render more than once on a page.
|
|
34
|
+
%>
|
|
35
|
+
<script>
|
|
36
|
+
(function () {
|
|
37
|
+
window.StudioModals = window.StudioModals || {};
|
|
38
|
+
// Standard minimum-visible duration for a load spinner, in ms. ~1200–1500
|
|
39
|
+
// reads as "processing → done"; below ~1s the spinner flashes and reads as a
|
|
40
|
+
// glitch. Callers may override per-modal, but default to this for consistency.
|
|
41
|
+
window.StudioModals.MIN_LOAD_MS = window.StudioModals.MIN_LOAD_MS || 1400;
|
|
42
|
+
// holdAtLeast(minMs).then(cb) — cb fires at max(minMs, elapsed-since-call).
|
|
43
|
+
// Stamp the moment a loading view becomes visible; the anti-flicker floor.
|
|
44
|
+
window.StudioModals.holdAtLeast = window.StudioModals.holdAtLeast || function (minMs) {
|
|
45
|
+
var startedAt = Date.now();
|
|
46
|
+
return {
|
|
47
|
+
then: function (callback) {
|
|
48
|
+
var remaining = Math.max(0, minMs - (Date.now() - startedAt));
|
|
49
|
+
if (remaining === 0) { callback(); return; }
|
|
50
|
+
setTimeout(callback, remaining);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
</script>
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
a { title } prop while a Turbo form uploads. Register it in your modal-host
|
|
3
3
|
block alongside crop-photo. Single root for the host's <template x-if>.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Locals:
|
|
6
|
+
store — Alpine store name backing the title read. Default "modals".
|
|
7
|
+
The living style guide passes store: "dsModals".
|
|
8
|
+
resolve_expr — optional demo auto-resolve (min-load convention). The real
|
|
9
|
+
Turbo submit drives the spinner and closes on completion, so
|
|
10
|
+
apps omit this; the /admin/style demo passes an expression
|
|
11
|
+
(e.g. close) so the specimen self-terminates after min_duration
|
|
12
|
+
instead of spinning forever. %>
|
|
7
13
|
<% saving_store = local_assigns.fetch(:store, "modals") %>
|
|
8
14
|
<div>
|
|
9
15
|
<%= render "studio/modals/blocks/processing_card",
|
|
10
|
-
title_key: "$store.#{saving_store}.current()?.props?.title || 'Saving…'"
|
|
16
|
+
title_key: "$store.#{saving_store}.current()?.props?.title || 'Saving…'",
|
|
17
|
+
resolve_expr: local_assigns[:resolve_expr] %>
|
|
11
18
|
</div>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
<%#
|
|
2
2
|
Reusable processing / loading card. Spinner + title + optional message.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
|
|
4
|
+
Minimum-visible-duration convention (studio/modals/_load_convention):
|
|
5
|
+
a load spinner must show for AT LEAST a floor, but longer if the real async
|
|
6
|
+
runs longer — resolveAt = max(min_duration, actual_completion) — so it never
|
|
7
|
+
flashes past the user ("did anything happen?") yet never cuts a slow op short.
|
|
8
|
+
Two ways to honor it:
|
|
9
|
+
• REAL async — the caller stamps window.StudioModals.holdAtLeast(min_duration)
|
|
10
|
+
when this card appears, then resolves in its completion callback.
|
|
11
|
+
• DEMO / auto-resolve — pass `resolve_expr`: this card self-schedules that
|
|
12
|
+
Alpine expression after `min_duration` (min_duration doubles as the demo's
|
|
13
|
+
auto-resolve timer), via the same holdAtLeast floor.
|
|
8
14
|
|
|
9
15
|
Locals — pass static strings OR `_key` variants for Alpine-driven
|
|
10
16
|
text that updates as state mutates (e.g. live store reads):
|
|
@@ -15,6 +21,10 @@
|
|
|
15
21
|
size: spinner size — 'sm', 'md' (default), 'lg'
|
|
16
22
|
color: color token for the spinner — 'primary' (default),
|
|
17
23
|
'success', 'warning'
|
|
24
|
+
min_duration: minimum visible ms (default 1400, mirrors
|
|
25
|
+
StudioModals.MIN_LOAD_MS). The convention's floor.
|
|
26
|
+
resolve_expr: Alpine expression run after min_duration to advance the flow
|
|
27
|
+
(demo auto-resolve). Omit for a real spinner the caller drives.
|
|
18
28
|
|
|
19
29
|
One of `title` or `title_key` is required.
|
|
20
30
|
%>
|
|
@@ -27,8 +37,12 @@
|
|
|
27
37
|
'lg' => 'w-16 h-16 border-4'
|
|
28
38
|
}
|
|
29
39
|
spinner_class = sizes[size] || sizes['md']
|
|
40
|
+
# min_duration mirrors StudioModals.MIN_LOAD_MS (the JS default) so the Ruby and
|
|
41
|
+
# JS sides of the convention agree on the same floor.
|
|
42
|
+
min_duration = local_assigns[:min_duration] || 1400
|
|
43
|
+
resolve_expr = local_assigns[:resolve_expr]
|
|
30
44
|
%>
|
|
31
|
-
<div class="text-center py-6"
|
|
45
|
+
<div class="text-center py-6"<%= " x-data=\"{}\" x-init=\"window.StudioModals.holdAtLeast(#{min_duration}).then(() => { #{resolve_expr} })\"".html_safe if resolve_expr %>>
|
|
32
46
|
<div class="mx-auto <%= spinner_class %> rounded-full border-<%= color %>/30 border-t-<%= color %> animate-spin mb-5"></div>
|
|
33
47
|
<% if local_assigns[:title_key] %>
|
|
34
48
|
<p class="text-base font-bold text-heading mb-1" x-text="<%= title_key %>"></p>
|
|
@@ -42,18 +42,22 @@
|
|
|
42
42
|
|
|
43
43
|
# Active-card glow-match expression builder: yields an Alpine boolean that is
|
|
44
44
|
# true when $store.dsModals.current() is THIS specimen's modal. It discriminates
|
|
45
|
-
# by sub-state so
|
|
46
|
-
# card
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
45
|
+
# by sub-state so specimens that open the SAME modal id still glow the right
|
|
46
|
+
# card, and so the glow FOLLOWS a walked flow as the step machine advances:
|
|
47
|
+
# step: the auth / entry-tokens step machine (step_default is the
|
|
48
|
+
# modal's own default step — 'credentials' for auth, 'picker'
|
|
49
|
+
# for entry-tokens).
|
|
50
|
+
# state: the on-chain-tx state (processing / success / error) — lets the
|
|
51
|
+
# glow track Connect Wallet -> Processing -> On-chain success.
|
|
52
|
+
# crop: the two crop-photo variants (:picker vs :crop), on props.cropReady.
|
|
53
|
+
# celebrate: the input specimen (form) vs the "updated" (celebrate) specimen
|
|
54
|
+
# of the SAME leveling modal id.
|
|
55
|
+
ds_glow = lambda do |id, step: nil, step_default: "credentials", crop: nil, celebrate: nil, state: nil|
|
|
51
56
|
cur = "$store.dsModals.current()"
|
|
52
57
|
expr = "#{cur} && #{cur}.id === '#{id}'"
|
|
53
|
-
expr += " && (#{cur}.props.step || '
|
|
58
|
+
expr += " && (#{cur}.props.step || '#{step_default}') === '#{step}'" if step
|
|
59
|
+
expr += " && #{cur}.props.state === '#{state}'" if state
|
|
54
60
|
expr += (crop == :crop ? " && !!#{cur}.props.cropReady" : " && !#{cur}.props.cropReady") unless crop.nil?
|
|
55
|
-
# celebrate discriminates the input specimen (form) from the "updated" specimen
|
|
56
|
-
# (celebrate) of the SAME leveling modal id, mirroring the step/crop sub-states.
|
|
57
61
|
expr += (celebrate ? " && !!#{cur}.props.celebrate" : " && !#{cur}.props.celebrate") unless celebrate.nil?
|
|
58
62
|
expr
|
|
59
63
|
end
|
|
@@ -263,6 +267,13 @@
|
|
|
263
267
|
})();
|
|
264
268
|
</script>
|
|
265
269
|
|
|
270
|
+
<%# The minimum-visible-duration convention (window.StudioModals.holdAtLeast +
|
|
271
|
+
MIN_LOAD_MS) — the SAME single definition the shared modal host renders, so
|
|
272
|
+
the demo load modals here (payment/entry processing, on-chain tx, saving)
|
|
273
|
+
honor resolveAt = max(min_duration, actual) and auto-resolve after
|
|
274
|
+
min_duration in demo mode. A consumer app already has it via the host. %>
|
|
275
|
+
<%= render "studio/modals/load_convention" %>
|
|
276
|
+
|
|
266
277
|
<%# Cropper.js + the cropPhotoModal factory — makes the Profile crop / upload
|
|
267
278
|
modals openable on this page (loads cropper.js via <script defer> and
|
|
268
279
|
defines the factory inline; no importmap change). %>
|
|
@@ -310,8 +321,11 @@
|
|
|
310
321
|
<template x-if="$store.dsModals.current().id === 'crop-photo'">
|
|
311
322
|
<div><%= render "studio/modals/crop_photo", store: "dsModals" %></div>
|
|
312
323
|
</template>
|
|
324
|
+
<%# saving — the demo passes resolve_expr so the spinner self-terminates
|
|
325
|
+
after min_duration (a real app's Turbo submit drives + closes it). %>
|
|
313
326
|
<template x-if="$store.dsModals.current().id === 'saving'">
|
|
314
|
-
<div><%= render "studio/modals/saving", store: "dsModals"
|
|
327
|
+
<div><%= render "studio/modals/saving", store: "dsModals",
|
|
328
|
+
resolve_expr: "$store.dsModals.close()" %></div>
|
|
315
329
|
</template>
|
|
316
330
|
|
|
317
331
|
<%# --- Web3 --- %>
|
|
@@ -335,9 +349,12 @@
|
|
|
335
349
|
</template>
|
|
336
350
|
|
|
337
351
|
<%# --- System and status (reusable card blocks) --- %>
|
|
352
|
+
<%# ds-processing demos the min-duration convention on the canonical block:
|
|
353
|
+
resolve_expr auto-swaps to the success card after min_duration. %>
|
|
338
354
|
<template x-if="$store.dsModals.current().id === 'ds-processing'">
|
|
339
355
|
<div><%= render "studio/modals/blocks/processing_card",
|
|
340
|
-
title: "Confirming entry", message: "Talking to the server…"
|
|
356
|
+
title: "Confirming entry", message: "Talking to the server…",
|
|
357
|
+
resolve_expr: "$store.dsModals.swap('ds-success')" %></div>
|
|
341
358
|
</template>
|
|
342
359
|
<template x-if="$store.dsModals.current().id === 'ds-success'">
|
|
343
360
|
<div><%= render "studio/modals/blocks/success_card",
|
|
@@ -523,68 +540,6 @@
|
|
|
523
540
|
</div>
|
|
524
541
|
</section>
|
|
525
542
|
|
|
526
|
-
<%# ===================================================================== %>
|
|
527
|
-
<%# 1b. ELIGIBILITY + ENTRY %>
|
|
528
|
-
<%# ===================================================================== %>
|
|
529
|
-
<section class="space-y-5">
|
|
530
|
-
<div class="space-y-1">
|
|
531
|
-
<div class="flex flex-wrap items-center gap-3">
|
|
532
|
-
<h3 class="text-xl font-bold text-heading">Eligibility & entry</h3>
|
|
533
|
-
<% if age_gate_on %>
|
|
534
|
-
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">age gate on</span>
|
|
535
|
-
<% else %>
|
|
536
|
-
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
|
|
537
|
-
<% end %>
|
|
538
|
-
</div>
|
|
539
|
-
<p class="text-muted text-sm">
|
|
540
|
-
The pre-entry gate and the entry-purchase flow. The <strong>age gate</strong>
|
|
541
|
-
(<code class="font-mono text-2xs">studio/modals/blocks/_age_verify</code>) is a
|
|
542
|
-
NEW engine primitive gated by
|
|
543
|
-
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code> — a DOB
|
|
544
|
-
modal whose minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
545
|
-
<strong>app-supplied</strong> (the engine hardcodes no policy). It is the heavier
|
|
546
|
-
sibling of the <code class="font-mono text-2xs">shared/_age_attestation</code>
|
|
547
|
-
checkbox inside Sign in above — <strong>run one or the other</strong>. The
|
|
548
|
-
<strong>entry-tokens</strong> flow is <strong>app-specific</strong> (payments
|
|
549
|
-
product): documented here composing engine chrome, but the packs, rails, and
|
|
550
|
-
the on-chain mint stay app-owned.
|
|
551
|
-
</p>
|
|
552
|
-
</div>
|
|
553
|
-
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
554
|
-
<%# Age gate — engine primitive, gated on :age_gate (disabled-but-present). %>
|
|
555
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
556
|
-
label: "Age gate (DOB)",
|
|
557
|
-
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')),
|
|
558
|
-
open_expr: "$store.dsModals.open('age-verify')",
|
|
559
|
-
glow_when: ds_glow.call("age-verify"),
|
|
560
|
-
disabled: !age_gate_on, openable: true, disabled_label: "age gate off" } do %>
|
|
561
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
562
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
563
|
-
<div class="grid grid-cols-3 gap-1">
|
|
564
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
565
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
566
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
567
|
-
</div>
|
|
568
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
569
|
-
</div>
|
|
570
|
-
<% end %>
|
|
571
|
-
|
|
572
|
-
<%# Entry tokens — app-specific flow, documented (not capability-gated). %>
|
|
573
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
574
|
-
label: "Entry tokens",
|
|
575
|
-
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' })),
|
|
576
|
-
open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
|
|
577
|
-
glow_when: ds_glow.call("entry-tokens") } do %>
|
|
578
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
579
|
-
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
580
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
581
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
|
|
582
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
583
|
-
</div>
|
|
584
|
-
<% end %>
|
|
585
|
-
</div>
|
|
586
|
-
</section>
|
|
587
|
-
|
|
588
543
|
<%# ===================================================================== %>
|
|
589
544
|
<%# 2. PROFILE — crop / upload, live via cropper_assets %>
|
|
590
545
|
<%# ===================================================================== %>
|
|
@@ -766,12 +721,13 @@
|
|
|
766
721
|
</section>
|
|
767
722
|
|
|
768
723
|
<%# ===================================================================== %>
|
|
769
|
-
<%#
|
|
724
|
+
<%# 2. WEB3 CONTEST — the walked wallet -> on-chain flow (glow follows it). %>
|
|
725
|
+
<%# Gated by :web3; off = disabled-but-present-yet-openable. %>
|
|
770
726
|
<%# ===================================================================== %>
|
|
771
727
|
<section class="space-y-5">
|
|
772
728
|
<div class="space-y-1">
|
|
773
729
|
<div class="flex flex-wrap items-center gap-3">
|
|
774
|
-
<h3 class="text-xl font-bold text-heading">Web3</h3>
|
|
730
|
+
<h3 class="text-xl font-bold text-heading">Web3 Contest</h3>
|
|
775
731
|
<% if web3_on %>
|
|
776
732
|
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">enabled on this app</span>
|
|
777
733
|
<% else %>
|
|
@@ -779,21 +735,31 @@
|
|
|
779
735
|
<% end %>
|
|
780
736
|
</div>
|
|
781
737
|
<p class="text-muted text-sm">
|
|
782
|
-
|
|
738
|
+
The wallet + on-chain modals, ported into the engine and gated by
|
|
783
739
|
<code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
|
|
784
740
|
<strong><%= web3_on ? "on" : "off" %></strong> here, so these render
|
|
785
741
|
<%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
|
|
786
|
-
The
|
|
787
|
-
|
|
742
|
+
The first three cards are a <strong>walked flow</strong> —
|
|
743
|
+
<strong>Connect Wallet → Processing on-chain transaction → On-chain
|
|
744
|
+
success</strong> — and the live card <strong>glows</strong>, following the
|
|
745
|
+
flow as it advances (open Connect Wallet and pick a wallet to watch it walk).
|
|
746
|
+
The <strong>Processing</strong> card is a load modal: its
|
|
747
|
+
<strong>success/error toggle</strong> chooses whether it resolves to success
|
|
748
|
+
or the error state, after the <strong>minimum-visible duration</strong>
|
|
749
|
+
(<code class="font-mono text-2xs">studio/modals/_load_convention</code>). The
|
|
750
|
+
Connect-wallet picker ships its <strong>Phantom / Solflare / Backpack brand
|
|
751
|
+
marks inline from the engine</strong>
|
|
788
752
|
(<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
|
|
789
753
|
so every app inherits them with no per-app icon files.
|
|
790
754
|
</p>
|
|
791
755
|
</div>
|
|
792
756
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
757
|
+
<%# 1. Connect wallet — the walk's entry; picking a wallet swaps to Processing. %>
|
|
793
758
|
<%= render layout: "style/modal_specimen", locals: {
|
|
794
759
|
label: "Connect wallet",
|
|
795
|
-
reference: %(the Web3 "Connect wallet" picker (studio-engine style/modals/_wallet_connect) —
|
|
760
|
+
reference: %(the Web3 Contest "Connect wallet" picker (studio-engine style/modals/_wallet_connect) — the walk's entry: picking a wallet runs a brief connecting state then swaps to the on-chain Processing modal. Open with $store.dsModals.open('wallet-connect')),
|
|
796
761
|
open_expr: "$store.dsModals.open('wallet-connect')",
|
|
762
|
+
glow_when: ds_glow.call("wallet-connect"),
|
|
797
763
|
disabled: !web3_on, openable: true } do %>
|
|
798
764
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
799
765
|
<span class="block h-2 w-20 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
@@ -802,10 +768,14 @@
|
|
|
802
768
|
</div>
|
|
803
769
|
<% end %>
|
|
804
770
|
|
|
771
|
+
<%# 2. Processing — the load modal; the success/error toggle picks the resolution. %>
|
|
805
772
|
<%= render layout: "style/modal_specimen", locals: {
|
|
806
|
-
label: "
|
|
807
|
-
reference: %(the Web3 "
|
|
808
|
-
|
|
773
|
+
label: "Processing on-chain tx",
|
|
774
|
+
reference: %(the Web3 Contest "Processing" on-chain load modal (style/modals/_onchain_tx via $store.dsSolanaModal) — holds the spinner at least MIN_LOAD_MS, then auto-resolves to success (or the error state when the toggle is set). Open with $store.dsModals.open('onchain-tx', { state: 'processing', demoResolve: true, demoError: false })),
|
|
775
|
+
card_data: "opts: { demoError: false }",
|
|
776
|
+
toggles: [{ model: "opts.demoError", label: "Resolve to error" }],
|
|
777
|
+
open_expr: "$store.dsModals.open('onchain-tx', { state: 'processing', title: 'Confirming on-chain', message: 'Waiting for the wallet signature…', demoResolve: true, demoError: opts.demoError })",
|
|
778
|
+
glow_when: ds_glow.call("onchain-tx", state: "processing"),
|
|
809
779
|
disabled: !web3_on, openable: true } do %>
|
|
810
780
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
811
781
|
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
@@ -813,10 +783,12 @@
|
|
|
813
783
|
</div>
|
|
814
784
|
<% end %>
|
|
815
785
|
|
|
786
|
+
<%# 3. On-chain success — the resolved success end-state (the walk's finish). %>
|
|
816
787
|
<%= render layout: "style/modal_specimen", locals: {
|
|
817
|
-
label: "On-chain
|
|
818
|
-
reference: %(the Web3 "On-chain
|
|
788
|
+
label: "On-chain success",
|
|
789
|
+
reference: %(the Web3 Contest "On-chain success" state (style/modals/_onchain_tx + studio/modals/blocks/_onchain_success) — the resolved success card the walk lands on. Open with $store.dsModals.open('onchain-tx', { state: 'success', txSignature: '…', successTitle: 'Mint complete' })),
|
|
819
790
|
open_expr: "$store.dsModals.open('onchain-tx', { state: 'success', txSignature: '5xTrDemoSignature1234567890', successTitle: 'Mint complete', successSubtitle: 'Your on-chain action confirmed.', ctaLabel: 'View wallet', ctaHref: null })",
|
|
791
|
+
glow_when: ds_glow.call("onchain-tx", state: "success"),
|
|
820
792
|
disabled: !web3_on, openable: true } do %>
|
|
821
793
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
822
794
|
<div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-success) 15%, transparent)">
|
|
@@ -826,10 +798,12 @@
|
|
|
826
798
|
</div>
|
|
827
799
|
<% end %>
|
|
828
800
|
|
|
801
|
+
<%# On-chain error — the resolved error end-state (the toggle's other branch). %>
|
|
829
802
|
<%= render layout: "style/modal_specimen", locals: {
|
|
830
|
-
label: "On-chain
|
|
831
|
-
reference: %(the Web3 "On-chain
|
|
803
|
+
label: "On-chain error",
|
|
804
|
+
reference: %(the Web3 Contest "On-chain error" state (style/modals/_onchain_tx) — the resolved error card the Processing toggle resolves to. Open with $store.dsModals.open('onchain-tx', { state: 'error', title: "Couldn't confirm", errorMessage: 'The transaction was rejected. Give it another try.' })),
|
|
832
805
|
open_expr: "$store.dsModals.open('onchain-tx', { state: 'error', title: 'Could not confirm', errorMessage: 'The transaction was rejected. Give it another try.' })",
|
|
806
|
+
glow_when: ds_glow.call("onchain-tx", state: "error"),
|
|
833
807
|
disabled: !web3_on, openable: true } do %>
|
|
834
808
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
835
809
|
<div class="text-2xl leading-none">⚠️</div>
|
|
@@ -837,9 +811,10 @@
|
|
|
837
811
|
</div>
|
|
838
812
|
<% end %>
|
|
839
813
|
|
|
814
|
+
<%# Wallet deposit — standalone top-up card (not part of the walk). %>
|
|
840
815
|
<%= render layout: "style/modal_specimen", locals: {
|
|
841
816
|
label: "Wallet deposit",
|
|
842
|
-
reference: %(the Web3 "Wallet deposit" top-up card (style/modals/_wallet_deposit) — open with $store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5…' })),
|
|
817
|
+
reference: %(the Web3 Contest "Wallet deposit" top-up card (style/modals/_wallet_deposit) — open with $store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5…' })),
|
|
843
818
|
open_expr: "$store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5demoWALLETaddr9xQ2' })",
|
|
844
819
|
disabled: !web3_on, openable: true } do %>
|
|
845
820
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -855,7 +830,7 @@
|
|
|
855
830
|
Rewards below (gated by :leveling). %>
|
|
856
831
|
<%= render layout: "style/modal_specimen", locals: {
|
|
857
832
|
label: "Entry confirmed",
|
|
858
|
-
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 })),
|
|
833
|
+
reference: %(the Web3 Contest "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 })),
|
|
859
834
|
open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 0, seedsTotal: 40 })",
|
|
860
835
|
disabled: !web3_on, openable: true } do %>
|
|
861
836
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -870,6 +845,144 @@
|
|
|
870
845
|
</div>
|
|
871
846
|
</section>
|
|
872
847
|
|
|
848
|
+
<%# ===================================================================== %>
|
|
849
|
+
<%# 3. CONTEST ENTRY & ELIGIBILITY — moved directly under Web3 Contest. The %>
|
|
850
|
+
<%# walked entry flow (glow follows), plus the honest web2/web3 map. %>
|
|
851
|
+
<%# ===================================================================== %>
|
|
852
|
+
<section class="space-y-5">
|
|
853
|
+
<div class="space-y-1">
|
|
854
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
855
|
+
<h3 class="text-xl font-bold text-heading">Contest entry & eligibility</h3>
|
|
856
|
+
<% if age_gate_on %>
|
|
857
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">age gate on</span>
|
|
858
|
+
<% else %>
|
|
859
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
|
|
860
|
+
<% end %>
|
|
861
|
+
</div>
|
|
862
|
+
<p class="text-muted text-sm">
|
|
863
|
+
The pre-entry gate and the full entry flow. The <strong>age gate</strong>
|
|
864
|
+
(<code class="font-mono text-2xs">studio/modals/blocks/_age_verify</code>) is an
|
|
865
|
+
engine primitive gated by
|
|
866
|
+
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code> — a DOB
|
|
867
|
+
modal whose minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
868
|
+
<strong>app-supplied</strong> (the engine hardcodes no policy). The entry flow is
|
|
869
|
+
a <strong>walked sequence</strong> — <strong>Entry tokens → Payment
|
|
870
|
+
processing → Entry Tokens Minted → Contest enter processing →
|
|
871
|
+
Contest entered</strong> — and the live card <strong>glows</strong>,
|
|
872
|
+
following the step machine (the two load steps honor the same min-duration
|
|
873
|
+
convention as Web3 Contest above).
|
|
874
|
+
</p>
|
|
875
|
+
<%# The web2/web3 finding — a real divergence, stated honestly, not invented. %>
|
|
876
|
+
<div class="mt-2 rounded-lg border border-subtle bg-surface p-3 text-xs text-secondary leading-snug">
|
|
877
|
+
<p class="text-heading font-semibold mb-1">Web2 vs web3 — the honest map</p>
|
|
878
|
+
<p class="mb-1">
|
|
879
|
+
The two funding paths <strong>diverge at the front-end and the entry-submit
|
|
880
|
+
endpoint, then converge</strong>. <strong>Web2</strong> (card / Stripe /
|
|
881
|
+
Coinflow / PayPal) mints one prepaid entry token per credit on-chain after
|
|
882
|
+
payment clears, then <strong>consumes</strong> one token to enter.
|
|
883
|
+
<strong>Web3</strong> (wallet / USDC on-chain) funds USDC directly and
|
|
884
|
+
<strong>mints no token</strong> — the mint → consume prelude
|
|
885
|
+
is web2-only.
|
|
886
|
+
</p>
|
|
887
|
+
<p>
|
|
888
|
+
Both create the <strong>same on-chain Entry PDA</strong> and resolve to the
|
|
889
|
+
<strong>same “Entry Confirmed” card</strong>. The walk below demos the web2
|
|
890
|
+
path end-to-end; web3 skips the mint and joins at
|
|
891
|
+
<strong>enter → entered</strong>.
|
|
892
|
+
</p>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
896
|
+
<%# Age gate — engine primitive, gated on :age_gate (disabled-but-present). %>
|
|
897
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
898
|
+
label: "Age gate (DOB)",
|
|
899
|
+
reference: %(the "Age gate" DOB modal (studio-engine studio/modals/blocks/_age_verify) — an 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')),
|
|
900
|
+
open_expr: "$store.dsModals.open('age-verify')",
|
|
901
|
+
glow_when: ds_glow.call("age-verify"),
|
|
902
|
+
disabled: !age_gate_on, openable: true, disabled_label: "age gate off" } do %>
|
|
903
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
904
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
905
|
+
<div class="grid grid-cols-3 gap-1">
|
|
906
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
907
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
908
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
909
|
+
</div>
|
|
910
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
911
|
+
</div>
|
|
912
|
+
<% end %>
|
|
913
|
+
|
|
914
|
+
<%# 1. Entry tokens — the picker (walk's entry). Picking a pack advances. %>
|
|
915
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
916
|
+
label: "Entry tokens",
|
|
917
|
+
reference: %(the "Entry tokens" picker (app-specific — documented composing studio/modals/blocks chrome). The packs, pricing, rails, and on-chain mint stay app-owned. Picking a pack walks Payment processing -> Minted -> enter -> entered. Open with $store.dsModals.open('entry-tokens', { step: 'picker' })),
|
|
918
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
|
|
919
|
+
glow_when: ds_glow.call("entry-tokens", step: "picker", step_default: "picker") } do %>
|
|
920
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
921
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
922
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
923
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
|
|
924
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
925
|
+
</div>
|
|
926
|
+
<% end %>
|
|
927
|
+
|
|
928
|
+
<%# 2. Payment processing — the mint-at-purchase load step (web2). %>
|
|
929
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
930
|
+
label: "Payment processing",
|
|
931
|
+
reference: %(the "Payment processing" step (entry-tokens step 'confirming') — "Confirming your purchase…" over "Payment cleared — a server job mints one prepaid entry credit per token on-chain." A load modal; honors the min-duration convention then auto-advances to Entry Tokens Minted. Open with $store.dsModals.open('entry-tokens', { step: 'confirming', mintedCount: 3, mintedBalance: '3' })),
|
|
932
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'confirming', mintedCount: 3, mintedBalance: '3' })",
|
|
933
|
+
glow_when: ds_glow.call("entry-tokens", step: "confirming", step_default: "picker") } do %>
|
|
934
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
935
|
+
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
936
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
937
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
938
|
+
</div>
|
|
939
|
+
<% end %>
|
|
940
|
+
|
|
941
|
+
<%# 3. Entry Tokens Minted — ready to consume at entry (hold to confirm). %>
|
|
942
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
943
|
+
label: "Entry Tokens Minted",
|
|
944
|
+
reference: %(the "Entry Tokens Minted" step (entry-tokens step 'minted') — "You now have N available." Holding to confirm consumes one token on-chain to create the contest entry (advances to Contest enter processing). Open with $store.dsModals.open('entry-tokens', { step: 'minted', mintedCount: 3, mintedBalance: '3' })),
|
|
945
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'minted', mintedCount: 3, mintedBalance: '3' })",
|
|
946
|
+
glow_when: ds_glow.call("entry-tokens", step: "minted", step_default: "picker") } do %>
|
|
947
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
948
|
+
<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)">
|
|
949
|
+
<span class="text-sm font-bold" style="color: var(--color-primary)">3</span>
|
|
950
|
+
</div>
|
|
951
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
952
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
953
|
+
</div>
|
|
954
|
+
<% end %>
|
|
955
|
+
|
|
956
|
+
<%# 4. Contest enter processing — the on-chain consume load step. %>
|
|
957
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
958
|
+
label: "Contest enter processing",
|
|
959
|
+
reference: %(the "Contest enter processing" step (entry-tokens step 'entering') — "Confirming your entry…" over "Consuming one token on-chain to create your contest entry." A load modal; honors the min-duration convention then auto-advances to Contest entered. Open with $store.dsModals.open('entry-tokens', { step: 'entering' })),
|
|
960
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'entering' })",
|
|
961
|
+
glow_when: ds_glow.call("entry-tokens", step: "entering", step_default: "picker") } do %>
|
|
962
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
963
|
+
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
964
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
965
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
966
|
+
</div>
|
|
967
|
+
<% end %>
|
|
968
|
+
|
|
969
|
+
<%# 5. Contest entered — the shared Entry Confirmed finish (web2 + web3 converge). %>
|
|
970
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
971
|
+
label: "Contest entered",
|
|
972
|
+
reference: %(the "Contest entered" success (entry-tokens step 'entered') — "Good Luck / Entry Confirmed" with the branded on-chain tx link and a Contest Lobby CTA. The SAME finish both web2 and web3 entries converge on. Open with $store.dsModals.open('entry-tokens', { step: 'entered', txSignature: '…' })),
|
|
973
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'entered', txSignature: '5xTrDemoEntrySignature1234567890abc' })",
|
|
974
|
+
glow_when: ds_glow.call("entry-tokens", step: "entered", step_default: "picker") } do %>
|
|
975
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
976
|
+
<div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-success) 15%, transparent)">
|
|
977
|
+
<svg class="w-4 h-4" style="color: var(--color-success)" 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>
|
|
978
|
+
</div>
|
|
979
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
980
|
+
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
981
|
+
</div>
|
|
982
|
+
<% end %>
|
|
983
|
+
</div>
|
|
984
|
+
</section>
|
|
985
|
+
|
|
873
986
|
<%# ===================================================================== %>
|
|
874
987
|
<%# 5. SYSTEM AND STATUS (reusable card blocks) %>
|
|
875
988
|
<%# ===================================================================== %>
|
|
@@ -109,6 +109,69 @@
|
|
|
109
109
|
</div>
|
|
110
110
|
</div>
|
|
111
111
|
|
|
112
|
+
<%# ---- The ENRICHED board chrome (0.28.0) ------------------------------ %>
|
|
113
|
+
<%# One specimen that exercises the five additive hooks the /tasks pilot surfaced,
|
|
114
|
+
all opt-in: (1) board_state + header_slot chrome home, (2) a coloured count
|
|
115
|
+
chip via count_class, (3) archive_zone exit re-parent, (4) the exit marker,
|
|
116
|
+
(5) a toggle-able column (show_expr) + a per-column kickoff slot + min_width.
|
|
117
|
+
No outer Alpine wrapper: the Show/Hide-archived toggle in header_slot and the
|
|
118
|
+
archived column's show_expr both bind to the SAME studioBoard state. %>
|
|
119
|
+
<% chrome_header = capture do %>
|
|
120
|
+
<div class="flex items-center gap-3 flex-wrap">
|
|
121
|
+
<button type="button"
|
|
122
|
+
class="px-3 py-1.5 rounded-lg text-sm font-medium transition border border-subtle"
|
|
123
|
+
:class="state.showArchived ? 'bg-primary text-white border-primary' : 'bg-surface text-secondary hover:text-heading'"
|
|
124
|
+
@click="toggleState('showArchived')"
|
|
125
|
+
data-test="chrome-archived-toggle">
|
|
126
|
+
<span x-text="state.showArchived ? 'Hide archived' : 'Show archived'">Show archived</span>
|
|
127
|
+
</button>
|
|
128
|
+
<span class="label-upper"><code class="font-mono text-2xs">header_slot</code> chrome · binds to <code class="font-mono text-2xs">state.showArchived</code> — no outer wrapper</span>
|
|
129
|
+
</div>
|
|
130
|
+
<% end %>
|
|
131
|
+
<% chrome_kickoff = capture do %>
|
|
132
|
+
<span class="inline-flex items-center gap-1 rounded border border-primary/30 bg-primary/10 px-1.5 py-0.5 text-2xs font-semibold uppercase tracking-wide text-primary">kickoff slot</span>
|
|
133
|
+
<% end %>
|
|
134
|
+
<%
|
|
135
|
+
# Per-column count_class colours (raw classes, the task_stage_count_classes shape),
|
|
136
|
+
# a kickoff chip + widened min_width on Designed, and a toggle-able Archived column
|
|
137
|
+
# (show_expr) the board re-parents into (archive_zone).
|
|
138
|
+
chrome_columns = [
|
|
139
|
+
{ key: "designed", label: "Designed",
|
|
140
|
+
count_class: "bg-blue-500/10 text-blue-500 border border-blue-500/30",
|
|
141
|
+
kickoff: chrome_kickoff, min_width: "sm:min-w-[15rem]",
|
|
142
|
+
cards: [{ id: "chrome-count-color", zone: "designed", title: "Coloured count chip", repo: "studio-engine", who: "SH" }] },
|
|
143
|
+
{ key: "building", label: "Building",
|
|
144
|
+
count_class: "bg-yellow-500/10 text-yellow-400 border border-yellow-500/30",
|
|
145
|
+
cards: [{ id: "chrome-bindable-col", zone: "building", title: "Bindable columns", repo: "turf-monster", who: "CA" }] },
|
|
146
|
+
{ key: "shipped", label: "Shipped",
|
|
147
|
+
count_class: "bg-emerald-500/10 text-emerald-500 border border-emerald-500/30",
|
|
148
|
+
cards: [{ id: "chrome-exit-marker", zone: "shipped", title: "Exit re-parent marker", repo: "mcritchie", who: "ST" }] },
|
|
149
|
+
{ key: "archived", label: "Archived",
|
|
150
|
+
count_class: "bg-gray-500/10 text-gray-400 border border-gray-500/30",
|
|
151
|
+
show_expr: "state.showArchived",
|
|
152
|
+
cards: [{ id: "chrome-archived-card", zone: "archived", title: "Archived (re-parent target)", repo: "mcritchie", who: "ST" }] }
|
|
153
|
+
]
|
|
154
|
+
%>
|
|
155
|
+
<div class="space-y-3">
|
|
156
|
+
<div class="flex items-center gap-2">
|
|
157
|
+
<span class="badge" style="color: var(--color-primary); border-color: var(--color-primary)">Enriched</span>
|
|
158
|
+
<p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> · 0.28.0 chrome — coloured counts, toggle-able column, archive re-parent</p>
|
|
159
|
+
</div>
|
|
160
|
+
<%= render "studio/board/board",
|
|
161
|
+
demo: true,
|
|
162
|
+
columns: chrome_columns,
|
|
163
|
+
card_partial: "style/board/demo_card",
|
|
164
|
+
card_as: :card,
|
|
165
|
+
reorder_url: "#",
|
|
166
|
+
move_url: "#",
|
|
167
|
+
move_param: { resource: "task", attr: "stage" },
|
|
168
|
+
group: "studio-board-chrome",
|
|
169
|
+
board_state: { showArchived: false },
|
|
170
|
+
archive_zone: "archived",
|
|
171
|
+
header_slot: chrome_header,
|
|
172
|
+
empty_label: "Drop tasks here" %>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
112
175
|
<%# ---- The static reference sketch ------------------------------------- %>
|
|
113
176
|
<div class="fade-edge-x overflow-x-auto pb-2">
|
|
114
177
|
<div class="flex gap-4 min-w-max">
|
|
@@ -162,9 +225,11 @@
|
|
|
162
225
|
</div>
|
|
163
226
|
<ul class="text-secondary text-sm space-y-1 list-disc list-inside">
|
|
164
227
|
<li>Column layout + horizontal scroll with faded edges</li>
|
|
165
|
-
<li>The <code class="font-mono text-2xs">stage-*</code> badge palette + count chips</li>
|
|
228
|
+
<li>The <code class="font-mono text-2xs">stage-*</code> badge palette + count chips (colour via <code class="font-mono text-2xs">count_class</code>)</li>
|
|
166
229
|
<li>Card chrome (<code class="font-mono text-2xs">.card</code>) and the drag / rank handle</li>
|
|
167
230
|
<li>Empty-column drop state</li>
|
|
231
|
+
<li>Chrome-state home (<code class="font-mono text-2xs">board_state</code> + <code class="font-mono text-2xs">header_slot</code>) — filters/toggles without an outer wrapper</li>
|
|
232
|
+
<li>Toggle-able columns (<code class="font-mono text-2xs">show_expr</code>), a kickoff slot, and archive re-parent (<code class="font-mono text-2xs">archive_zone</code>)</li>
|
|
168
233
|
</ul>
|
|
169
234
|
</div>
|
|
170
235
|
<div class="card p-5 space-y-2">
|
|
@@ -175,7 +240,7 @@
|
|
|
175
240
|
<ul class="text-secondary text-sm space-y-1 list-disc list-inside">
|
|
176
241
|
<li>Which stages become columns</li>
|
|
177
242
|
<li>The card body — fields, chips, avatars, live timers</li>
|
|
178
|
-
<li>
|
|
243
|
+
<li>Filter/toggle logic + header actions, composed onto the shared chrome state</li>
|
|
179
244
|
<li>Live-update wiring (Turbo Streams on the deploy board)</li>
|
|
180
245
|
</ul>
|
|
181
246
|
</div>
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
<%#
|
|
2
|
-
Living style guide specimen — the entry
|
|
3
|
-
APP-SPECIFIC flow (Turf Monster's "Get Entry Tokens");
|
|
4
|
-
does NOT own the packs, pricing,
|
|
5
|
-
engine CHROME (studio/modals/blocks/
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
Living style guide specimen — the contest entry & eligibility flow. This
|
|
3
|
+
documents an APP-SPECIFIC flow (Turf Monster's "Get Entry Tokens" → enter);
|
|
4
|
+
the engine deliberately does NOT own the packs, pricing, payment rails, or any
|
|
5
|
+
on-chain write. The specimen composes only engine CHROME (studio/modals/blocks/
|
|
6
|
+
shell, card_header, progress_pill, solana_tx_link) and represents the packs and
|
|
7
|
+
the on-chain moments as an illustrative demo — the app owns the money + signing.
|
|
8
8
|
|
|
9
9
|
Opened on the page-scoped dsModals host with an internal step machine on
|
|
10
|
-
props.step
|
|
10
|
+
props.step, the full web2 walk:
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
picker -> confirming -> minted -> entering -> entered
|
|
13
|
+
|
|
14
|
+
Single-root templates throughout. The two load steps (confirming, entering)
|
|
15
|
+
honor the minimum-visible-duration convention (studio/modals/_load_convention):
|
|
16
|
+
the spinner holds at least MIN_LOAD_MS, then auto-advances (demo mode) —
|
|
17
|
+
resolveAt = max(min_duration, actual_completion).
|
|
18
|
+
|
|
19
|
+
On-chain model (documented, not owned here). An entry token is a PROGRAM-OWNED
|
|
20
|
+
prepaid entry credit recorded on-chain (a per-token account), NOT a fungible SPL
|
|
21
|
+
token in the user's wallet. WEB2 (card / Stripe / Coinflow / PayPal) has two
|
|
22
|
+
on-chain moments: (1) MINT at purchase, server-signed after the payment clears —
|
|
23
|
+
not on the buyer's click; (2) CONSUME at entry, when the hold-to-confirm gesture
|
|
24
|
+
spends one token to create the contest entry. WEB3 (wallet / USDC on-chain)
|
|
25
|
+
funds USDC directly and mints NO token — so the mint→consume prelude is
|
|
26
|
+
web2-only. Both paths converge at the SAME on-chain Entry PDA and the SAME
|
|
27
|
+
"Entry Confirmed" card.
|
|
18
28
|
%>
|
|
19
29
|
<%
|
|
20
30
|
# Illustrative packs only — a real app reads these from its own config
|
|
@@ -29,10 +39,20 @@
|
|
|
29
39
|
get props() { var c = Alpine.store('dsModals').current(); return (c && c.props) || {}; },
|
|
30
40
|
buy(count, balance) {
|
|
31
41
|
Alpine.store('dsModals').advance({ step: 'confirming', mintedCount: count, mintedBalance: balance });
|
|
32
|
-
|
|
42
|
+
},
|
|
43
|
+
confirmEntry() {
|
|
44
|
+
Alpine.store('dsModals').advance({ step: 'entering' });
|
|
45
|
+
},
|
|
46
|
+
// Demo auto-resolve honoring the min-load convention: hold the spinner at
|
|
47
|
+
// least MIN_LOAD_MS, then advance — but only while this modal is still on
|
|
48
|
+
// the given step (guards against an early close / manual advance).
|
|
49
|
+
autoAdvance(fromStep, patch) {
|
|
50
|
+
window.StudioModals.holdAtLeast(window.StudioModals.MIN_LOAD_MS).then(function () {
|
|
33
51
|
var c = Alpine.store('dsModals').current();
|
|
34
|
-
if (c && c.id === 'entry-tokens'
|
|
35
|
-
|
|
52
|
+
if (c && c.id === 'entry-tokens' && c.props.step === fromStep) {
|
|
53
|
+
Alpine.store('dsModals').advance(patch);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
36
56
|
}
|
|
37
57
|
}" class="relative">
|
|
38
58
|
|
|
@@ -68,36 +88,52 @@
|
|
|
68
88
|
<% end %>
|
|
69
89
|
</div>
|
|
70
90
|
|
|
71
|
-
<%# Rails —
|
|
72
|
-
The USDC-direct path is intentionally NOT a pack here (it's a separate
|
|
73
|
-
rail that transfers USDC and mints no token). %>
|
|
91
|
+
<%# Rails — which funding path mints tokens vs funds an entry directly. %>
|
|
74
92
|
<div class="mt-4 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted">
|
|
75
93
|
<span>Pay by card</span>
|
|
76
94
|
<span aria-hidden="true">·</span>
|
|
77
95
|
<span>PayPal / Venmo</span>
|
|
96
|
+
<span aria-hidden="true">·</span>
|
|
97
|
+
<span>Coinflow</span>
|
|
78
98
|
</div>
|
|
79
|
-
<p class="text-center text-2xs text-muted mt-1">
|
|
99
|
+
<p class="text-center text-2xs text-muted mt-1">
|
|
100
|
+
These are the <strong>web2</strong> rails — they mint prepaid entry tokens.
|
|
101
|
+
The <strong>web3</strong> path funds USDC on-chain directly and mints no token.
|
|
102
|
+
</p>
|
|
80
103
|
|
|
81
|
-
<%# The seam callout — what the engine owns vs
|
|
104
|
+
<%# The seam callout — what the engine owns vs the app, plus the honest
|
|
105
|
+
web2/web3 map (a real divergence, not invented). %>
|
|
82
106
|
<div class="mt-4 rounded-lg border border-subtle bg-surface p-3 text-2xs text-secondary leading-snug">
|
|
83
107
|
<p class="text-heading font-semibold mb-1">App-specific flow, engine chrome</p>
|
|
108
|
+
<p class="mb-2">
|
|
109
|
+
The engine supplies only the shell, header, progress pill, and tx link. The
|
|
110
|
+
packs, pricing, payment rails, and every on-chain write are
|
|
111
|
+
<strong>app-owned</strong> — this specimen represents them as a demo. On-chain,
|
|
112
|
+
an entry token is a <strong>prepaid entry credit recorded on-chain</strong>
|
|
113
|
+
(a per-token account), not a wallet-held token.
|
|
114
|
+
</p>
|
|
115
|
+
<p class="text-heading font-semibold mb-1">Web2 vs web3 — where they diverge and converge</p>
|
|
84
116
|
<p class="mb-1">
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
117
|
+
<strong>Web2</strong> (card / Stripe / Coinflow / PayPal): a server job mints
|
|
118
|
+
one prepaid entry credit per token on-chain once payment clears, then
|
|
119
|
+
hold-to-confirm <strong>consumes</strong> one token to create the entry.
|
|
120
|
+
</p>
|
|
121
|
+
<p class="mb-1">
|
|
122
|
+
<strong>Web3</strong> (wallet / USDC on-chain): funds USDC directly and
|
|
123
|
+
<strong>mints no token</strong> — the mint → consume prelude is web2-only.
|
|
88
124
|
</p>
|
|
89
125
|
<p>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
Both paths converge on the <strong>same on-chain Entry PDA</strong> and the
|
|
127
|
+
<strong>same “Entry Confirmed” card</strong> — divergent funding front-end,
|
|
128
|
+
one shared entry → entered finish.
|
|
93
129
|
</p>
|
|
94
130
|
</div>
|
|
95
131
|
</div>
|
|
96
132
|
</template>
|
|
97
133
|
|
|
98
|
-
<%# ===== CONFIRMING (mint-at-purchase) ===== %>
|
|
134
|
+
<%# ===== CONFIRMING (payment processing — mint-at-purchase) ===== %>
|
|
99
135
|
<template x-if="props.step === 'confirming'">
|
|
100
|
-
<div x-init="
|
|
136
|
+
<div x-init="autoAdvance('confirming', { step: 'minted' })">
|
|
101
137
|
<%= render "studio/modals/blocks/card_header",
|
|
102
138
|
spinner: true,
|
|
103
139
|
title: "Confirming your purchase…",
|
|
@@ -114,10 +150,39 @@
|
|
|
114
150
|
You now have <span class="font-bold text-primary font-mono" x-text="props.mintedBalance"></span> available.
|
|
115
151
|
<% end %>
|
|
116
152
|
<p class="text-xs text-secondary text-center mb-4 leading-snug">
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
Holding to confirm now <strong>consumes one token on-chain</strong> to create
|
|
154
|
+
your contest entry.
|
|
119
155
|
</p>
|
|
120
|
-
<button @click="
|
|
156
|
+
<button @click="confirmEntry()" class="btn btn-primary btn-lg w-full">Hold to Confirm Entry</button>
|
|
157
|
+
<button @click="$store.dsModals.close()"
|
|
158
|
+
class="block mx-auto mt-3 text-xs text-secondary hover:text-heading underline underline-offset-2">
|
|
159
|
+
I’ll confirm later
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
163
|
+
|
|
164
|
+
<%# ===== ENTERING (contest enter processing — the on-chain consume) ===== %>
|
|
165
|
+
<template x-if="props.step === 'entering'">
|
|
166
|
+
<div x-init="autoAdvance('entering', { step: 'entered', txSignature: '5xTrDemoEntrySignature1234567890abc' })">
|
|
167
|
+
<%= render "studio/modals/blocks/card_header",
|
|
168
|
+
spinner: true,
|
|
169
|
+
title: "Confirming your entry…",
|
|
170
|
+
subtitle: "Consuming one token on-chain to create your contest entry." %>
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
|
|
174
|
+
<%# ===== ENTERED (contest entered — the shared Entry Confirmed finish) ===== %>
|
|
175
|
+
<template x-if="props.step === 'entered'">
|
|
176
|
+
<div>
|
|
177
|
+
<%= render "studio/modals/blocks/card_header",
|
|
178
|
+
size: :lg, icon_color: 'primary',
|
|
179
|
+
title: "Good Luck", subtitle: "Entry Confirmed" %>
|
|
180
|
+
<%= render "studio/modals/blocks/solana_tx_link", tx_signature_key: "props.txSignature" %>
|
|
181
|
+
<button @click="$store.dsModals.close()" class="btn btn-primary btn-lg w-full">Contest Lobby ⚔️</button>
|
|
182
|
+
<button @click="$store.dsModals.close()"
|
|
183
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
184
|
+
Dismiss
|
|
185
|
+
</button>
|
|
121
186
|
</div>
|
|
122
187
|
</template>
|
|
123
188
|
</div>
|
|
@@ -24,6 +24,30 @@
|
|
|
24
24
|
}, 100);
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
|
+
// Demo auto-resolve (min-load convention): a processing modal opened with
|
|
28
|
+
// demoResolve holds its spinner at least MIN_LOAD_MS, then advances to
|
|
29
|
+
// success — or to error when demoError is set (the Processing specimen's
|
|
30
|
+
// success/error toggle). Guarded to the still-open, still-processing modal.
|
|
31
|
+
var entry = Alpine.store('dsModals').current();
|
|
32
|
+
if (!entry || entry.id !== 'onchain-tx' || !entry.props ||
|
|
33
|
+
entry.props.state !== 'processing' || !entry.props.demoResolve) return;
|
|
34
|
+
var minMs = entry.props.minDuration ||
|
|
35
|
+
(window.StudioModals && window.StudioModals.MIN_LOAD_MS) || 1400;
|
|
36
|
+
window.StudioModals.holdAtLeast(minMs).then(function () {
|
|
37
|
+
var cur = Alpine.store('dsModals').current();
|
|
38
|
+
if (!cur || cur.id !== 'onchain-tx' || cur.props.state !== 'processing') return;
|
|
39
|
+
if (entry.props.demoError) {
|
|
40
|
+
Alpine.store('dsModals').advance({ state: 'error',
|
|
41
|
+
title: 'Could not confirm',
|
|
42
|
+
errorMessage: 'The transaction was rejected. Give it another try.' });
|
|
43
|
+
} else {
|
|
44
|
+
Alpine.store('dsModals').advance({ state: 'success',
|
|
45
|
+
txSignature: '5xTrDemoSignature1234567890abcXYZ',
|
|
46
|
+
successTitle: 'Mint complete',
|
|
47
|
+
successSubtitle: 'Your on-chain action confirmed.',
|
|
48
|
+
ctaLabel: 'View wallet', ctaHref: null });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
27
51
|
}
|
|
28
52
|
}">
|
|
29
53
|
|
|
@@ -60,7 +60,18 @@
|
|
|
60
60
|
if (!this.attested()) return;
|
|
61
61
|
this.connecting = true; this.picking = name; this.error = '';
|
|
62
62
|
var self = this;
|
|
63
|
-
|
|
63
|
+
// DEMO walk continuity: a picked wallet runs a brief connecting state,
|
|
64
|
+
// then swaps to the on-chain tx modal in its processing state, which
|
|
65
|
+
// auto-resolves to on-chain success (demoResolve). So the glow follows
|
|
66
|
+
// Connect Wallet -> Processing -> On-chain success. (No real connect.)
|
|
67
|
+
setTimeout(function () {
|
|
68
|
+
self.connecting = false; self.picking = '';
|
|
69
|
+
Alpine.store('dsModals').swap('onchain-tx', {
|
|
70
|
+
state: 'processing', demoResolve: true, demoError: false,
|
|
71
|
+
title: 'Confirming on-chain',
|
|
72
|
+
message: 'Waiting for the wallet signature…'
|
|
73
|
+
});
|
|
74
|
+
}, 1100);
|
|
64
75
|
},
|
|
65
76
|
deepLink() {
|
|
66
77
|
if (!this.attested()) return;
|
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.28.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -273,6 +273,7 @@ files:
|
|
|
273
273
|
- app/views/studio/modals/_crop_photo.html.erb
|
|
274
274
|
- app/views/studio/modals/_host.html.erb
|
|
275
275
|
- app/views/studio/modals/_image_upload.html.erb
|
|
276
|
+
- app/views/studio/modals/_load_convention.html.erb
|
|
276
277
|
- app/views/studio/modals/_saving.html.erb
|
|
277
278
|
- app/views/studio/modals/auth/_resend_footer.html.erb
|
|
278
279
|
- app/views/studio/modals/blocks/_age_verify.html.erb
|