studio-engine 0.21.0 → 0.23.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,13 +1,52 @@
1
1
  <%# Tasks section of the living style guide (admin/style).
2
2
 
3
- A DEMONSTRATOR of the shared board primitive the columns, cards, stage
4
- badges, and drag/rank affordance that every board (McRitchie Studio's
5
- /deployments is the top-of-line) builds from. It is a static sketch, not a
6
- backend-wired sortable board; the point is to show the standard so custom
7
- boards can adopt it. The stage-* badge palette (components/_badge) is the
8
- spine. %>
3
+ The shared board primitive, LIVE. Phase D lifted the columns / cards / drag +
4
+ optimistic move / reorder that the three near-identical McRitchie Studio boards
5
+ (tasks / news / content) each hand-rolled into one engine primitive
6
+ (studio/board/_board + the window.studioBoard factory + vendored SortableJS).
7
+ The first block below renders that real primitive with `demo: true` — you can
8
+ DRAG a card between columns and the move animates + toasts, but nothing POSTs.
9
+ The blocks under it stay a static reference: the stage-* badge palette
10
+ (components/_badge) that is the pipeline spine, and the standard-vs-custom
11
+ split of what a board inherits vs sets. %>
9
12
  <%
10
- # The shared pipeline palette: each column maps to a stage-* badge scheme.
13
+ # LIVE specimen the real studio/board/_board primitive, demo mode. Plain Hash
14
+ # records (the style guide has no models); each card id becomes id="card-<id>".
15
+ # Each demo card carries its current `zone` (the column it sits in) — in a real
16
+ # board the record knows its own stage; here the Hash does.
17
+ live_board_columns = [
18
+ { key: "designed", label: "Designed", badge_class: "stage-fresh",
19
+ cards: [
20
+ { id: "engine-board-primitive", zone: "designed", title: "Engine board primitive", repo: "studio-engine", who: "SH" },
21
+ { id: "coinflow-buy-rail", zone: "designed", title: "Coinflow buy rail", repo: "turf-monster", who: "JA" }
22
+ ] },
23
+ { key: "building", label: "Building", badge_class: "stage-shaping",
24
+ cards: [{ id: "navbar-rpc-preload", zone: "building", title: "Navbar RPC preload", repo: "turf-monster", who: "CA" }] },
25
+ { key: "submitted", label: "Submitted", badge_class: "stage-structured",
26
+ cards: [{ id: "magic-link-ttl-bump", zone: "submitted", title: "Magic-link TTL bump", repo: "studio-engine", who: "SH" }] },
27
+ { key: "shipped", label: "Shipped", badge_class: "stage-shipped",
28
+ cards: [{ id: "ses-email-backend", zone: "shipped", title: "SES email backend", repo: "mcritchie", who: "ST" }] }
29
+ ]
30
+
31
+ # Copy-paste-for-an-agent usage. Built by array-join, NOT a heredoc: a heredoc as
32
+ # the last expression of an ERB scriptlet trips Erubi, and literal ERB tags in the
33
+ # body would close the tag. Plain render() calls read clearly for an agent.
34
+ board_usage_snippet = [
35
+ "# once at layout level (a <script> in the board template never runs):",
36
+ 'render "studio/board_assets"',
37
+ "",
38
+ "# then the board itself:",
39
+ 'render "studio/board/board",',
40
+ " columns: @columns, # [{ key:, label:, cards:, badge_class: }]",
41
+ ' card_partial: "tasks/task_card",',
42
+ " reorder_url: tasks_reorder_path, # POST { slugs: [...], zone: }",
43
+ ' move_url: "/tasks/:id.json", # PATCH { task: { stage: } }',
44
+ ' move_param: { resource: "task", attr: "stage" },',
45
+ ' live_channel: "deployments"'
46
+ ].join("\n")
47
+
48
+ # The static reference palette below (unchanged): each column maps to a stage-*
49
+ # badge scheme.
11
50
  board_columns = [
12
51
  { key: "stage-fresh", label: "Fresh", count: 2 },
13
52
  { key: "stage-shaping", label: "Shaping", count: 1 },
@@ -32,13 +71,45 @@
32
71
  <h2 class="text-2xl font-bold text-heading">Tasks</h2>
33
72
  <p class="text-muted text-sm max-w-2xl">
34
73
  The shared board primitive that every board builds from &mdash; columns, cards, the
35
- <code class="font-mono text-2xs">stage-*</code> badge palette, and the drag / rank affordance.
36
- McRitchie Studio's <code class="font-mono text-2xs">/deployments</code> board is the top-of-line;
37
- this is a static demonstrator (full extraction lands in a later phase).
74
+ <code class="font-mono text-2xs">stage-*</code> badge palette, and the drag / optimistic
75
+ move + reorder. McRitchie Studio's <code class="font-mono text-2xs">/deployments</code> board is
76
+ the top-of-line; the specimen below is the <strong>real</strong>
77
+ <code class="font-mono text-2xs">studio/board/board</code> primitive in
78
+ <code class="font-mono text-2xs">demo: true</code> mode &mdash; drag a card, no save.
38
79
  </p>
39
80
  </div>
40
81
 
41
- <%# ---- The board ------------------------------------------------------- %>
82
+ <%# ---- The LIVE primitive (demo mode) --------------------------------- %>
83
+ <%# Render the factory once (a <script> in the board template would never run), then
84
+ the board. demo: true → drag animates + toasts locally, nothing POSTs. %>
85
+ <%= render "studio/board_assets" %>
86
+ <div class="space-y-3">
87
+ <div class="flex items-center gap-2">
88
+ <span class="badge" style="color: var(--color-success); border-color: var(--color-success)">Live</span>
89
+ <p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> &middot; demo &mdash; drag a card between columns</p>
90
+ </div>
91
+ <%= render "studio/board/board",
92
+ demo: true,
93
+ columns: live_board_columns,
94
+ card_partial: "style/board/demo_card",
95
+ card_as: :card,
96
+ reorder_url: "#",
97
+ move_url: "#",
98
+ move_param: { resource: "task", attr: "stage" },
99
+ group: "studio-board-demo",
100
+ empty_label: "Drop tasks here" %>
101
+ <div class="relative" x-data="{ copied: false }">
102
+ <pre class="bg-inset text-body text-2xs rounded-lg px-3 py-2 pr-16 overflow-x-auto"><code x-ref="boardUsage"><%= board_usage_snippet %></code></pre>
103
+ <button type="button"
104
+ class="btn btn-neutral btn-sm absolute top-2 right-2"
105
+ aria-label="Copy usage snippet"
106
+ @click="navigator.clipboard?.writeText($refs.boardUsage.innerText); copied = true; setTimeout(() => copied = false, 1200)">
107
+ <span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
108
+ </button>
109
+ </div>
110
+ </div>
111
+
112
+ <%# ---- The static reference sketch ------------------------------------- %>
42
113
  <div class="fade-edge-x overflow-x-auto pb-2">
43
114
  <div class="flex gap-4 min-w-max">
44
115
  <% board_columns.each do |col| %>
@@ -259,6 +259,52 @@
259
259
  </div>
260
260
  </section>
261
261
 
262
+ <!-- Confetti & pulse -->
263
+ <section class="space-y-5">
264
+ <div class="space-y-1">
265
+ <h3 class="text-xl font-bold text-heading">Confetti &amp; pulse</h3>
266
+ <p class="text-muted text-sm max-w-2xl">
267
+ Celebration effects lifted from Turf Monster into the engine. Confetti is
268
+ <code class="font-mono text-2xs">window.studioConfetti</code> - a zero-dependency,
269
+ no-CDN effect (canvas-confetti is vendored into the engine and shipped through the
270
+ asset pipeline). Two callable variants, both honoring
271
+ <code class="font-mono text-2xs">prefers-reduced-motion</code>. The pulse is the
272
+ <code class="font-mono text-2xs">.pulse-cta</code> attention beat. Fire the confetti
273
+ with the buttons below.
274
+ </p>
275
+ </div>
276
+ <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
277
+ <%= render layout: "style/specimen",
278
+ locals: { klass: "window.studioConfetti.burst(el)",
279
+ usage: %(window.studioConfetti.burst(cardEl)\n// radial burst from behind a card/modal, from its center) } do %>
280
+ <div x-data="{}" class="flex flex-col items-center gap-3 py-2">
281
+ <div x-ref="burstCard" class="card px-5 py-4 text-sm font-semibold text-heading text-center">Success!</div>
282
+ <button type="button" class="btn btn-primary btn-sm"
283
+ @click="window.studioConfetti && window.studioConfetti.burst($refs.burstCard)">Fire burst</button>
284
+ </div>
285
+ <% end %>
286
+
287
+ <%= render layout: "style/specimen",
288
+ locals: { klass: "window.studioConfetti.cannons()",
289
+ usage: %(window.studioConfetti.cannons()\n// left + right side-cannons, full-screen celebration) } do %>
290
+ <div x-data="{}" class="flex flex-col items-center gap-3 py-2">
291
+ <p class="text-sm font-semibold text-heading text-center">You joined a contest!</p>
292
+ <button type="button" class="btn btn-success btn-sm"
293
+ @click="window.studioConfetti && window.studioConfetti.cannons()">Fire cannons</button>
294
+ </div>
295
+ <% end %>
296
+
297
+ <%= render layout: "style/specimen",
298
+ locals: { klass: ".pulse-cta (knobs: --pulse-cta-color / -speed / -scale)",
299
+ usage: %(<button class="btn btn-primary pulse-cta">Change Username</button>\n<button class="btn btn-success pulse-cta" style="--pulse-cta-color: var(--color-success)">Go</button>) } do %>
300
+ <div class="flex flex-wrap items-center justify-center gap-4">
301
+ <button type="button" class="btn btn-primary pulse-cta">Change Username</button>
302
+ <button type="button" class="btn btn-success pulse-cta" style="--pulse-cta-color: var(--color-success)">Go</button>
303
+ </div>
304
+ <% end %>
305
+ </div>
306
+ </section>
307
+
262
308
  <!-- Leveling (capability-gated: Studio.feature?(:leveling)) -->
263
309
  <section class="space-y-5">
264
310
  <div class="space-y-1">
@@ -0,0 +1,22 @@
1
+ <%# locals: (card:, id_attr: "slug", zone_attr: "stage") %>
2
+ <%#
3
+ A demo board card for the /admin/style Board specimen. Renders THROUGH the engine
4
+ card-shell chrome (studio/board/_card_shell) so the specimen exercises the real
5
+ identity contract (id="card-<id>", class .kanban-card, data-<id_attr>/<zone_attr>),
6
+ not a bespoke stand-in. `card` is a plain Hash — the specimen has no models. The
7
+ id_attr / zone_attr flow through from the board so the same partial demonstrates
8
+ the primitive under both the default and a custom attribute naming.
9
+ %>
10
+ <%= render layout: "studio/board/card_shell",
11
+ locals: { id: card[:id], zone: card[:zone], id_attr: id_attr, zone_attr: zone_attr,
12
+ class: "card p-3 space-y-2 cursor-grab active:cursor-grabbing" } do %>
13
+ <div class="flex items-start gap-2">
14
+ <span class="text-muted select-none leading-none" aria-hidden="true">⠿</span>
15
+ <p class="text-sm font-semibold text-heading leading-snug flex-1"><%= card[:title] %></p>
16
+ </div>
17
+ <div class="flex items-center justify-between gap-2">
18
+ <%= render "components/badge", text: card[:repo], scheme: "neutral" %>
19
+ <span class="inline-flex h-6 w-6 items-center justify-center rounded-full text-2xs font-bold text-white shrink-0"
20
+ style="background: var(--color-cta)"><%= card[:who] %></span>
21
+ </div>
22
+ <% end %>
data/lib/studio/engine.rb CHANGED
@@ -4,6 +4,9 @@ module Studio
4
4
  app.config.assets.precompile += %w[
5
5
  studio/sticky_table_header.css
6
6
  studio/sticky_table_header.js
7
+ studio/canvas_confetti.js
8
+ studio/studio_confetti.js
9
+ studio/sortable.js
7
10
  ]
8
11
  end
9
12
 
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.21.0"
2
+ VERSION = "0.23.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.21.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -170,12 +170,16 @@ files:
170
170
  - README.md
171
171
  - app/assets/images/resend-favicon.png
172
172
  - app/assets/images/ses-favicon.png
173
+ - app/assets/javascripts/studio/canvas_confetti.js
174
+ - app/assets/javascripts/studio/sortable.js
173
175
  - app/assets/javascripts/studio/sticky_table_header.js
176
+ - app/assets/javascripts/studio/studio_confetti.js
174
177
  - app/assets/stylesheets/studio/sticky_table_header.css
175
178
  - app/assets/tailwind/studio_engine/engine-motion.css
176
179
  - app/assets/tailwind/studio_engine/engine.css
177
180
  - app/controllers/concerns/solana/session_auth.rb
178
181
  - app/controllers/concerns/studio/admin_models.rb
182
+ - app/controllers/concerns/studio/board/reorderable.rb
179
183
  - app/controllers/concerns/studio/error_handling.rb
180
184
  - app/controllers/concerns/studio/impersonation.rb
181
185
  - app/controllers/concerns/studio/link_consumption.rb
@@ -203,6 +207,7 @@ files:
203
207
  - app/mailers/application_mailer.rb
204
208
  - app/mailers/user_mailer.rb
205
209
  - app/models/concerns/sluggable.rb
210
+ - app/models/concerns/studio/board/rankable.rb
206
211
  - app/models/concerns/studio/broadcastable.rb
207
212
  - app/models/current.rb
208
213
  - app/models/error_log.rb
@@ -244,8 +249,10 @@ files:
244
249
  - app/views/sessions/_sso_continue.html.erb
245
250
  - app/views/sessions/new.html.erb
246
251
  - app/views/studio/_age_verify_assets.html.erb
252
+ - app/views/studio/_board_assets.html.erb
247
253
  - app/views/studio/_confirm_interstitial.html.erb
248
254
  - app/views/studio/_cropper_assets.html.erb
255
+ - app/views/studio/_leveling_activity_assets.html.erb
249
256
  - app/views/studio/admin_models/_arenas_table.html.erb
250
257
  - app/views/studio/admin_models/_teams_table.html.erb
251
258
  - app/views/studio/admin_models/index.html.erb
@@ -256,6 +263,9 @@ files:
256
263
  - app/views/studio/banners/_email_status_button.html.erb
257
264
  - app/views/studio/banners/_environment.html.erb
258
265
  - app/views/studio/banners/_impersonation.html.erb
266
+ - app/views/studio/board/_board.html.erb
267
+ - app/views/studio/board/_card_shell.html.erb
268
+ - app/views/studio/board/_column.html.erb
259
269
  - app/views/studio/email_images/index.html.erb
260
270
  - app/views/studio/links/confirm.html.erb
261
271
  - app/views/studio/local_emails/index.html.erb
@@ -266,11 +276,13 @@ files:
266
276
  - app/views/studio/modals/auth/_resend_footer.html.erb
267
277
  - app/views/studio/modals/blocks/_age_verify.html.erb
268
278
  - app/views/studio/modals/blocks/_card_header.html.erb
279
+ - app/views/studio/modals/blocks/_change_username.html.erb
269
280
  - app/views/studio/modals/blocks/_cta_redirect.html.erb
270
281
  - app/views/studio/modals/blocks/_digit_reel.html.erb
271
282
  - app/views/studio/modals/blocks/_entry_confirmed.html.erb
272
283
  - app/views/studio/modals/blocks/_error_card.html.erb
273
284
  - app/views/studio/modals/blocks/_free_entry_earned.html.erb
285
+ - app/views/studio/modals/blocks/_leveling_activity.html.erb
274
286
  - app/views/studio/modals/blocks/_onchain_success.html.erb
275
287
  - app/views/studio/modals/blocks/_processing_card.html.erb
276
288
  - app/views/studio/modals/blocks/_progress_countdown.html.erb
@@ -294,6 +306,7 @@ files:
294
306
  - app/views/style/_tasks.html.erb
295
307
  - app/views/style/_theme.html.erb
296
308
  - app/views/style/_tricks.html.erb
309
+ - app/views/style/board/_demo_card.html.erb
297
310
  - app/views/style/index.html.erb
298
311
  - app/views/style/modals/_age_verify.html.erb
299
312
  - app/views/style/modals/_auth.html.erb