studio-engine 0.129.0 → 0.130.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.
@@ -1,20 +1,10 @@
1
- <%# Tricks section of the living style guide (admin/style).
2
- A board of style tricks you can SEE rendered live and hand to an agent to use
3
- out of the box. Each specimen (via the shared _specimen partial) surfaces the
4
- rendered primitive, its class name (one-click copy), and a copyable usage
5
- snippet. Every class here is a REAL engine class (engine.css component layer
6
- or engine-motion.css motion/effect/leveling layer), so a consumer that
7
- imports the engine's Tailwind entry (host.css brings both) styles the
8
- specimens for real.
9
-
10
- The Leveling group renders the REAL .level-badge tiers ported into the engine
11
- from Turf Monster, capability-gated by Studio.feature?(:leveling). When a host
12
- has leveling OFF (McRitchie Studio's default), its specimens render "disabled
13
- but present" via the _specimen `disabled:` local - visible and previewable,
14
- but greyed and flagged, never hidden.
15
-
16
- The color tokens/swatches now live in the Theme section (the landing section
17
- owns "the colors"); this section is the trick board. %>
1
+ <%# Tricks section of the living style guide (admin/style). Buttons come first,
2
+ then what you can add to a button, then the other primitives. Each specimen
3
+ (the shared _specimen partial) shows the real engine class live, with its name
4
+ and a usage snippet to copy.
5
+
6
+ The Leveling group is gated by Studio.feature?(:leveling). With it off, its
7
+ specimens render "disabled but present": visible, greyed and flagged, never hidden. %>
18
8
  <%
19
9
  # Keyed by the WHOLE class, never "btn-" + a name: Tailwind compiles only the
20
10
  # @utility names it can read in source.
@@ -30,26 +20,22 @@
30
20
  }
31
21
 
32
22
  leveling_on = Studio.feature?(:leveling)
23
+
24
+ # An ERB snippet cannot be written literally inside an ERB template: the scanner
25
+ # closes the tag at the first `%` + `>` it sees, whatever Ruby string it sits in.
26
+ # So assemble the markers: the raw template never contains one.
27
+ erb = ->(code) { "<" + "%= " + code + " %" + ">" }
28
+ erb_end = "<" + "% end %" + ">"
33
29
  %>
34
- <section id="tricks" class="space-y-12" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)">
35
- <div class="space-y-1">
36
- <h2 class="type-h3 text-heading">Tricks</h2>
37
- <p class="text-muted text-sm max-w-2xl">
38
- A board of style tricks you can see and hand to an agent to use out of the box.
39
- Each specimen surfaces the rendered primitive, its <strong>class name (one-click
40
- copy)</strong>, and a copyable usage snippet - the button, surface, scrim, motion,
41
- effect, and leveling primitives. The color tokens live in <a href="#theme" class="underline">Theme</a>.
42
- </p>
43
- </div>
30
+ <section id="tricks" class="section-target space-y-12">
31
+ <%= render "studio/section_header", title: "Tricks" %>
32
+ <p class="text-muted text-sm max-w-2xl">Buttons first, then what you can add to them.</p>
44
33
 
45
34
  <!-- Buttons -->
46
35
  <section class="space-y-5">
47
36
  <div class="space-y-1">
48
37
  <h3 class="type-h4 text-heading">Buttons</h3>
49
- <p class="text-muted text-sm">
50
- <code class="font-mono text-2xs">.btn</code> base plus one role variant, and the
51
- <code class="font-mono text-2xs">.btn-sm</code> / <code class="font-mono text-2xs">.btn-lg</code> sizes.
52
- </p>
38
+ <p class="text-muted text-sm"><code class="font-mono text-2xs">.btn</code> plus one role and one size.</p>
53
39
  </div>
54
40
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
55
41
  <% button_variants.each do |name, label| %>
@@ -72,16 +58,117 @@
72
58
  </div>
73
59
  </section>
74
60
 
61
+ <!-- Button tricks -->
62
+ <section id="button-tricks" class="section-subtarget space-y-5">
63
+ <div class="space-y-1">
64
+ <h3 class="type-h4 text-heading">Button tricks</h3>
65
+ <p class="text-muted text-sm">Add any of these to a <code class="font-mono text-2xs">.btn</code>.</p>
66
+ </div>
67
+ <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
68
+ <%# The CARD shape: the button carries its own background and the ring still
69
+ rings, because the primitive punches the host's box out of both wedge
70
+ layers. The WRAPPER shape (bare host + opaque child) is staged by the
71
+ Modals section's glow cards. engine-motion.css section 9b has both. %>
72
+ <%= render layout: "style/specimen",
73
+ locals: { klass: ".edge-rotate-glow (knobs: --edge-rotate-glow-color / -color-b)",
74
+ note: "Adds a glow that rotates around the button's edge.",
75
+ usage: %(<button class="btn btn-primary edge-rotate-glow">Save</button>\n<button class="btn btn-primary edge-rotate-glow"\n style="--edge-rotate-glow-color: #F08030; --edge-rotate-glow-color-b: #6890F0">Two colors</button>) } do %>
76
+ <button type="button" class="btn btn-primary edge-rotate-glow">Save</button>
77
+ <button type="button" class="btn btn-primary edge-rotate-glow"
78
+ style="--edge-rotate-glow-color: #F08030; --edge-rotate-glow-color-b: #6890F0">Two colors</button>
79
+ <% end %>
80
+
81
+ <%= render layout: "style/specimen",
82
+ locals: { klass: ".studio-border-glow",
83
+ note: "Adds a moving rainbow border with a soft halo.",
84
+ usage: %(<button class="btn btn-primary studio-border-glow">Save</button>) } do %>
85
+ <button type="button" class="btn btn-primary studio-border-glow">Save</button>
86
+ <% end %>
87
+
88
+ <%= render layout: "style/specimen",
89
+ locals: { klass: ".sheen .sheen--loop",
90
+ note: "Adds a band of light that sweeps across the button.",
91
+ usage: %(<button class="btn btn-primary sheen">Once</button>\n<button class="btn btn-primary sheen sheen--loop">Loop</button>) } do %>
92
+ <button type="button" class="btn btn-primary sheen">Once</button>
93
+ <button type="button" class="btn btn-primary sheen sheen--loop">Loop</button>
94
+ <% end %>
95
+
96
+ <%= render layout: "style/specimen",
97
+ locals: { klass: ".pulse-cta (knobs: --pulse-cta-color / -speed / -scale)",
98
+ note: "Adds a slow beat with a ring that spreads from the edge.",
99
+ usage: %(<button class="btn btn-primary pulse-cta">Save</button>\n<button class="btn btn-success pulse-cta" style="--pulse-cta-color: var(--color-success)">Go</button>) } do %>
100
+ <button type="button" class="btn btn-primary pulse-cta">Save</button>
101
+ <button type="button" class="btn btn-success pulse-cta" style="--pulse-cta-color: var(--color-success)">Go</button>
102
+ <% end %>
103
+
104
+ <%= render layout: "style/specimen",
105
+ locals: { klass: ".fizz-stack (studio/fizz)",
106
+ note: "Adds bubbles that rise from behind the button.",
107
+ usage: [erb.call(%(render layout: "studio/fizz", locals: { seed: "save" } do)),
108
+ %( <button class="btn btn-primary">Save</button>),
109
+ erb_end].join("\n") } do %>
110
+ <%= render layout: "studio/fizz", locals: { seed: "style-fizz" } do %>
111
+ <button type="button" class="btn btn-primary">Save</button>
112
+ <% end %>
113
+ <%= render layout: "studio/fizz", locals: { seed: "style-fizz-colors", colors: %w[#8b5cf6 #8b5cf6 #8b5cf6 #22d3ee #22d3ee #22d3ee #f59e0b #f59e0b #f59e0b #ef4444 #ef4444 #ef4444 #10b981 #10b981 #10b981 #ec4899 #ec4899 #ec4899] } do %>
114
+ <button type="button" class="btn btn-neutral">Six colors</button>
115
+ <% end %>
116
+ <% end %>
117
+
118
+ <%# The live button: press and hold for two seconds. The card clips, so this one
119
+ sets fizz_portal and its bubbles rise past the card's edge. %>
120
+ <%= render layout: "style/specimen",
121
+ locals: { klass: ".hold-btn (studio/hold_button)",
122
+ note: "Makes the button confirm only after a two-second hold.",
123
+ usage: %(render "studio/hold_button", hold_id: "confirm", duration: 2000, fizz_portal: true) } do %>
124
+ <div class="w-full max-w-xs" x-data="{ submitting: false }">
125
+ <%= render "studio/hold_button", hold_id: "style-hold", duration: 2000, fizz_portal: true,
126
+ default_text: "Hold to Confirm", hold_text: "Almost there…", success_text: "Confirmed!" %>
127
+ <button type="button" class="btn btn-neutral btn-sm mt-3 mx-auto block"
128
+ @click="document.querySelector('.hold-btn[data-hold-id=style-hold]').classList.remove('process','success','error','loading')">Reset</button>
129
+ </div>
130
+ <% end %>
131
+
132
+ <%# The same button, re-themed from an ancestor: no new CSS. %>
133
+ <%= render layout: "style/specimen",
134
+ locals: { klass: ".hold-btn (--hold-* overrides)",
135
+ note: "Recolors a hold button from any parent element.",
136
+ usage: %(<div style="--hold-bg-from:#f0abfc;--hold-success-to:#4c1d95">…</div>) } do %>
137
+ <div class="w-full max-w-xs" x-data="{ submitting: false }"
138
+ style="--hold-bg-from:#f0abfc;--hold-bg-mid:#a855f7;--hold-bg-to:#6b21a8;
139
+ --hold-success-from:#c4b5fd;--hold-success-to:#4c1d95;--hold-success-glow-rgb:139 92 246">
140
+ <%= render "studio/hold_button", hold_id: "style-hold-custom", duration: 2000,
141
+ default_text: "Hold to Confirm", success_text: "Confirmed!",
142
+ fizz_colors: %w[#f0abfc #6b21a8 #a855f7 #a5b4fc #312e81 #6366f1 #fbcfe8 #831843 #ec4899
143
+ #bae6fd #0c4a6e #0ea5e9 #ddd6fe #4c1d95 #8b5cf6 #fde68a #78350f #f59e0b] %>
144
+ <button type="button" class="btn btn-neutral btn-sm mt-3 mx-auto block"
145
+ @click="document.querySelector('.hold-btn[data-hold-id=style-hold-custom]').classList.remove('process','success','error','loading')">Reset</button>
146
+ </div>
147
+ <% end %>
148
+
149
+ <%= render layout: "style/specimen",
150
+ locals: { klass: "window.studioConfetti.burst(el)",
151
+ note: "Fires confetti out from behind the button.",
152
+ usage: %(<button class="btn btn-primary" x-data @click="window.studioConfetti.burst($el)">Save</button>) } do %>
153
+ <button type="button" class="btn btn-primary" x-data
154
+ @click="window.studioConfetti && window.studioConfetti.burst($el)">Burst</button>
155
+ <% end %>
156
+
157
+ <%= render layout: "style/specimen",
158
+ locals: { klass: "window.studioConfetti.cannons()",
159
+ note: "Fires confetti in from both sides of the screen.",
160
+ usage: %(<button class="btn btn-success" x-data @click="window.studioConfetti.cannons()">Done</button>) } do %>
161
+ <button type="button" class="btn btn-success" x-data
162
+ @click="window.studioConfetti && window.studioConfetti.cannons()">Cannons</button>
163
+ <% end %>
164
+ </div>
165
+ </section>
166
+
75
167
  <!-- Page header -->
76
168
  <section class="space-y-5" data-page-header-specimen>
77
169
  <div class="space-y-1">
78
170
  <h3 class="type-h4 text-heading">Page header</h3>
79
- <p class="text-muted text-sm">
80
- <code class="font-mono text-2xs">studio/_page_header</code>: the title, optional meta beside
81
- it and optional tabs on the right. This page's own title row is one: scroll to see it take the bar.
82
- It joins the pinned stack, so a table's sticky header sits under it: navbar, page row, table
83
- header, each below the last. The rule is in <code class="font-mono text-2xs">docs/PAGE_HEADER.md</code>.
84
- </p>
171
+ <p class="text-muted text-sm"><code class="font-mono text-2xs">studio/_page_header</code> is the title row, with optional meta and tabs.</p>
85
172
  </div>
86
173
  <div class="grid gap-4 lg:grid-cols-2">
87
174
  <%# The classes alone, not the partial: the partial's row is sticky, joins the
@@ -107,11 +194,33 @@
107
194
  </div>
108
195
  </section>
109
196
 
197
+ <!-- Section header -->
198
+ <section class="space-y-5" data-section-header-specimen>
199
+ <div class="space-y-1">
200
+ <h3 class="type-h4 text-heading">Section header</h3>
201
+ <p class="text-muted text-sm">
202
+ <code class="font-mono text-2xs">studio/_section_header</code>: a section's title that sticks under
203
+ the page row until the next section's takes its place. This page's own section titles are these.
204
+ The rule is in <code class="font-mono text-2xs">docs/PAGE_HEADER.md</code>.
205
+ </p>
206
+ </div>
207
+ <div class="grid gap-4 lg:grid-cols-2">
208
+ <%# The classes alone, not the partial: the partial's header sticks and joins the
209
+ pinned stack, and a specimen must do neither. %>
210
+ <% [["At rest", {}], ["Stuck", { stuck: "" }]].each do |state, data| %>
211
+ <%= render layout: "style/specimen",
212
+ locals: { klass: "studio/_section_header (#{state.downcase})",
213
+ usage: %(<section id="modals" class="section-target">\n <%= render "studio/section_header", title: "Modals" %\>\n ...\n</section>) } do %>
214
+ <%= tag.p("Modals", class: "section-header px-4", data: data) %>
215
+ <% end %>
216
+ <% end %>
217
+ </div>
218
+ </section>
219
+
110
220
  <!-- Surfaces, text and form -->
111
221
  <section class="space-y-5">
112
222
  <div class="space-y-1">
113
223
  <h3 class="type-h4 text-heading">Surfaces, text &amp; form</h3>
114
- <p class="text-muted text-sm">Cards, badges, inputs, the empty state, and the uppercase meta label.</p>
115
224
  </div>
116
225
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
117
226
  <%= render layout: "style/specimen",
@@ -160,17 +269,8 @@
160
269
  <section class="space-y-5">
161
270
  <div class="space-y-1">
162
271
  <h3 class="type-h4 text-heading">Time stamps — the &ldquo;at&rdquo; format</h3>
163
- <p class="text-muted text-sm">
164
- <code class="font-mono text-2xs">at_time_tag</code> stamps WHEN something happened, on
165
- the reader&rsquo;s own clock. The stamps below are live: the server rendered them in this
166
- app&rsquo;s timezone and <code class="font-mono text-2xs">studio/at_time_script</code>
167
- re-stamped them in yours. Hover one for the relative time and your zone.
168
- </p>
272
+ <p class="text-muted text-sm"><code class="font-mono text-2xs">at_time_tag</code> stamps when something happened, on the reader&rsquo;s own clock.</p>
169
273
  </div>
170
- <%# An ERB snippet cannot be written literally inside an ERB template: the
171
- scanner closes the tag at the first `%` + `>` it sees, whatever Ruby string
172
- it sits in. So assemble the markers — the raw template never contains one. %>
173
- <% erb = ->(code) { "<" + "%= " + code + " %" + ">" } %>
174
274
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
175
275
  <%= render layout: "style/specimen",
176
276
  locals: { klass: "at_time_tag(time)",
@@ -225,26 +325,19 @@
225
325
  <!-- Branded loader -->
226
326
  <%
227
327
  loader_config = Studio.loader
228
- # An ERB tag as text, built so this file never holds a closing tag inside one.
229
- erb_tag = ->(code) { "<" + "%= #{code} %" + ">" }
230
328
  loader_art_styles = Studio::Loader::ART_STYLES.keys.select do |style|
231
329
  loader_config[:art] && Studio::Loader.art(loader_config[:art], style)
232
330
  end
233
331
  %>
234
- <section id="loader" class="space-y-5" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)">
332
+ <section id="loader" class="section-subtarget space-y-5">
235
333
  <div class="space-y-1">
236
334
  <h3 class="type-h4 text-heading">Branded loader</h3>
237
- <p class="text-muted text-sm max-w-2xl">
238
- This app's own icon in its theme colours, in the style <code class="font-mono text-2xs">config.loader</code>
239
- names (<strong><%= loader_config[:style].to_s.tr("_", " ") %></strong>). Page loads, long waits and pending
240
- buttons all draw it. One class, <code class="font-mono text-2xs">.studio-loader</code>, sized by
241
- <code class="font-mono text-2xs">--studio-loader-size</code>. studio-engine docs/LOADER.md.
242
- </p>
335
+ <p class="text-muted text-sm max-w-2xl">This app's icon in its theme colors, in the <strong><%= loader_config[:style].to_s.tr("_", " ") %></strong> style.</p>
243
336
  </div>
244
337
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
245
338
  <%= render layout: "style/specimen",
246
339
  locals: { klass: ".studio-loader (this app's style)",
247
- usage: [erb_tag.call("studio_loader"), erb_tag.call('studio_loader size: 20, label: "Saving"')].join("\n") } do %>
340
+ usage: [erb.call("studio_loader"), erb.call('studio_loader size: 20, label: "Saving"')].join("\n") } do %>
248
341
  <%= studio_loader %>
249
342
  <%= studio_loader size: 32 %>
250
343
  <%= studio_loader size: 20 %>
@@ -252,7 +345,7 @@
252
345
 
253
346
  <%= render layout: "style/specimen",
254
347
  locals: { klass: "studio_loader_pending (inside a busy button)",
255
- usage: ["<button class=\"btn btn-primary\">", " <span data-loader-idle>Save</span>", " #{erb_tag.call('studio_loader_pending')}", "</button>"].join("\n") } do %>
348
+ usage: ["<button class=\"btn btn-primary\">", " <span data-loader-idle>Save</span>", " #{erb.call('studio_loader_pending')}", "</button>"].join("\n") } do %>
256
349
  <button type="button" class="btn btn-primary" aria-busy="true">
257
350
  <%= studio_loader_pending %><span class="ml-2">Saving…</span>
258
351
  </button>
@@ -260,7 +353,7 @@
260
353
 
261
354
  <%= render layout: "style/specimen",
262
355
  locals: { klass: "studio_loader_overlay (the page-load overlay)",
263
- usage: ["#{erb_tag.call('studio_loader_overlay')} (once, in the layout body)", 'window.StudioLoader.show("Caption") / .hide()'].join("\n") } do %>
356
+ usage: ["#{erb.call('studio_loader_overlay')} (once, in the layout body)", 'window.StudioLoader.show("Caption") / .hide()'].join("\n") } do %>
264
357
  <button type="button" class="btn btn-secondary" data-test="style-show-loader-overlay"
265
358
  x-data x-on:click="window.StudioLoader && window.StudioLoader.show('Loading your page…'); setTimeout(() => window.StudioLoader && window.StudioLoader.hide(), 3000)">
266
359
  Show the overlay for 3 seconds
@@ -285,18 +378,9 @@
285
378
  <section class="space-y-5">
286
379
  <div class="space-y-1">
287
380
  <h3 class="type-h4 text-heading">Motion primitives</h3>
288
- <p class="text-muted text-sm">
289
- The seven <code class="font-mono text-2xs">engine-motion.css</code> primitives. Each knob is a themeable
290
- CSS variable - tune it inline with <code class="font-mono text-2xs">style="--knob: value"</code>.
291
- </p>
381
+ <p class="text-muted text-sm">Tune any knob inline with <code class="font-mono text-2xs">style="--knob: value"</code>.</p>
292
382
  </div>
293
383
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
294
- <%= render layout: "style/specimen",
295
- locals: { klass: ".studio-border-glow (rounded-xl wrapper)",
296
- usage: %(<div class="studio-border-glow rounded-xl">...</div>) } do %>
297
- <div class="studio-border-glow rounded-xl p-5 bg-surface text-center text-sm text-heading">Border glow</div>
298
- <% end %>
299
-
300
384
  <%= render layout: "style/specimen",
301
385
  locals: { klass: ".spinner (knob: --spinner-size)",
302
386
  usage: %(<span class="spinner"></span>\n<span class="spinner" style="--spinner-size: 2rem"></span>) } do %>
@@ -316,13 +400,6 @@
316
400
  <span class="loading-dots" style="--loading-dots-size: 12px; --loading-dots-color: var(--color-cta)"><span></span><span></span><span></span></span>
317
401
  <% end %>
318
402
 
319
- <%= render layout: "style/specimen",
320
- locals: { klass: ".sheen .sheen--loop",
321
- usage: %(<span class="sheen sheen--loop">...</span>) } do %>
322
- <span class="sheen sheen--loop badge text-body">Level 5 unlocked</span>
323
- <span class="sheen sheen--loop card px-4 py-2 text-sm text-body" style="--sheen-duration: 3s">Sheen sweep</span>
324
- <% end %>
325
-
326
403
  <%= render layout: "style/specimen",
327
404
  locals: { klass: ".ping .ping--dot (knob: --ping-color)",
328
405
  usage: %(<span class="ping ping--dot"></span>) } do %>
@@ -372,12 +449,6 @@
372
449
  <section class="space-y-5">
373
450
  <div class="space-y-1">
374
451
  <h3 class="type-h4 text-heading">Effects</h3>
375
- <p class="text-muted text-sm">
376
- The <code class="font-mono text-2xs">engine-motion.css</code> visual-effect primitives beyond motion -
377
- gradient text, a soft glow halo, a one- or two-color rotating <strong>selection glow</strong>
378
- (ported from Turf Monster's selected-team-card ring), frosted glass, and a conic wash. Each is
379
- themed through the role tokens and tunable via CSS-var knobs.
380
- </p>
381
452
  </div>
382
453
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
383
454
  <%= render layout: "style/specimen",
@@ -399,21 +470,6 @@
399
470
  </div>
400
471
  <% end %>
401
472
 
402
- <%# The CARD shape: the host carries its own background and the ring still
403
- rings, because the primitive punches the host's box out of both wedge
404
- layers. The WRAPPER shape (bare host + opaque child) is staged by the
405
- Modals section's glow cards — both are documented in engine-motion.css
406
- §9b. Two wedges travel the edge, so a pair of colors reads as a pair. %>
407
- <%= render layout: "style/specimen",
408
- locals: { klass: ".studio-team-glow (knobs: --studio-team-glow-color / -color-b)",
409
- usage: %(<div class="studio-team-glow rounded-xl bg-surface p-5">Selected</div>\n<div class="studio-team-glow rounded-xl bg-surface p-5"\n style="--studio-team-glow-color: #F08030; --studio-team-glow-color-b: #6890F0">Fire / Flying</div>) } do %>
410
- <div class="flex flex-wrap items-center justify-center gap-6">
411
- <div class="studio-team-glow rounded-xl bg-surface border border-subtle px-5 py-4 text-sm font-semibold text-heading text-center">Selected</div>
412
- <div class="studio-team-glow rounded-xl bg-surface border border-subtle px-5 py-4 text-sm font-semibold text-heading text-center"
413
- style="--studio-team-glow-color: #F08030; --studio-team-glow-color-b: #6890F0">Fire / Flying</div>
414
- </div>
415
- <% end %>
416
-
417
473
  <%= render layout: "style/specimen",
418
474
  locals: { klass: ".surface-glass (knobs: --surface-glass-opacity / -blur)",
419
475
  usage: %(<div class="surface-glass rounded-xl p-4">...</div>) } do %>
@@ -433,115 +489,6 @@
433
489
  </div>
434
490
  </section>
435
491
 
436
- <!-- Hold to confirm -->
437
- <section class="space-y-5">
438
- <div class="space-y-1">
439
- <h3 class="type-h4 text-heading">Hold to confirm</h3>
440
- <p class="text-muted text-sm max-w-2xl">
441
- A press-and-hold CTA for an action you do not want taken by accident, and the
442
- carbonation behind it. Ported from Turf Monster, where it confirms a contest entry.
443
- The bubbles sit <em>behind</em> the button and show only where they escape its edges;
444
- they simmer at rest, boil on hover and through the hold, then burst outward on
445
- success. The whole thing themes off the primary role token, so it wears this app&rsquo;s
446
- brand — and every color also reads a <code class="font-mono text-2xs">--hold-*</code>
447
- input, so one instance can be re-themed from any ancestor.
448
- <code class="font-mono text-2xs">prefers-reduced-motion</code> switches the bubbles off.
449
- Inside a card that clips, pass <code class="font-mono text-2xs">fizz_portal: true</code>
450
- and the bubbles rise past the card&rsquo;s edge instead of stopping at it.
451
- </p>
452
- </div>
453
- <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
454
- <%# 1. The fizz on its own: no button, just the layers. %>
455
- <%= render layout: "style/specimen",
456
- locals: { klass: ".hold-stack > .hold-fizz (.fizz-bit)",
457
- usage: %(render "studio/fizz_layer", seed: "demo") } do %>
458
- <div class="w-full max-w-xs">
459
- <span class="hold-stack fizz-lively" style="height:3rem;--fizz-c-1:#8b5cf6;--fizz-c-4:#22d3ee;--fizz-c-7:#f59e0b;--fizz-c-10:#ef4444;--fizz-c-13:#10b981;--fizz-c-16:#ec4899">
460
- <%= render "studio/fizz_layer", seed: "style-fizz" %>
461
- <%= render "studio/fizz_layer", seed: "style-fizz~extra", extra: true %>
462
- <span class="flex items-center justify-center h-12 text-xs text-muted">six zones, six colors</span>
463
- </span>
464
- </div>
465
- <% end %>
466
-
467
- <%# 2. The button itself, live: press and hold for two seconds. The card
468
- clips, so this one sets fizz_portal and its bubbles rise past the
469
- card's edge. %>
470
- <%= render layout: "style/specimen",
471
- locals: { klass: ".hold-btn (studio/hold_button)",
472
- usage: %(render "studio/hold_button", hold_id: "confirm", duration: 2000, fizz_portal: true) } do %>
473
- <div class="w-full max-w-xs" x-data="{ submitting: false }">
474
- <%= render "studio/hold_button", hold_id: "style-hold", duration: 2000, fizz_portal: true,
475
- default_text: "Hold to Confirm", hold_text: "Almost there…", success_text: "Confirmed!" %>
476
- <button type="button" class="btn btn-neutral btn-sm mt-3 mx-auto block"
477
- @click="document.querySelector('.hold-btn[data-hold-id=style-hold]').classList.remove('process','success','error','loading')">Reset</button>
478
- </div>
479
- <% end %>
480
-
481
- <%# 3. The same button, re-themed from an ancestor — no new CSS. %>
482
- <%= render layout: "style/specimen",
483
- locals: { klass: ".hold-btn (--hold-* overrides)",
484
- usage: %(<div style="--hold-bg-from:#f0abfc;--hold-success-to:#4c1d95">…</div>) } do %>
485
- <div class="w-full max-w-xs" x-data="{ submitting: false }"
486
- style="--hold-bg-from:#f0abfc;--hold-bg-mid:#a855f7;--hold-bg-to:#6b21a8;
487
- --hold-success-from:#c4b5fd;--hold-success-to:#4c1d95;--hold-success-glow-rgb:139 92 246">
488
- <%= render "studio/hold_button", hold_id: "style-hold-custom", duration: 2000,
489
- default_text: "Hold to Confirm", success_text: "Confirmed!",
490
- fizz_colors: %w[#f0abfc #6b21a8 #a855f7 #a5b4fc #312e81 #6366f1 #fbcfe8 #831843 #ec4899
491
- #bae6fd #0c4a6e #0ea5e9 #ddd6fe #4c1d95 #8b5cf6 #fde68a #78350f #f59e0b] %>
492
- <button type="button" class="btn btn-neutral btn-sm mt-3 mx-auto block"
493
- @click="document.querySelector('.hold-btn[data-hold-id=style-hold-custom]').classList.remove('process','success','error','loading')">Reset</button>
494
- </div>
495
- <% end %>
496
- </div>
497
- </section>
498
-
499
- <!-- Confetti & pulse -->
500
- <section class="space-y-5">
501
- <div class="space-y-1">
502
- <h3 class="type-h4 text-heading">Confetti &amp; pulse</h3>
503
- <p class="text-muted text-sm max-w-2xl">
504
- Celebration effects lifted from Turf Monster into the engine. Confetti is
505
- <code class="font-mono text-2xs">window.studioConfetti</code> - a zero-dependency,
506
- no-CDN effect (canvas-confetti is vendored into the engine and shipped through the
507
- asset pipeline). Two callable variants, both honoring
508
- <code class="font-mono text-2xs">prefers-reduced-motion</code>. The pulse is the
509
- <code class="font-mono text-2xs">.pulse-cta</code> attention beat. Fire the confetti
510
- with the buttons below.
511
- </p>
512
- </div>
513
- <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
514
- <%= render layout: "style/specimen",
515
- locals: { klass: "window.studioConfetti.burst(el)",
516
- usage: %(window.studioConfetti.burst(cardEl)\n// radial burst from behind a card/modal, from its center) } do %>
517
- <div x-data="{}" class="flex flex-col items-center gap-3 py-2">
518
- <div x-ref="burstCard" class="card px-5 py-4 text-sm font-semibold text-heading text-center">Success!</div>
519
- <button type="button" class="btn btn-primary btn-sm"
520
- @click="window.studioConfetti && window.studioConfetti.burst($refs.burstCard)">Fire burst</button>
521
- </div>
522
- <% end %>
523
-
524
- <%= render layout: "style/specimen",
525
- locals: { klass: "window.studioConfetti.cannons()",
526
- usage: %(window.studioConfetti.cannons()\n// left + right side-cannons, full-screen celebration) } do %>
527
- <div x-data="{}" class="flex flex-col items-center gap-3 py-2">
528
- <p class="text-sm font-semibold text-heading text-center">You joined a contest!</p>
529
- <button type="button" class="btn btn-success btn-sm"
530
- @click="window.studioConfetti && window.studioConfetti.cannons()">Fire cannons</button>
531
- </div>
532
- <% end %>
533
-
534
- <%= render layout: "style/specimen",
535
- locals: { klass: ".pulse-cta (knobs: --pulse-cta-color / -speed / -scale)",
536
- usage: %(<button class="btn btn-primary pulse-cta">Change Username</button>\n<button class="btn btn-success pulse-cta" style="--pulse-cta-color: var(--color-success)">Go</button>) } do %>
537
- <div class="flex flex-wrap items-center justify-center gap-4">
538
- <button type="button" class="btn btn-primary pulse-cta">Change Username</button>
539
- <button type="button" class="btn btn-success pulse-cta" style="--pulse-cta-color: var(--color-success)">Go</button>
540
- </div>
541
- <% end %>
542
- </div>
543
- </section>
544
-
545
492
  <!-- Leveling (capability-gated: Studio.feature?(:leveling)) -->
546
493
  <section class="space-y-5">
547
494
  <div class="space-y-1">
@@ -553,15 +500,7 @@
553
500
  <span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">disabled on this app</span>
554
501
  <% end %>
555
502
  </div>
556
- <p class="text-muted text-sm max-w-2xl">
557
- The real <code class="font-mono text-2xs">.level-badge</code> tiers, gated by
558
- <code class="font-mono text-2xs">Studio.feature?(:leveling)</code>. Ported into the engine
559
- (<code class="font-mono text-2xs">engine-motion.css</code>) from Turf Monster's leveling - the
560
- outline starter through the holographic Level 10 - so any consumer with leveling on ships the
561
- polished ladder. The tiers theme off <code class="font-mono text-2xs">--color-primary-*</code>, so
562
- they restyle per app. Leveling is <strong><%= leveling_on ? "on" : "off" %></strong> here, so these
563
- render <%= leveling_on ? "live" : "disabled-but-present - visible and previewable, but greyed and flagged" %>.
564
- </p>
503
+ <p class="text-muted text-sm max-w-2xl">Level pills, gated by <code class="font-mono text-2xs">Studio.feature?(:leveling)</code>.</p>
565
504
  </div>
566
505
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
567
506
  <%= render layout: "style/specimen",
@@ -22,7 +22,8 @@
22
22
 
23
23
  HEADINGS wear the type ladder, on trial on this page only (style/_type_ladder): the one
24
24
  <h1> is studio/_page_header's, at type-h2; a section is an <h2> at type-h3; a subsection
25
- is an <h3> at type-h4.
25
+ is an <h3> at type-h4. The section's <h2> is studio/_section_header's, in type-h3's numbers,
26
+ which sticks under the page row until the next section's takes its place.
26
27
 
27
28
  Specimens use ONLY the engine's own classes (engine.css component layer plus
28
29
  engine-motion.css motion/effect/leveling layer), so a consumer that imports
@@ -59,7 +60,7 @@
59
60
  %>
60
61
  <div class="max-w-6xl mx-auto px-4 pt-6 pb-16" data-type-ladder>
61
62
  <%# The page's title and its section tabs: studio/_page_header, the row of the
62
- triple stack (docs/PAGE_HEADER.md). It sits at --z-sticky, above --z-raised,
63
+ pinned stack (docs/PAGE_HEADER.md). It sits at --z-sticky, above --z-raised,
63
64
  so the live hold button's portal bubbles scroll under it rather than over it.
64
65
 
65
66
  The style-section-nav controller moves the highlight as the page scrolls
@@ -77,7 +78,10 @@
77
78
  data: { style_section_nav_target: "link" } }
78
79
  } %>
79
80
 
80
- <div class="space-y-16 pt-10">
81
+ <%# 6rem between sections: a first jump from the top of the page lands by the navbar's
82
+ collapse below the line, and the section above must be gone from the bar by then
83
+ (76px on the hub at phone width). %>
84
+ <div class="space-y-24 pt-10">
81
85
  <%= render "style/theme" %>
82
86
  <%= render "style/modals" %>
83
87
  <% if host_modals %>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.129.0"
2
+ VERSION = "0.130.0"
3
3
  end
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.129.0
4
+ version: 0.130.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -640,11 +640,13 @@ files:
640
640
  - app/views/studio/_board_assets.html.erb
641
641
  - app/views/studio/_confirm_interstitial.html.erb
642
642
  - app/views/studio/_cropper_assets.html.erb
643
+ - app/views/studio/_fizz.html.erb
643
644
  - app/views/studio/_fizz_layer.html.erb
644
645
  - app/views/studio/_hold_button.html.erb
645
646
  - app/views/studio/_hold_button_guard.html.erb
646
647
  - app/views/studio/_leveling_activity_assets.html.erb
647
648
  - app/views/studio/_page_header.html.erb
649
+ - app/views/studio/_section_header.html.erb
648
650
  - app/views/studio/_session_stamp.html.erb
649
651
  - app/views/studio/admin_models/_arenas_table.html.erb
650
652
  - app/views/studio/admin_models/_sessions_table.html.erb