studio-engine 0.70.0 → 0.71.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1c217135776ee0fcd67dd4ed2e9a72f1aedf5260ac23cfc23615ab83754133a
4
- data.tar.gz: 42878ae8f85ed0c911b99c940e3c5094ca7329fed4bbb727ebfa4466bba99dfb
3
+ metadata.gz: b84ed97df94f71d6135899621e982649f6c80b41d5821b12842ab5c0e6d47ce7
4
+ data.tar.gz: 2b1af46db99092b30d9049d2afbbefa9cc810878bcbc376d9e04f780cc6e7a05
5
5
  SHA512:
6
- metadata.gz: d72745a5c54559b745fc0fefe1ac1c45b3921efeccbe5769f3b1da5ceaa1e11463c317984847fc1300fb0387610c33c4f0c7504e279871630f5535db563c935e
7
- data.tar.gz: 16be22a876c99c848169a2dcd551593ff4b8c377427acd9d84c04bf732847d261422c79d9b7f30397bf8b04c2d13a894999cc542a95bb1037f9deeaea0c5c32a
6
+ metadata.gz: 294aa380598143d9f79592e2b0e62bab038dbb886b374ce765ff9fdf0da349e460049ae284e397f234647f3b9dfdea24263cd8ed31bf74bbe00d1e14e1bbf49f
7
+ data.tar.gz: d119392c02adb20753485b0b6efcaf01e3c3f3ea0aa26f9b5e36fe1f3de3fb4df77ede836f0a79ec937bd556ad14a9317078fa25f028d890e27f531e74ad8f37
data/CHANGELOG.md CHANGED
@@ -6,6 +6,39 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
6
6
 
7
7
  ### Added
8
8
 
9
+ - **The living style guide grows a HOST SECTION seam.** `/admin/style` rendered
10
+ four hard-coded engine sections (Theme, Modals, Tricks, Tasks) with no way for
11
+ a consuming app to contribute one of its own. An app now defines
12
+ **`app/views/style/host/_modals.html.erb`** and the guide grows a fifth
13
+ section for that app's own modals, between Modals and Tricks, plus its pill in
14
+ the sticky section nav. Found by the same three-term
15
+ `lookup_context.exists?("modals", ["style/host"], true)` the modal host uses
16
+ for `modals/_host_extras` — one convention in this codebase, not two — so
17
+ defining the file IS the registration: no initializer, no locals, no list to
18
+ append to.
19
+
20
+ **The gem keeps dictating the structure.** `style/_host.html.erb` renders the
21
+ section element, the `#host-modals` anchor, the heading (`Studio.app_name`),
22
+ the intro line and the Alpine scope; the app supplies specimens only. An app
23
+ that ships no such partial gets **nothing** — no pill, no heading, no
24
+ container — and that is asserted as a whole-document comparison rather than a
25
+ handful of refutes: the page an app WITH a section gets, minus that section
26
+ and its pill, must equal the page a base app gets. Five of the six apps
27
+ mounting this engine will ship no host section, and their page is unchanged.
28
+
29
+ **A host specimen drives `$store.modals`, not `dsModals`, and the distinction
30
+ is the whole point.** The guide's own Modals section needs a page-scoped store
31
+ because its specimens are the ENGINE's ids, which an app's layout host has no
32
+ registration for — pushing one there paints an empty card behind the specimen.
33
+ An app's own ids ARE registered in its layout host, so a host section opens
34
+ the **real** production card with no mirror, no second registration list and
35
+ no iframe. The doc comment on `style/_host.html.erb` is the consumer contract
36
+ and says so outright, because a reader who assumes `dsModals` builds that
37
+ mirror instead — a second copy of every card, free to drift from the one the
38
+ app ships, which is the opposite of what a living style guide is for.
39
+
40
+ **The seam ships no host section itself.** The first consumer is
41
+ turf-monster's own `style/host/_modals`, which lands as its own task.
9
42
  - **A `required` mode on the shared first-name card.**
10
43
  `studio/modals/onboarding/_first_name` grows one local. `required: true`
11
44
  renders the card with BOTH skip affordances gone — the "Skip for now" button
data/README.md CHANGED
@@ -449,6 +449,48 @@ than assuming drop-in equivalence:
449
449
  the replacement lands after the 220ms slide-out (previously an immediate
450
450
  top-of-stack assignment).
451
451
 
452
+ ### Style guide — your app's own section
453
+
454
+ `/admin/style` is the engine's living style guide, and every section on it is
455
+ the engine's: Theme, Modals, Tricks, Tasks. An app grows a **fifth section, its
456
+ own**, by defining one partial:
457
+
458
+ ```erb
459
+ <%# app/views/style/host/_modals.html.erb %>
460
+ <div class="card p-4 space-y-3">
461
+ <p class="label-upper">Wallet setup</p>
462
+ <button type="button" class="btn btn-primary btn-sm"
463
+ @click="$store.modals.open('wallet-setup', { returnUrl: window.location.href })">
464
+ Open
465
+ </button>
466
+ </div>
467
+ ```
468
+
469
+ That is the whole registration — the same optional-partial convention as
470
+ `modals/_host_extras`, resolved by the same three-term `lookup_context.exists?`.
471
+ An app that ships no such file gets no section, no heading, and no nav pill; its
472
+ page is byte-for-byte what it was.
473
+
474
+ **The gem keeps dictating the structure.** It renders the section element, the
475
+ `#host-modals` anchor, the heading (your `Studio.app_name`), the intro line, and
476
+ the sticky nav pill, and it places the section between Modals and Tricks. Your
477
+ partial supplies specimens only — no `<section>`, no `<h2>`, nothing to register
478
+ in a nav.
479
+
480
+ **Drive `$store.modals`, not `dsModals`.** The guide's own Modals section stands
481
+ up a page-scoped store because its specimens are the *engine's* ids, and pushing
482
+ an id your layout host has never registered opens that host onto an empty card.
483
+ Your own ids are already registered there, so
484
+ `$store.modals.open(id, props)` renders the **real** card, in production chrome,
485
+ with production behavior. There is nothing to mirror and no second registration
486
+ list to keep in sync — building one is the mistake this note exists to prevent.
487
+ This assumes your layout mounts the shared host. Not every consuming app does
488
+ (see "Page-scoped hosts" above); where none is mounted `$store.modals` is
489
+ undefined, so mount `studio/modals/host` before writing a trigger.
490
+
491
+ Full contract, including what the partial may assume: the doc comment at the top
492
+ of `app/views/style/_host.html.erb`.
493
+
452
494
  ### User nav slots
453
495
 
454
496
  `components/_user_nav.html.erb` renders the right-side navbar user section.
@@ -0,0 +1,111 @@
1
+ <%# The HOST section of the living style guide (admin/style).
2
+
3
+ THE ONE SEAM a consuming app has for growing its own specimens onto this
4
+ page. Every other section on this page is the gem's. This one is the app's,
5
+ and the gem still dictates its structure: the section element, the anchor,
6
+ the heading, the intro line, and the nav pill above are all rendered HERE,
7
+ from the engine. The app supplies content only.
8
+
9
+ "Host" means the CONSUMING APP, the same sense it carries in the modal
10
+ host's own seam (modals/_host_extras). It does NOT mean the modal-host
11
+ shell, which is studio/modals/_host - a different file and a different
12
+ meaning of the word.
13
+
14
+
15
+ THE CONTRACT - what a consuming app writes
16
+ ------------------------------------------
17
+ Define ONE partial in the app:
18
+
19
+ app/views/style/host/_modals.html.erb
20
+
21
+ That is the whole registration. There is no initializer, no locals, no
22
+ list to append to, and nothing to render from the app's own layout. The
23
+ guide finds the partial by CONVENTION, exactly the way the modal host
24
+ finds modals/_host_extras:
25
+
26
+ lookup_context.exists?("modals", ["style/host"], true)
27
+
28
+ - the same three-term form: name, prefixes, partial. That call lives in
29
+ style/index.html.erb, which owns it because the sticky section nav needs
30
+ the same answer the render does. An app that ships no such partial gets no
31
+ pill, no heading, no container, and no bytes: the guide is unchanged.
32
+
33
+ WHY THIS PATH. A directory (style/host/) rather than a flat name in the
34
+ gem's own style/ prefix, because the gem already ships style/_modals,
35
+ style/_theme, style/_tricks and style/_tasks - an app file dropped beside
36
+ them shadows a section by name, and the failure is silent. Nothing of the
37
+ gem's lives under style/host/, so an app cannot collide with the engine
38
+ there. Naming the partial for the SECTION it contributes (modals) rather
39
+ than for the seam leaves room for a later style/host/_tricks without
40
+ renaming what is already shipped.
41
+
42
+
43
+ WHAT THE PARTIAL MAY ASSUME
44
+ ---------------------------
45
+ - It renders INSIDE this section element, already anchored, spaced and
46
+ headed. So it supplies cards and prose only: no section tag of its own,
47
+ no h2, no scroll-margin, no nav entry.
48
+ - The engine component layer (engine.css) and motion layer
49
+ (engine-motion.css) are loaded, so card, btn, badge, label-upper, the
50
+ role tokens and the motion/effect primitives all style for real.
51
+ - Alpine is running and this section carries an x-data scope, so a
52
+ specimen can use x-on / at-click bindings directly. A plain onclick
53
+ calling Alpine.store works too.
54
+ - NOT that a shared modal host exists. Only an app whose layout renders
55
+ studio/modals/host has Alpine.store("modals") at all: mcritchie-studio
56
+ and turf-monster mount it; acquisition-studio, mcritchie-industries and
57
+ moms-app render none (the same split README's "Page-scoped hosts" and
58
+ studio/emails/index.html.erb already name). On an app with no shared
59
+ host $store.modals is UNDEFINED and a trigger throws on click, so mount
60
+ studio/modals/host in the layout first - or give the page its own host
61
+ with studio/modals/scoped_host and drive that instead.
62
+
63
+
64
+ WHICH STORE IT DRIVES - read this before writing a trigger
65
+ ----------------------------------------------------------
66
+ Drive the app's REAL store:
67
+
68
+ $store.modals.open(id, props)
69
+
70
+ NOT dsModals. dsModals is the page-scoped host that the gem's own Modals
71
+ section above stands up, and copying it here is the one wrong turn this
72
+ seam invites.
73
+
74
+ The gem's section needs dsModals for a reason that does NOT apply to an
75
+ app's own modals. Its specimens are the ENGINE's ids, and an app's layout
76
+ host registers only that app's ids under an unconditional template x-if on
77
+ the current modal. Pushing a NOVEL id onto the app's store would open that
78
+ host onto a card it has no registration for, painting an EMPTY panel behind
79
+ the specimen. So the gem's section owns a private store and registers all
80
+ of its specimen content itself.
81
+
82
+ An app's own modal ids are already registered in that app's layout host.
83
+ Push one onto $store.modals and the real host renders the real card - the
84
+ production card, in production chrome, with production behaviour. There is
85
+ nothing to mirror, no second registration list to keep in sync, and no
86
+ iframe. Turf Monster's /admin/modals gallery has driven the real store from
87
+ a plain button this way since it was built; this seam is that same move,
88
+ given a home on the guide.
89
+
90
+ A reader who assumes dsModals will build a mirror instead: a second copy of
91
+ every card, free to drift from the one the app actually ships, which is the
92
+ opposite of what a living style guide is for. %>
93
+ <section id="host-modals" class="space-y-8" x-data="{}"
94
+ style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)">
95
+ <div class="space-y-1">
96
+ <h2 class="text-2xl font-bold text-heading"><%= Studio.app_name %></h2>
97
+ <p class="text-muted text-sm max-w-2xl">
98
+ This app&rsquo;s own modals, contributed by
99
+ <code class="font-mono text-2xs bg-inset px-1.5 py-0.5 rounded">app/views/style/host/_modals.html.erb</code>.
100
+ Every section above belongs to the engine and is inherited; everything
101
+ here belongs to <%= Studio.app_name %> and ships with it. These specimens
102
+ drive the app&rsquo;s <strong>real</strong>
103
+ <code class="font-mono text-2xs bg-inset px-1.5 py-0.5 rounded">$store.modals</code>
104
+ host, so the card that opens here is the card that opens in production.
105
+ </p>
106
+ </div>
107
+
108
+ <%# The app's content. Reached only when it resolves - style/index.html.erb
109
+ owns that guard, because the section nav needs the same answer. %>
110
+ <%= render "style/host/modals" %>
111
+ </section>
@@ -3,22 +3,48 @@
3
3
  application.html.erb, inheriting that app's navbar and theme, so every
4
4
  specimen restyles per app and in dark or light automatically.
5
5
 
6
- Four sections, reached by the sticky section nav below and rendered as
7
- sibling partials: Theme is the landing section (the color foundation - it
6
+ Four ENGINE sections, reached by the sticky section nav below and rendered
7
+ as sibling partials: Theme is the landing section (the color foundation - it
8
8
  owns the role tokens/swatches, the folded-in /admin/theme editor, and the
9
9
  live preview), then Modals (the shared host + engine card blocks), Tricks
10
10
  (the button/surface/motion/effect/leveling primitives, framed as a board of
11
11
  copy-paste-for-an-agent snippets), and Tasks (the shared board primitive).
12
12
 
13
+ A FIFTH section is the host app's own, and appears only on an app that asks
14
+ for it: define app/views/style/host/_modals.html.erb in the consuming app
15
+ and the guide grows a section (and a nav pill) for that app's own modals,
16
+ between Modals and Tricks. The gem still renders the section chrome; the app
17
+ supplies the specimens. Full contract, including which modal store a host
18
+ specimen drives and why it is not the one the Modals section uses:
19
+ app/views/style/_host.html.erb.
20
+
13
21
  Specimens use ONLY the engine's own classes (engine.css component layer plus
14
22
  engine-motion.css motion/effect/leveling layer), so a consumer that bundles
15
23
  engine.css and imports engine-motion.css styles them for real. The primitive
16
24
  CSS is NOT inlined here - the consumer's Tailwind build provides it. Page
17
25
  chrome uses the engine's own .card / .label-upper / theme tokens. %>
18
26
  <%
27
+ # THE HOST SECTION SEAM. A consuming app grows its OWN modal specimens onto
28
+ # this page by defining app/views/style/host/_modals.html.erb. Found by the
29
+ # same three-term lookup_context.exists?(name, prefixes, partial) the modal
30
+ # host uses for modals/_host_extras (studio/modals/_host.html.erb) - one
31
+ # convention in this codebase, not two.
32
+ #
33
+ # Resolved ONCE, here, because two places need the same answer: the sticky
34
+ # section nav below and the render further down. An app that ships no such
35
+ # partial gets no pill, no heading and no container - the page is what it was.
36
+ # The section chrome and the consumer contract live in style/_host.html.erb.
37
+ host_modals = lookup_context.exists?("modals", ["style/host"], true)
38
+
19
39
  sections = [
20
40
  ["theme", "Theme"],
21
- ["modals", "Modals"],
41
+ ["modals", "Modals"]
42
+ ]
43
+ # Adjacent to Modals, not appended: this page groups by SUBJECT, and a host's
44
+ # own modals are the same subject as the engine's. Labelled with the app's own
45
+ # name, so the pill says whose section it is.
46
+ sections << ["host-modals", Studio.app_name] if host_modals
47
+ sections += [
22
48
  ["tricks", "Tricks"],
23
49
  ["tasks", "Tasks"]
24
50
  ]
@@ -54,6 +80,9 @@
54
80
  <div class="space-y-16 pt-10">
55
81
  <%= render "style/theme" %>
56
82
  <%= render "style/modals" %>
83
+ <% if host_modals %>
84
+ <%= render "style/host" %>
85
+ <% end %>
57
86
  <%= render "style/tricks" %>
58
87
  <%= render "style/tasks" %>
59
88
  </div>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.70.0"
2
+ VERSION = "0.71.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.70.0
4
+ version: 0.71.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -509,6 +509,7 @@ files:
509
509
  - app/views/studio/profiles/_save_controls.html.erb
510
510
  - app/views/studio/profiles/edit.html.erb
511
511
  - app/views/studio/profiles/show.html.erb
512
+ - app/views/style/_host.html.erb
512
513
  - app/views/style/_modal_specimen.html.erb
513
514
  - app/views/style/_modals.html.erb
514
515
  - app/views/style/_specimen.html.erb