studio-engine 0.127.0 → 0.129.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +161 -0
  3. data/README.md +9 -3
  4. data/app/assets/javascripts/studio/page_transition_reveal.js +1 -1
  5. data/app/assets/tailwind/studio_engine/engine.css +572 -37
  6. data/app/controllers/concerns/studio/error_handling.rb +17 -0
  7. data/app/controllers/studio/surveys_controller.rb +16 -3
  8. data/app/helpers/studio/loader_helper.rb +2 -2
  9. data/app/helpers/studio_icon_helper.rb +10 -0
  10. data/app/javascript/studio/agent_chat.js +88 -6
  11. data/app/javascript/studio/agent_chat_voice.js +59 -5
  12. data/app/javascript/studio/alpine_stores.js +80 -22
  13. data/app/javascript/studio/loader.js +130 -5
  14. data/app/javascript/studio/microphone_chip.js +28 -2
  15. data/app/javascript/studio/page_transitions.js +28 -19
  16. data/app/javascript/studio/survey.js +16 -3
  17. data/app/javascript/studio/theme_editor.js +4 -0
  18. data/app/javascript/studio/voice_recorder.js +18 -7
  19. data/app/models/studio/survey_response.rb +24 -17
  20. data/app/views/components/_geo_signpost.html.erb +1 -0
  21. data/app/views/components/_link_sidebar.html.erb +40 -12
  22. data/app/views/components/_sidebar_panel.html.erb +15 -5
  23. data/app/views/layouts/studio/_head.html.erb +18 -7
  24. data/app/views/layouts/studio/_smooth_load.html.erb +1 -0
  25. data/app/views/layouts/studio/component_preview.html.erb +1 -1
  26. data/app/views/registrations/new.html.erb +4 -3
  27. data/app/views/sessions/new.html.erb +4 -3
  28. data/app/views/studio/agent_chat/_chat.html.erb +2 -1
  29. data/app/views/studio/agent_chat/_turn.html.erb +3 -1
  30. data/app/views/studio/agent_chat/_voice.html.erb +28 -13
  31. data/app/views/studio/loader/_overlay.html.erb +4 -4
  32. data/app/views/style/_components.html.erb +2 -4
  33. data/app/views/style/_glass.html.erb +83 -0
  34. data/app/views/style/_host.html.erb +2 -10
  35. data/app/views/style/_modals.html.erb +38 -231
  36. data/app/views/style/_scrims.html.erb +2 -9
  37. data/app/views/style/_site_footer.html.erb +5 -13
  38. data/app/views/style/_tasks.html.erb +16 -27
  39. data/app/views/style/_theme.html.erb +56 -78
  40. data/app/views/style/_tricks.html.erb +13 -11
  41. data/app/views/style/_type_ladder.html.erb +33 -0
  42. data/app/views/style/index.html.erb +5 -1
  43. data/docs/SITE_FOOTER.md +1 -1
  44. data/lib/studio/agent_chat.rb +4 -2
  45. data/lib/studio/icons/README.md +12 -0
  46. data/lib/studio/icons/lucide/LICENSE +43 -0
  47. data/lib/studio/icons/lucide/activity.svg +15 -0
  48. data/lib/studio/icons/lucide/book-open.svg +16 -0
  49. data/lib/studio/icons/lucide/chart-column.svg +18 -0
  50. data/lib/studio/icons/lucide/diamond.svg +15 -0
  51. data/lib/studio/icons/lucide/door-stairwell.svg +19 -0
  52. data/lib/studio/icons/lucide/globe.svg +17 -0
  53. data/lib/studio/icons/lucide/hammer.svg +17 -0
  54. data/lib/studio/icons/lucide/inbox.svg +16 -0
  55. data/lib/studio/icons/lucide/key-round.svg +16 -0
  56. data/lib/studio/icons/lucide/layout-grid.svg +18 -0
  57. data/lib/studio/icons/lucide/log-out.svg +17 -0
  58. data/lib/studio/icons/lucide/message-circle.svg +15 -0
  59. data/lib/studio/icons/lucide/package.svg +18 -0
  60. data/lib/studio/icons/lucide/palette.svg +19 -0
  61. data/lib/studio/icons/lucide/plug.svg +18 -0
  62. data/lib/studio/icons/lucide/rocket.svg +18 -0
  63. data/lib/studio/icons/lucide/user.svg +16 -0
  64. data/lib/studio/icons.rb +52 -0
  65. data/lib/studio/loader.rb +7 -0
  66. data/lib/studio/sidebar_sections.rb +52 -4
  67. data/lib/studio/site_footer.rb +23 -2
  68. data/lib/studio/version.rb +1 -1
  69. data/lib/studio.rb +54 -2
  70. metadata +24 -1
@@ -155,16 +155,8 @@
155
155
  x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
156
156
  @age-verified.window="$store.dsModals.open('entry-confirmed', { txSignature: null, lobbyUrl: null, seedsEarned: 0 })">
157
157
  <div class="space-y-1">
158
- <h2 class="text-2xl font-bold text-heading">Modals</h2>
159
- <p class="text-muted text-sm">
160
- The shared modal host and the reusable modal partials, ported from Turf
161
- Monster into the engine. Each tile is a <strong>visual representation, not
162
- to scale</strong> &mdash; <strong>click a whole card</strong> to open the real
163
- modal (Enter/Space work too). The header <strong>Copy</strong> yields an
164
- agent-ready reference you can paste into a chat; option <strong>toggles</strong>
165
- configure the modal before you open it; and the card that matches the open
166
- modal <strong>glows</strong>.
167
- </p>
158
+ <h2 class="type-h3 text-heading">Modals</h2>
159
+ <p class="text-muted text-sm">Click a card to open the real modal.</p>
168
160
  </div>
169
161
 
170
162
  <%# ---- The section's own modal host (dsModals) --------------------------
@@ -461,23 +453,8 @@
461
453
  <%# ===================================================================== %>
462
454
  <section id="modals-auth" class="space-y-5">
463
455
  <div class="space-y-1">
464
- <h3 class="text-xl font-bold text-heading">Auth</h3>
465
- <p class="text-muted text-sm">
466
- <strong>The engine ships no sign-in card.</strong> A sign-in modal is a
467
- product decision &mdash; which methods, what terms, whose legal copy
468
- &mdash; so each app owns its own and cards it on <em>its</em> section of
469
- this guide, against the partial that actually renders in production.
470
- What the engine owns is the <strong>pieces</strong> that card is built
471
- from, and they are below.
472
- </p>
473
- <p class="text-muted text-sm">
474
- Until 2026-09-09 this section held a full auth modal
475
- (<code class="font-mono text-2xs">style/modals/_auth</code>). It was a
476
- <strong>mirror</strong> of turf-monster&rsquo;s card, and it drifted:
477
- reviewing it told you about a copy nobody shipped. It is gone, and these
478
- two fragments &mdash; which it happened to be the only renderer of &mdash;
479
- now stand on their own.
480
- </p>
456
+ <h3 class="type-h4 text-heading">Auth</h3>
457
+ <p class="text-muted text-sm">The engine ships no sign-in card, only these pieces.</p>
481
458
  </div>
482
459
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
483
460
  <%# email field — the one piece every app's auth card renders. %>
@@ -523,32 +500,8 @@
523
500
  cannot show, now that the wallet step lives with the wallet cards. %>
524
501
  <section id="modals-profile" class="space-y-5">
525
502
  <div class="space-y-1">
526
- <h3 class="text-xl font-bold text-heading">Profile</h3>
527
- <p class="text-muted text-sm">
528
- Who the account IS &mdash; the name it goes by, the birthday that decides
529
- eligibility, and the picture. The first three cards are asked at signup
530
- (a host walks them post-auth), the last three are reachable forever from
531
- the profile page; all six are engine primitives.
532
- </p>
533
- <p class="text-muted text-sm">
534
- <strong>Birthday and Age gate are one flow, deliberately split into two
535
- cards.</strong> The birthday card ASKS; when the answer is too young the
536
- age-gate card ANSWERS, carrying a countdown to the day they qualify, a way
537
- to watch instead, and a way back to fix a mistyped year. Until 2026-08-24
538
- that refusal lived inside the birthday card as red text beside a disabled
539
- button &mdash; a state with nothing to press. Open <strong>Birthday</strong>
540
- with <strong>Age validation</strong> on and submit any date to walk the
541
- handoff; switch the toggle off for the other mode, an app with no age bar
542
- that just wants the date.
543
- </p>
544
- <p class="text-muted text-sm">
545
- The avatar modals
546
- (<code class="font-mono text-2xs">studio/modals/_crop_photo</code>,
547
- <code class="font-mono text-2xs">studio/modals/_image_upload</code>)
548
- open live here &mdash;
549
- <code class="font-mono text-2xs">studio/cropper_assets</code> is rendered
550
- above, so cropper.js loads and the modals mount on this page's host.
551
- </p>
503
+ <h3 class="type-h4 text-heading">Profile</h3>
504
+ <p class="text-muted text-sm">The name, birthday and picture cards.</p>
552
505
  </div>
553
506
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
554
507
  <%= render layout: "style/modal_specimen", locals: {
@@ -568,11 +521,6 @@
568
521
  <%# the skip link that sits under the button, and is also a skip %>
569
522
  <span class="block h-1.5 w-14 mx-auto rounded" style="background: var(--color-text); opacity: .25"></span>
570
523
  </div>
571
- <p class="text-muted text-2xs mt-3">
572
- Saving or skipping dispatches
573
- <code class="font-mono">ds-onboarding-step-done</code> here rather than
574
- the app-level event, so the specimen cannot drive a real chain.
575
- </p>
576
524
  <% end %>
577
525
 
578
526
  <%# BIRTHDAY — the ask. ONE card, two modes, because the difference between
@@ -711,35 +659,14 @@
711
659
  <section id="modals-profile-leveling" class="space-y-5">
712
660
  <div class="space-y-1">
713
661
  <div class="flex flex-wrap items-center gap-3">
714
- <h3 class="text-xl font-bold text-heading">Profile Leveling</h3>
662
+ <h3 class="type-h4 text-heading">Profile Leveling</h3>
715
663
  <% if leveling_on %>
716
664
  <span class="badge" style="color: var(--color-success-ink); border-color: var(--color-success)">leveling on</span>
717
665
  <% else %>
718
666
  <span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">leveling off</span>
719
667
  <% end %>
720
668
  </div>
721
- <p class="text-muted text-sm">
722
- The profile leveling flow, ported from Turf Monster as ONE primitive
723
- (<code class="font-mono text-2xs">studio/modals/blocks/_leveling_activity</code>,
724
- with <code class="font-mono text-2xs">_change_username</code> as its username
725
- specialization). Walk all four states — <strong>Change Username &rarr; Great
726
- Username</strong>, then <strong>Join the Newsletter &rarr; Subscribed!</strong>
727
- — each "updated" card opens its modal straight at the success state, just like
728
- the Auth cards open a given step. Each card carries its <strong>own Leveling
729
- toggle</strong> (like the Auth card's method checkboxes): <strong>checked</strong>
730
- is the Turf Monster shape (the seeds bar + Free Entry Token progress),
731
- <strong>unchecked</strong> is the McRitchie Studio shape (plain input + Save,
732
- then a simple confirmation). Flip a card's toggle, then open it — so you can
733
- preview Change Username leveling-on beside Great Username leveling-off. Same
734
- modal id either way — the primitive reads
735
- <code class="font-mono text-2xs">leveling</code> at runtime, so there is no
736
- with/without fork and <strong>no "Quest N of N" counter</strong>. The save is an
737
- <strong>app-supplied callback</strong>
738
- (<code class="font-mono text-2xs">submit_url</code> + an opaque
739
- <code class="font-mono text-2xs">finalize_hook</code> for a wallet step) —
740
- <strong>no wallet, signing, or on-chain write lives in the engine</strong>.
741
- (These demos resolve locally.)
742
- </p>
669
+ <p class="text-muted text-sm">Each card&rsquo;s toggle switches leveling on or off before you open it.</p>
743
670
  </div>
744
671
  <%# The walk, left to right: Change Username -> Great Username -> Join Newsletter
745
672
  -> Subscribed. The two "updated" cards open the same id at props.celebrate. %>
@@ -865,42 +792,17 @@
865
792
  <section id="modals-web3" class="space-y-5">
866
793
  <div class="space-y-1">
867
794
  <div class="flex flex-wrap items-center gap-3">
868
- <h3 class="text-xl font-bold text-heading">Web3</h3>
795
+ <h3 class="type-h4 text-heading">Web3</h3>
869
796
  <% if web3_on %>
870
797
  <span class="badge" style="color: var(--color-success-ink); border-color: var(--color-success)">enabled on this app</span>
871
798
  <% else %>
872
799
  <span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">disabled on this app</span>
873
800
  <% end %>
874
801
  </div>
875
- <p class="text-muted text-sm">
876
- The wallet + on-chain modals, ported into the engine and gated by
877
- <code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
878
- <strong><%= web3_on ? "on" : "off" %></strong> here, so these render
879
- <%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
880
- <% unless web3_gem %>
881
- <strong>This app bundles no solana-studio</strong>, so the three cards it
882
- backs &mdash; Connect wallet and the two Sign Wallet states &mdash; are
883
- listed for reference but <strong>cannot be opened here</strong>. Nothing
884
- registers them, so a trigger would open an empty panel.
885
- <% end %>
886
- The cards run in the order a player meets them &mdash; <strong>connect a
887
- wallet, prove it, spend from it</strong>: Connect wallet &rarr; the two
888
- Sign Wallet states &rarr; Entry confirmed. The
889
- Connect-wallet picker ships its <strong>Phantom / Solflare / Backpack brand
890
- marks inline from the engine</strong>
891
- (<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
892
- so every app inherits them with no per-app icon files.
893
- The two <strong>Sign Wallet</strong> cards are a different kind of thing
894
- from the rest of this section: they render solana-studio's
895
- <code class="font-mono text-2xs">solana_studio/modals/_web3_step_up</code>
896
- &mdash; the <strong>REAL shared partial a host renders in production</strong>,
897
- not a style-guide copy of it &mdash; so what you review here is the
898
- shipped card, and it cannot drift from what users see. They also carry the
899
- <strong>standard web3 auth button</strong>: a wallet row (brand mark, the
900
- wallet&rsquo;s own name, Installed badge, chevron) rather than a filled
901
- CTA, glowing via <code class="font-mono text-2xs">pulse-cta</code> because
902
- it is the one thing to press.
903
- </p>
802
+ <p class="text-muted text-sm">The wallet and on-chain modals, in the order a player meets them.</p>
803
+ <% unless web3_gem %>
804
+ <p class="text-muted text-sm">This app bundles no solana-studio, so the three cards it backs cannot be opened here.</p>
805
+ <% end %>
904
806
  </div>
905
807
  <%# ONE ordered run, read left to right as a player meets it: get a wallet,
906
808
  prove it, spend from it. This carried three labelled h4 runs for a day
@@ -1035,35 +937,14 @@
1035
937
  <section id="modals-contest-entry" class="space-y-5">
1036
938
  <div class="space-y-1">
1037
939
  <div class="flex flex-wrap items-center gap-3">
1038
- <h3 class="text-xl font-bold text-heading">Contest entry &amp; eligibility</h3>
940
+ <h3 class="type-h4 text-heading">Contest entry &amp; eligibility</h3>
1039
941
  <% if age_gate_on %>
1040
942
  <span class="badge" style="color: var(--color-success-ink); border-color: var(--color-success)">age gate on</span>
1041
943
  <% else %>
1042
944
  <span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
1043
945
  <% end %>
1044
946
  </div>
1045
- <p class="text-muted text-sm">
1046
- The pre-entry gate and the full entry flow. The age bar is enforced HERE,
1047
- but its two cards &mdash;
1048
- <code class="font-mono text-2xs">studio/modals/blocks/_birthday</code> (the
1049
- ask) and <code class="font-mono text-2xs">_age_gate</code> (the refusal)
1050
- &mdash; are shown in the <strong>Profile</strong> section, because they are
1051
- about who the account is. Both are engine primitives gated by
1052
- <code class="font-mono text-2xs">Studio.feature?(:age_gate)</code>, and their
1053
- minimum age, jurisdiction label, endpoint, and legal copy are all
1054
- <strong>app-supplied</strong> (the engine hardcodes no policy).
1055
- </p>
1056
- <p class="text-muted text-sm">
1057
- <strong>The entry flow itself has no specimen here, on purpose.</strong>
1058
- Packs, pricing, payment rails and every on-chain write are the app&rsquo;s
1059
- &mdash; the engine owns none of them, and the walked purchase sequence that
1060
- used to sit in this section was a <strong>mirror</strong> of turf-monster&rsquo;s
1061
- cards. It drifted, and it demonstrated a payment face that had stopped
1062
- rendering in production. It was retired on 2026-09-09; turf-monster cards
1063
- the live flow on its own section of this guide.
1064
- <strong>What stays is the map below</strong>, because the shape of the flow
1065
- is the part an app should not have to rediscover.
1066
- </p>
947
+ <p class="text-muted text-sm">The app owns the entry flow, so only its map lives here.</p>
1067
948
  <%# The web2/web3 finding — a real divergence, stated honestly, not invented.
1068
949
  THIS BOX IS THE SURVIVING HALF of style/modals/_entry_tokens, whose header
1069
950
  carried the same model in a comment. When that mirror was deleted the
@@ -1071,28 +952,12 @@
1071
952
  nobody opens — the two sentences about what a token IS, and about who
1072
953
  signs the mint, came across in that move. %>
1073
954
  <div class="mt-2 rounded-lg border border-subtle bg-surface p-3 text-xs text-secondary leading-snug">
1074
- <p class="text-heading font-semibold mb-1">Web2 vs web3 — the honest map</p>
1075
- <p class="mb-1">
1076
- The two funding paths <strong>diverge at the front-end and the entry-submit
1077
- endpoint, then converge</strong>. <strong>Web2</strong> (card / Stripe /
1078
- Coinflow / PayPal) mints one prepaid entry token per credit on-chain after
1079
- payment clears, then <strong>consumes</strong> one token to enter.
1080
- <strong>Web3</strong> (wallet / USDC on-chain) funds USDC directly and
1081
- <strong>mints no token</strong> &mdash; the mint&nbsp;&rarr;&nbsp;consume prelude
1082
- is web2-only.
1083
- </p>
1084
- <p class="mb-1">
1085
- An entry token is a <strong>program-owned prepaid entry credit</strong>
1086
- recorded on-chain as its own account &mdash; <strong>not</strong> a fungible
1087
- SPL token sitting in the player&rsquo;s wallet, which is the wrong mental
1088
- model to carry into the code. And the mint is <strong>server-signed once
1089
- the payment clears</strong>, never on the buyer&rsquo;s click: the two
1090
- on-chain moments a web2 entry has are that mint and the later consume.
1091
- </p>
1092
- <p>
1093
- Both create the <strong>same on-chain Entry PDA</strong> and resolve to the
1094
- <strong>same “Entry Confirmed” card</strong>.
1095
- </p>
955
+ <p class="text-heading font-semibold mb-1">Web2 vs web3</p>
956
+ <p class="mb-1">Web2 (card, Stripe, Coinflow, PayPal) mints one prepaid entry token per credit after payment clears, then consumes one to enter.</p>
957
+ <p class="mb-1">Web3 (wallet, USDC) funds USDC directly and mints no token, so the mint and consume steps are web2-only.</p>
958
+ <p class="mb-1">An entry token is a program-owned prepaid credit on-chain, not a wallet SPL token.</p>
959
+ <p class="mb-1">The mint is server-signed once payment clears, never on the buyer&rsquo;s click.</p>
960
+ <p>Both paths create the same on-chain Entry PDA and end on the same Entry Confirmed card.</p>
1096
961
  </div>
1097
962
  </div>
1098
963
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -1133,11 +998,7 @@
1133
998
  <%# ===================================================================== %>
1134
999
  <section id="modals-system-status" class="space-y-5">
1135
1000
  <div class="space-y-1">
1136
- <h3 class="text-xl font-bold text-heading">System &amp; status</h3>
1137
- <p class="text-muted text-sm">
1138
- The reusable card blocks under <code class="font-mono text-2xs">studio/modals/blocks/</code> &mdash;
1139
- the studio-canonical processing, success, error, and countdown states.
1140
- </p>
1001
+ <h3 class="type-h4 text-heading">System &amp; status</h3>
1141
1002
  </div>
1142
1003
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
1143
1004
  <%= render layout: "style/modal_specimen", locals: {
@@ -1283,12 +1144,8 @@
1283
1144
  <%# ===================================================================== %>
1284
1145
  <section id="modals-templates" class="space-y-5">
1285
1146
  <div class="space-y-1">
1286
- <h3 class="text-xl font-bold text-heading">Templates</h3>
1287
- <p class="text-muted text-sm">
1288
- Reusable archetypes under <code class="font-mono text-2xs">studio/modals/templates/</code> &mdash;
1289
- the copy-from reference cards for a new modal (wizard, form, action,
1290
- status, success).
1291
- </p>
1147
+ <h3 class="type-h4 text-heading">Templates</h3>
1148
+ <p class="text-muted text-sm">Copy from these when you build a new modal.</p>
1292
1149
  </div>
1293
1150
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
1294
1151
  <% [
@@ -1318,25 +1175,14 @@
1318
1175
  <section id="modals-rewards" class="space-y-5">
1319
1176
  <div class="space-y-1">
1320
1177
  <div class="flex flex-wrap items-center gap-3">
1321
- <h3 class="text-xl font-bold text-heading">Rewards</h3>
1178
+ <h3 class="type-h4 text-heading">Rewards</h3>
1322
1179
  <% if leveling_on %>
1323
1180
  <span class="badge" style="color: var(--color-success-ink); border-color: var(--color-success)">enabled on this app</span>
1324
1181
  <% else %>
1325
1182
  <span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">disabled on this app</span>
1326
1183
  <% end %>
1327
1184
  </div>
1328
- <p class="text-muted text-sm">
1329
- The seeds bar + level-up celebrations, gated by
1330
- <code class="font-mono text-2xs">Studio.feature?(:leveling)</code> — an
1331
- app-level setting <strong>independent of :web3</strong> (some apps have
1332
- Solana but no leveling). Leveling is <strong><%= leveling_on ? "on" : "off" %></strong>
1333
- here, so these render
1334
- <%= leveling_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
1335
- The seed counts, level, and free entry are <strong>app-supplied props</strong>
1336
- (<code class="font-mono text-2xs">seedsEarned</code>,
1337
- <code class="font-mono text-2xs">seedsTotal</code>, …) — the engine renders
1338
- the bar, the app feeds the numbers.
1339
- </p>
1185
+ <p class="text-muted text-sm">The seeds bar and level-up celebrations.</p>
1340
1186
  </div>
1341
1187
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
1342
1188
  <%= render layout: "style/modal_specimen", locals: {
@@ -1395,28 +1241,17 @@
1395
1241
  <%# ===================================================================== %>
1396
1242
  <section id="modals-motion-registry" class="space-y-5">
1397
1243
  <div class="space-y-1">
1398
- <h3 class="text-xl font-bold text-heading">Enter &amp; leave animations</h3>
1399
- <p class="text-muted text-sm">
1400
- The per-modal entrance/exit registry &mdash;
1401
- <code class="font-mono text-2xs">window.ModalAnimations</code>, published by
1402
- <code class="font-mono text-2xs">studio/modals/_host</code>. Any modal opens with
1403
- <code class="font-mono text-2xs">open(id, { enterAnim, exitAnim })</code>; omit either
1404
- and it uses <code class="font-mono text-2xs">pop</code>. Every control below is
1405
- <strong class="text-heading font-semibold">generated from the live registry</strong>,
1406
- so registering a new animation surfaces it here with no edit to this page. Demo card:
1407
- <code class="font-mono text-2xs">email-change-pending</code>.
1408
- </p>
1244
+ <h3 class="type-h4 text-heading">Enter &amp; leave animations</h3>
1245
+ <p class="text-muted text-sm">Every control comes from the live animation registry.</p>
1409
1246
  </div>
1410
1247
 
1411
1248
  <div class="grid gap-4 sm:grid-cols-2">
1412
1249
  <%# One control per registered ENTER key, built by the script below. %>
1413
1250
  <div class="card p-5 space-y-3">
1414
1251
  <h4 class="text-heading font-semibold text-sm">
1415
- Enter <span class="text-muted text-xs font-normal">&mdash; watch it arrive</span>
1252
+ Enter
1416
1253
  </h4>
1417
- <p class="text-secondary text-xs">
1418
- Opens the demo card with that entrance. Dismiss with Escape, click-outside, or Close.
1419
- </p>
1254
+ <p class="text-secondary text-xs">Opens the demo card.</p>
1420
1255
  <div id="modal-anim-enter-buttons" class="flex flex-wrap gap-2"></div>
1421
1256
  </div>
1422
1257
 
@@ -1424,18 +1259,15 @@
1424
1259
  auto-closes a beat later so the exit keyframe is what you watch. %>
1425
1260
  <div class="card p-5 space-y-3">
1426
1261
  <h4 class="text-heading font-semibold text-sm">
1427
- Leave <span class="text-muted text-xs font-normal">&mdash; watch it dismiss</span>
1262
+ Leave
1428
1263
  </h4>
1429
- <p class="text-secondary text-xs">
1430
- Opens with a plain entrance, then closes itself so the exit keyframe is the star.
1431
- </p>
1264
+ <p class="text-secondary text-xs">Opens, then closes itself.</p>
1432
1265
  <div id="modal-anim-exit-buttons" class="flex flex-wrap gap-2"></div>
1433
1266
  </div>
1434
1267
 
1435
1268
  <%# The full matrix: any entrance crossed with any exit. %>
1436
1269
  <div class="card p-5 space-y-3 sm:col-span-2">
1437
1270
  <h4 class="text-heading font-semibold text-sm">Combine</h4>
1438
- <p class="text-secondary text-xs">Pick any entrance and exit, then run the round trip.</p>
1439
1271
  <div class="flex flex-wrap items-end gap-3">
1440
1272
  <label class="text-xs text-secondary">Enter
1441
1273
  <select id="modal-anim-enter-select"
@@ -1461,19 +1293,8 @@
1461
1293
  <%# ===================================================================== %>
1462
1294
  <section id="modals-stack-mechanics" class="space-y-5">
1463
1295
  <div class="space-y-1">
1464
- <h3 class="text-xl font-bold text-heading">Stack behaviour</h3>
1465
- <p class="text-muted text-sm">
1466
- The engine modal-stack mechanics, poked at directly on
1467
- <code class="font-mono text-2xs">$store.dsModals</code>:
1468
- <code class="font-mono text-2xs">advance()</code>, dismissibility, the
1469
- minimum-visible-duration floor, and LIFO stacking. They all run on one
1470
- throwaway vehicle
1471
- (<code class="font-mono text-2xs">style/modals/_ds_stack_demo</code>) built
1472
- from engine blocks &mdash; <strong>the mechanics are the subject, the card
1473
- is not</strong>. Until 2026-09-09 they ran on a copy of turf-monster&rsquo;s
1474
- on-chain card, which is how six unrelated demos came to depend on one
1475
- consumer&rsquo;s markup.
1476
- </p>
1296
+ <h3 class="type-h4 text-heading">Stack behaviour</h3>
1297
+ <p class="text-muted text-sm">The modal stack&rsquo;s rules, shown on a throwaway card.</p>
1477
1298
  </div>
1478
1299
 
1479
1300
  <div class="grid gap-4 sm:grid-cols-2">
@@ -1485,12 +1306,7 @@
1485
1306
  vehicle carries three faces on one. %>
1486
1307
  <div class="card p-5 space-y-3">
1487
1308
  <h4 class="text-heading font-semibold text-sm">In-flow advance()</h4>
1488
- <p class="text-secondary text-xs">
1489
- <code class="font-mono text-2xs">advance(patch)</code> patches the OPEN
1490
- entry rather than pushing a new one, so the card&rsquo;s Alpine scope
1491
- survives the transition. Watch the timed pair change a card that never
1492
- left the stack.
1493
- </p>
1309
+ <p class="text-secondary text-xs">Patches the open card, so its state survives.</p>
1494
1310
  <div class="flex flex-wrap gap-2">
1495
1311
  <button type="button" class="btn btn-outline btn-sm"
1496
1312
  data-studio-action="click->style-modals#demo" data-style-modals-name-param="processing">Processing</button>
@@ -1509,10 +1325,7 @@
1509
1325
  cannot orphan an in-flight signature. %>
1510
1326
  <div class="card p-5 space-y-3">
1511
1327
  <h4 class="text-heading font-semibold text-sm">Dismissibility</h4>
1512
- <p class="text-secondary text-xs">
1513
- A processing card passes <code class="font-mono text-2xs">dismissible: false</code>, so
1514
- Escape and click-outside are both suppressed until it resolves.
1515
- </p>
1328
+ <p class="text-secondary text-xs">A card with <code class="font-mono text-2xs">dismissible: false</code> ignores Escape and click-outside.</p>
1516
1329
  <div class="flex flex-wrap gap-2">
1517
1330
  <button type="button" class="btn btn-outline btn-sm"
1518
1331
  data-studio-action="click->style-modals#demo" data-style-modals-name-param="processing">Try Escape (locked)</button>
@@ -1524,10 +1337,7 @@
1524
1337
  <%# The anti-flicker floor from studio/modal_host (window.StudioModals.holdAtLeast). %>
1525
1338
  <div class="card p-5 space-y-3">
1526
1339
  <h4 class="text-heading font-semibold text-sm">Hold at least</h4>
1527
- <p class="text-secondary text-xs">
1528
- <code class="font-mono text-2xs">StudioModals.holdAtLeast(ms)</code> keeps a fast
1529
- operation from flashing the spinner. Run both and watch the difference.
1530
- </p>
1340
+ <p class="text-secondary text-xs">Keeps a fast operation from flashing the spinner.</p>
1531
1341
  <div class="flex flex-wrap gap-2">
1532
1342
  <button type="button" class="btn btn-primary btn-sm"
1533
1343
  data-studio-action="click->style-modals#demo" data-style-modals-name-param="fastWithHold">Fast op, hold 1500ms</button>
@@ -1539,10 +1349,7 @@
1539
1349
  <%# LIFO: open pushes, close pops back to what was underneath. %>
1540
1350
  <div class="card p-5 space-y-3">
1541
1351
  <h4 class="text-heading font-semibold text-sm">Stacking</h4>
1542
- <p class="text-secondary text-xs">
1543
- LIFO &mdash; a second <code class="font-mono text-2xs">open()</code> pushes the first
1544
- card behind it, and <code class="font-mono text-2xs">close()</code> pops back to it.
1545
- </p>
1352
+ <p class="text-secondary text-xs">A second <code class="font-mono text-2xs">open()</code> stacks on top, and <code class="font-mono text-2xs">close()</code> pops back.</p>
1546
1353
  <div class="flex flex-wrap gap-2">
1547
1354
  <button type="button" class="btn btn-primary btn-sm"
1548
1355
  data-studio-action="click->style-modals#demo" data-style-modals-name-param="stackTwo">Open two</button>
@@ -37,15 +37,8 @@
37
37
  %>
38
38
  <section id="scrims" class="space-y-5" data-scrim-group>
39
39
  <div class="space-y-1">
40
- <h3 class="text-xl font-bold text-heading">Tints &amp; shades</h3>
41
- <p class="text-muted text-sm max-w-2xl">
42
- A <strong>shade</strong> lays the dark base over media and light ink reads on it; a
43
- <strong>tint</strong> lays the light base over media and dark ink reads on it.
44
- <code class="font-mono text-2xs">.scrim</code> picks by theme;
45
- <code class="font-mono text-2xs">.scrim-tint</code> / <code class="font-mono text-2xs">.scrim-shade</code>
46
- force one. Put the tone on the shared parent so the wash and the
47
- <code class="font-mono text-2xs">.scrim-ink</code> text both take it.
48
- </p>
40
+ <h3 class="type-h4 text-heading">Tints &amp; shades</h3>
41
+ <p class="text-muted text-sm max-w-2xl">A shade darkens media under light ink, and a tint lightens it under dark ink.</p>
49
42
  </div>
50
43
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
51
44
  <% specimens.each do |s| %>
@@ -35,13 +35,8 @@
35
35
  %>
36
36
  <section id="site-footer" class="space-y-5" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)">
37
37
  <div class="space-y-1">
38
- <h3 class="text-xl font-bold text-heading">Site footer and booking</h3>
39
- <p class="text-muted text-sm max-w-2xl">
40
- One layout line renders the public site's footer from
41
- <code class="font-mono text-2xs">config.site_footer</code>; every part is optional. With
42
- <code class="font-mono text-2xs">config.booking_url</code> the same pages get Google Calendar's
43
- booking page inline, in a popup and on <code class="font-mono text-2xs">/schedule</code>.
44
- </p>
38
+ <h3 class="type-h4 text-heading">Site footer and booking</h3>
39
+ <p class="text-muted text-sm max-w-2xl">One layout line renders the footer, and <code class="font-mono text-2xs">config.booking_url</code> turns on booking.</p>
45
40
  <p class="text-muted text-sm" data-site-footer-status>
46
41
  This app:
47
42
  footer <strong class="text-body"><%= own_facts ? "declared" : (Studio.site_footer_enabled? ? "default (not declared)" : "off") %></strong> &middot;
@@ -55,10 +50,7 @@
55
50
  <%= render layout: "style/specimen",
56
51
  locals: { klass: "studio_site_footer",
57
52
  usage: %(studio_site_footer # one line, at the end of the layout body) } do %>
58
- <p class="text-sm text-secondary text-center">
59
- The footer, where it should show: every page for a visitor, listed controllers when signed in.
60
- Previewed below.
61
- </p>
53
+ <p class="text-sm text-secondary text-center">Previewed below.</p>
62
54
  <% end %>
63
55
 
64
56
  <%= render layout: "style/specimen",
@@ -74,10 +66,10 @@
74
66
 
75
67
  <% [ [ "studio_booking_frame", nil, "whole",
76
68
  %(studio_booking_frame # whole at rest: the default, and crop: false),
77
- "Whole at rest, the default. Google's page as Google draws it: its header, the slot picker, its credit lines." ],
69
+ "Whole at rest, the default." ],
78
70
  [ "config.booking_crop", sample_crop, "cropped",
79
71
  %(config.booking_crop = { top: 200, bottom: 600, frame_height: 720 } # bin/booking-crop-measure prints yours),
80
- "Cropped at rest to the slot picker, from numbers measured on the app's own schedule. Opens to the whole page when it is used." ]
72
+ "Cropped to the slot picker at rest." ]
81
73
  ].each do |klass, crop, state, usage, caption| %>
82
74
  <%= render layout: "style/specimen",
83
75
  locals: { klass: klass, disabled: !booking, disabled_label: "no booking_url", usage: usage } do %>
@@ -65,15 +65,8 @@
65
65
  %>
66
66
  <section id="tasks" class="space-y-8" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)">
67
67
  <div class="space-y-1">
68
- <h2 class="text-2xl font-bold text-heading">Tasks</h2>
69
- <p class="text-muted text-sm max-w-2xl">
70
- The shared board primitive that every board builds from &mdash; columns, cards, the
71
- <code class="font-mono text-2xs">stage-*</code> badge palette, and the drag / optimistic
72
- move + reorder. McRitchie Studio's <code class="font-mono text-2xs">/deployments</code> board is
73
- the top-of-line; the specimen below is the <strong>real</strong>
74
- <code class="font-mono text-2xs">studio/board/board</code> primitive in
75
- <code class="font-mono text-2xs">demo: true</code> mode &mdash; drag a card, no save.
76
- </p>
68
+ <h2 class="type-h3 text-heading">Tasks</h2>
69
+ <p class="text-muted text-sm max-w-2xl">The real board primitive in demo mode, so a drag saves nothing.</p>
77
70
  </div>
78
71
 
79
72
  <%# ---- The LIVE primitive (demo mode) --------------------------------- %>
@@ -81,7 +74,7 @@
81
74
  <div class="space-y-3">
82
75
  <div class="flex items-center gap-2">
83
76
  <span class="badge" style="color: var(--color-success-ink); border-color: var(--color-success)">Live</span>
84
- <p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> &middot; demo &mdash; drag a card between columns</p>
77
+ <p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> &middot; demo</p>
85
78
  </div>
86
79
  <%= render "studio/board/board",
87
80
  demo: true,
@@ -120,7 +113,7 @@
120
113
  data-test="chrome-archived-toggle">
121
114
  <span x-text="state.showArchived ? 'Hide archived' : 'Show archived'">Show archived</span>
122
115
  </button>
123
- <span class="label-upper"><code class="font-mono text-2xs">header_slot</code> chrome &middot; binds to <code class="font-mono text-2xs">state.showArchived</code> &mdash; no outer wrapper</span>
116
+ <span class="label-upper"><code class="font-mono text-2xs">header_slot</code> &middot; binds to <code class="font-mono text-2xs">state.showArchived</code></span>
124
117
  </div>
125
118
  <% end %>
126
119
  <% chrome_kickoff = capture do %>
@@ -150,7 +143,7 @@
150
143
  <div class="space-y-3">
151
144
  <div class="flex items-center gap-2">
152
145
  <span class="badge" style="color: var(--color-primary); border-color: var(--color-primary)">Enriched</span>
153
- <p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> &middot; 0.28.0 chrome &mdash; coloured counts, toggle-able column, archive re-parent</p>
146
+ <p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> &middot; coloured counts, toggle-able column, archive re-parent</p>
154
147
  </div>
155
148
  <%= render "studio/board/board",
156
149
  demo: true,
@@ -200,7 +193,7 @@
200
193
  <div class="space-y-3">
201
194
  <div class="flex items-center gap-2">
202
195
  <span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">Grid + Lock</span>
203
- <p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> &middot; DG (0.29.0) &mdash; two-level side&rarr;position grid, reorder within a lane, &#128274; pinned starters stay put</p>
196
+ <p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> &middot; two-level grid, reorder within a lane, &#128274; pinned starters</p>
204
197
  </div>
205
198
  <%= render "studio/board/board",
206
199
  demo: true,
@@ -246,17 +239,13 @@
246
239
 
247
240
  <%# ---- The stage palette ---------------------------------------------- %>
248
241
  <div class="space-y-3">
249
- <p class="label-upper">Stage palette — the pipeline spine</p>
242
+ <p class="label-upper">Stage palette</p>
250
243
  <div class="flex flex-wrap gap-2">
251
244
  <% %w[stage-fresh stage-shaping stage-structured stage-refined stage-cohered stage-shipped stage-closed].each do |stage| %>
252
245
  <span data-stage="<%= stage %>"><%= render "components/badge", text: stage.sub("stage-", ""), scheme: stage %></span>
253
246
  <% end %>
254
247
  </div>
255
- <p class="text-muted text-xs">
256
- Seven stage roles from <code class="font-mono text-2xs">components/_badge</code>. Every pipeline
257
- (tasks, deployments, and any future board) draws its columns from this one palette, so a card
258
- reads the same wherever it lives.
259
- </p>
248
+ <p class="text-muted text-xs">Every pipeline draws its columns from this palette.</p>
260
249
  </div>
261
250
 
262
251
  <%# ---- Standard vs custom -------------------------------------------- %>
@@ -267,12 +256,12 @@
267
256
  <p class="text-heading font-semibold text-sm">What every board inherits</p>
268
257
  </div>
269
258
  <ul class="text-secondary text-sm space-y-1 list-disc list-inside">
270
- <li>Column layout + horizontal scroll with faded edges</li>
271
- <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>
272
- <li>Card chrome (<code class="font-mono text-2xs">.card</code>) and the drag / rank handle</li>
259
+ <li>Column layout and horizontal scroll</li>
260
+ <li>The <code class="font-mono text-2xs">stage-*</code> badges and count chips</li>
261
+ <li>Card chrome and the drag handle</li>
273
262
  <li>Empty-column drop state</li>
274
- <li>Chrome-state home (<code class="font-mono text-2xs">board_state</code> + <code class="font-mono text-2xs">header_slot</code>) &mdash; filters/toggles without an outer wrapper</li>
275
- <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>
263
+ <li>Filters and toggles with no outer wrapper</li>
264
+ <li>Toggle-able columns, a kickoff slot and archive re-parent</li>
276
265
  </ul>
277
266
  </div>
278
267
  <div class="card p-5 space-y-2">
@@ -282,9 +271,9 @@
282
271
  </div>
283
272
  <ul class="text-secondary text-sm space-y-1 list-disc list-inside">
284
273
  <li>Which stages become columns</li>
285
- <li>The card body — fields, chips, avatars, live timers</li>
286
- <li>Filter/toggle logic + header actions, composed onto the shared chrome state</li>
287
- <li>Live-update wiring (Turbo Streams on the deploy board)</li>
274
+ <li>The card body</li>
275
+ <li>Filter logic and header actions</li>
276
+ <li>Live updates</li>
288
277
  </ul>
289
278
  </div>
290
279
  </div>