studio-engine 0.131.0 → 0.133.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -1
  3. data/README.md +65 -28
  4. data/app/assets/tailwind/studio_engine/engine.css +158 -2
  5. data/app/javascript/studio/application.js +1 -0
  6. data/app/javascript/studio/carousel.js +74 -1
  7. data/app/javascript/studio/controllers/carousel_controller.js +163 -9
  8. data/app/javascript/studio/controllers/page_header_controller.js +64 -1
  9. data/app/javascript/studio/controllers/style_section_nav_controller.js +26 -0
  10. data/app/javascript/studio/in_page_links.js +308 -0
  11. data/app/javascript/studio/page_header.js +17 -0
  12. data/app/views/studio/_carousel.html.erb +54 -11
  13. data/app/views/studio/_page_header.html.erb +64 -12
  14. data/app/views/studio/auth/_sign_in_body.html.erb +236 -0
  15. data/app/views/studio/auth/_sign_in_card.html.erb +50 -0
  16. data/app/views/studio/auth/_sign_in_modal.html.erb +36 -0
  17. data/app/views/studio/modals/auth/_resend_footer.html.erb +6 -0
  18. data/app/views/style/_glass.html.erb +4 -8
  19. data/app/views/style/_loaders.html.erb +11 -35
  20. data/app/views/style/_modals.html.erb +29 -39
  21. data/app/views/style/_scrims.html.erb +10 -20
  22. data/app/views/style/_site_footer.html.erb +10 -18
  23. data/app/views/style/_specimen.html.erb +13 -41
  24. data/app/views/style/_tasks.html.erb +4 -22
  25. data/app/views/style/_trick_label.html.erb +33 -0
  26. data/app/views/style/_tricks.html.erb +127 -207
  27. data/app/views/style/index.html.erb +2 -2
  28. data/app/views/style/modals/_wallet_connect.html.erb +2 -1
  29. data/config/studio_tricks.yml +470 -0
  30. data/lib/studio/page_header.rb +53 -0
  31. data/lib/studio/tricks.rb +110 -0
  32. data/lib/studio/version.rb +1 -1
  33. data/lib/studio.rb +6 -4
  34. metadata +9 -3
  35. data/app/views/style/modals/_ds_email_field.html.erb +0 -58
  36. data/app/views/style/modals/_ds_resend_footer.html.erb +0 -70
@@ -17,22 +17,15 @@
17
17
  <%
18
18
  media_style = "background-image: repeating-linear-gradient(135deg, var(--color-primary) 0 18px, var(--color-accent) 18px 36px, var(--color-warning) 36px 54px, var(--color-success) 54px 72px)"
19
19
 
20
+ # Each specimen names its trick (config/studio_tricks.yml): Scrim, Tint, Shade, and
21
+ # the Scrim's soft and strong variants, which each keep a stage of their own so the
22
+ # lab can grade every wash on its own panel.
20
23
  specimens = [
21
- { key: "theme", tone: nil, scrim: "scrim",
22
- klass: ".scrim",
23
- note: "Theme-aware: a tint in light mode, a shade in dark." },
24
- { key: "tint", tone: "scrim-tint", scrim: "scrim",
25
- klass: ".scrim-tint .scrim",
26
- note: "Forced tint, in either theme. Dark ink." },
27
- { key: "shade", tone: "scrim-shade", scrim: "scrim",
28
- klass: ".scrim-shade .scrim",
29
- note: "Forced shade, in either theme. Light ink." },
30
- { key: "soft", tone: nil, scrim: "scrim scrim-soft",
31
- klass: ".scrim .scrim-soft",
32
- note: "A lighter wash: more of the media shows." },
33
- { key: "strong", tone: nil, scrim: "scrim scrim-strong",
34
- klass: ".scrim .scrim-strong",
35
- note: "A heavier wash, for long or small text." }
24
+ { key: "theme", tone: nil, scrim: "scrim", trick: "scrim" },
25
+ { key: "tint", tone: "scrim-tint", scrim: "scrim", trick: "tint" },
26
+ { key: "shade", tone: "scrim-shade", scrim: "scrim", trick: "shade" },
27
+ { key: "soft", tone: nil, scrim: "scrim scrim-soft", trick: "scrim", variant: "soft" },
28
+ { key: "strong", tone: nil, scrim: "scrim scrim-strong", trick: "scrim", variant: "strong" }
36
29
  ]
37
30
  %>
38
31
  <section id="scrims" class="space-y-5" data-scrim-group>
@@ -42,16 +35,13 @@
42
35
  </div>
43
36
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
44
37
  <% specimens.each do |s| %>
45
- <% tone_attr = s[:tone] ? %( class="#{s[:tone]} relative ...") : %( class="relative ...") %>
46
- <%= render layout: "style/specimen",
47
- locals: { klass: s[:klass],
48
- usage: %(<section#{tone_attr}>\n <img class="absolute inset-0 ..." src="...">\n <div class="#{s[:scrim]} absolute inset-0"></div>\n <div class="relative z-10">\n <h2 class="scrim-ink">Headline</h2>\n <p class="scrim-ink-muted">Subtitle</p>\n </div>\n</section>) } do %>
38
+ <%= render layout: "style/specimen", locals: { trick: s[:trick], variant: s[:variant] } do %>
49
39
  <div class="relative w-full max-w-xs h-36 rounded-lg overflow-hidden <%= s[:tone] %>" data-scrim-specimen="<%= s[:key] %>">
50
40
  <div class="absolute inset-0" style="<%= media_style %>" aria-hidden="true"></div>
51
41
  <div class="<%= s[:scrim] %> absolute inset-0" data-scrim-wash></div>
52
42
  <div class="relative z-10 h-full flex flex-col items-center justify-center gap-1 px-4 text-center">
53
43
  <p class="scrim-ink text-lg font-extrabold" data-scrim-ink>Solutions For Business</p>
54
- <p class="scrim-ink-muted text-sm" data-scrim-ink-muted><%= s[:note] %></p>
44
+ <p class="scrim-ink-muted text-sm" data-scrim-ink-muted><%= Studio::Tricks.fetch(s[:trick], s[:variant]).note %></p>
55
45
  </div>
56
46
  </div>
57
47
  <% end %>
@@ -2,13 +2,13 @@
2
2
  The primitives a public site ends with: the footer, its map, and the Google
3
3
  Calendar booking frame, popup and link (docs/SITE_FOOTER.md,
4
4
  docs/BOOKING.md). Each card
5
- gives the helper to call; the preview below them is the real footer partial,
5
+ names its trick and the helper to call; the preview below them is the real footer partial,
6
6
  rendered with THIS app's facts when it has declared any and with a sample
7
7
  when it has not, so the group is never empty.
8
8
 
9
9
  The booking frame is DRAWN, not embedded: a style guide should not ask
10
10
  Google for a calendar every time an admin scrolls past. Its two states, whole
11
- and cropped, are quarter-scale drawings of a made-up schedule (a 720px page
11
+ and cropped, share the Booking Frame card: quarter-scale drawings of a made-up schedule (a 720px page
12
12
  whose box runs from 200 to 600), cropped by the same arithmetic the real
13
13
  wrapper uses (Studio::Booking.crop). No real schedule's numbers are here. %>
14
14
  <%
@@ -47,16 +47,12 @@
47
47
  </div>
48
48
 
49
49
  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
50
- <%= render layout: "style/specimen",
51
- locals: { klass: "studio_site_footer",
52
- usage: %(studio_site_footer # one line, at the end of the layout body) } do %>
50
+ <%= render layout: "style/specimen", locals: { trick: "site_footer" } do %>
53
51
  <p class="text-sm text-secondary text-center">Previewed below.</p>
54
52
  <% end %>
55
53
 
56
54
  <%= render layout: "style/specimen",
57
- locals: { klass: "studio_booking_link",
58
- disabled: !booking, disabled_label: "no booking_url",
59
- usage: %(studio_booking_link "Schedule a call", class: "btn btn-primary") } do %>
55
+ locals: { trick: "booking_link", disabled: !booking, disabled_label: "no booking_url" } do %>
60
56
  <% if booking %>
61
57
  <%= studio_booking_link "Schedule a call", class: "btn btn-primary" %>
62
58
  <% else %>
@@ -64,16 +60,12 @@
64
60
  <% end %>
65
61
  <% end %>
66
62
 
67
- <% [ [ "studio_booking_frame", nil, "whole",
68
- %(studio_booking_frame # whole at rest: the default, and crop: false),
69
- "Whole at rest, the default." ],
70
- [ "config.booking_crop", sample_crop, "cropped",
71
- %(config.booking_crop = { top: 200, bottom: 600, frame_height: 720 } # bin/booking-crop-measure prints yours),
72
- "Cropped to the slot picker at rest." ]
73
- ].each do |klass, crop, state, usage, caption| %>
74
- <%= render layout: "style/specimen",
75
- locals: { klass: klass, disabled: !booking, disabled_label: "no booking_url", usage: usage } do %>
76
- <div class="w-full space-y-3" data-booking-specimen="<%= state %>">
63
+ <%= render layout: "style/specimen",
64
+ locals: { trick: "booking_frame", disabled: !booking, disabled_label: "no booking_url" } do %>
65
+ <% [ [ nil, "whole", "Whole at rest, the default." ],
66
+ [ sample_crop, "cropped", "Cropped to the slot picker at rest." ]
67
+ ].each do |crop, state, caption| %>
68
+ <div class="space-y-3" data-booking-specimen="<%= state %>">
77
69
  <div style="margin-inline: auto; width: 11rem; overflow: hidden; background: #fff; border-radius: .5rem; border: 1px solid rgb(127 127 127 / .35); height: <%= ((crop ? crop[:window] : 720) * scale).round %>px;">
78
70
  <div style="margin-top: -<%= ((crop ? crop[:offset] : 0) * scale).round %>px;">
79
71
  <% sample_page.each do |label, from, to, box| %>
@@ -1,13 +1,15 @@
1
1
  <%# Specimen card for the living style guide (admin/style).
2
- Reusable chrome around ONE primitive, framed for copy-paste-for-an-agent:
3
- stage - the rendered primitive (yielded from the caller block)
4
- meta - its class name and a copyable usage snippet; one Copy per card (the
5
- snippet's, else the class name's)
2
+ Reusable chrome around ONE primitive, named so Alex McRitchie can say it to an
3
+ agent ("add a Sheen"):
4
+ stage - the rendered primitive (yielded from the caller block), with its
5
+ variants side by side when the card folds them in
6
+ label - the trick's name, a one-line note and its class or helper, read from
7
+ config/studio_tricks.yml by key (style/_trick_label), with one Copy
8
+ that copies the agent line, never a markup snippet
6
9
  locals:
7
- klass (String, required) - the class label, shown prominently with a
8
- copy button so an agent can grab the exact class list.
9
- usage (String, optional) - a copyable usage snippet.
10
- note (String, optional) - one short sentence on what it does.
10
+ trick (String, required) - the registry key (config/studio_tricks.yml).
11
+ variant (String, optional) - a variant with a card of its own
12
+ ("Loader · orbit"); the card is then named for the variant.
11
13
  disabled (Boolean, optional) - render "disabled but present": the
12
14
  primitive stays visible + previewable but is muted/greyed
13
15
  and flagged, for a capability the host app has turned off
@@ -30,7 +32,7 @@
30
32
  # utilities and lift the opacity a touch so the trigger reads as tappable.
31
33
  inner_cls = openable ? "opacity-60 grayscale" : "opacity-40 grayscale select-none pointer-events-none"
32
34
  %>
33
- <article class="card overflow-hidden flex flex-col h-full<%= " opacity-95" if disabled %>" <%= 'aria-disabled="true"'.html_safe if disabled %>>
35
+ <article class="card overflow-hidden flex flex-col h-full<%= " opacity-95" if disabled %>" <%= tag.attributes(data: { trick: local_assigns.fetch(:trick), trick_variant: local_assigns[:variant] }, aria: { disabled: ("true" if disabled) }) %>>
34
36
  <div class="relative flex flex-1 flex-wrap items-center justify-center gap-4 p-6 bg-surface-alt min-h-32">
35
37
  <% if disabled %>
36
38
  <span class="absolute top-2 right-2 badge z-10" style="color: var(--color-warning); border-color: var(--color-warning)"><%= disabled_label %></span>
@@ -39,37 +41,7 @@
39
41
  <%= yield %>
40
42
  <% end %>
41
43
  </div>
42
- <div class="border-t border-subtle p-3 space-y-2">
43
- <% if local_assigns[:note].present? %>
44
- <p class="text-sm text-body"><%= local_assigns[:note] %></p>
45
- <% end %>
46
- <%# ONE Copy per card. With a usage snippet, the snippet's Copy is the one: it holds
47
- the class too. Without one, the class name carries the Copy. %>
48
- <% snippet = local_assigns[:usage] %>
49
- <div class="flex items-start justify-between gap-2" x-data="{ copied: false }">
50
- <code x-ref="klass" class="font-mono text-xs font-bold text-heading break-all leading-relaxed flex-1 min-w-0"><%= local_assigns[:klass] %></code>
51
- <% unless snippet.present? %>
52
- <button type="button"
53
- class="btn btn-neutral btn-sm shrink-0"
54
- aria-label="Copy class name"
55
- @click="navigator.clipboard?.writeText($refs.klass.innerText); copied = true; setTimeout(() => copied = false, 1200)">
56
- <span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
57
- </button>
58
- <% end %>
59
- </div>
60
- <% if snippet.present? %>
61
- <%# The code wraps inside its box, and pr-24 keeps every line clear of the Copy
62
- button, "Copied" included: 74px with its inset on macOS, 3px wider in CI's
63
- Linux fonts, so pr-20 was not enough. %>
64
- <div class="relative" x-data="{ copied: false }">
65
- <pre class="bg-inset text-body text-2xs rounded-lg px-3 py-2 pr-24 whitespace-pre-wrap break-words"><code x-ref="snippet"><%= snippet %></code></pre>
66
- <button type="button"
67
- class="btn btn-neutral btn-sm absolute top-2 right-2"
68
- aria-label="Copy usage snippet"
69
- @click="navigator.clipboard?.writeText($refs.snippet.innerText); copied = true; setTimeout(() => copied = false, 1200)">
70
- <span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
71
- </button>
72
- </div>
73
- <% end %>
44
+ <div class="border-t border-subtle p-3">
45
+ <%= render "style/trick_label", trick: local_assigns.fetch(:trick), variant: local_assigns[:variant] %>
74
46
  </div>
75
47
  </article>
@@ -28,20 +28,6 @@
28
28
  cards: [{ id: "ses-email-backend", zone: "shipped", title: "SES email backend", repo: "mcritchie", who: "ST" }] }
29
29
  ]
30
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
- "# the board; it needs nothing else on the page:",
36
- 'render "studio/board/board",',
37
- " columns: @columns, # [{ key:, label:, cards:, badge_class: }]",
38
- ' card_partial: "tasks/task_card",',
39
- " reorder_url: tasks_reorder_path, # POST { slugs: [...], zone: }",
40
- ' move_url: "/tasks/:id.json", # PATCH { task: { stage: } }',
41
- ' move_param: { resource: "task", attr: "stage" },',
42
- ' live_channel: "deployments"'
43
- ].join("\n")
44
-
45
31
  # The static reference palette below (unchanged): each column maps to a stage-*
46
32
  # badge scheme.
47
33
  board_columns = [
@@ -84,14 +70,10 @@
84
70
  move_param: { resource: "task", attr: "stage" },
85
71
  group: "studio-board-demo",
86
72
  empty_label: "Drop tasks here" %>
87
- <div class="relative" x-data="{ copied: false }">
88
- <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>
89
- <button type="button"
90
- class="btn btn-neutral btn-sm absolute top-2 right-2"
91
- aria-label="Copy usage snippet"
92
- @click="navigator.clipboard?.writeText($refs.boardUsage.innerText); copied = true; setTimeout(() => copied = false, 1200)">
93
- <span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
94
- </button>
73
+ <%# Its name, not a snippet (config/studio_tricks.yml): an agent reads the locals
74
+ from studio/board/_board's header. %>
75
+ <div class="card p-3" data-trick="board">
76
+ <%= render "style/trick_label", trick: "board" %>
95
77
  </div>
96
78
  </div>
97
79
 
@@ -0,0 +1,33 @@
1
+ <%# A trick's name, note and token, with one Copy (config/studio_tricks.yml).
2
+ The name is the heading because it is what a person says to an agent ("add a
3
+ Sheen"); the token is what the agent writes, so it sits under it, small and
4
+ muted. Copy puts the agent line on the clipboard: the name and the token,
5
+ variants included (Studio::Tricks.agent_line). No markup snippet: an agent can
6
+ read the markup from the engine, and only the name has to travel by word.
7
+ locals:
8
+ trick (String, required) - the registry key.
9
+ variant (String, optional) - one variant's own label. %>
10
+ <% t = Studio::Tricks.fetch(trick, local_assigns[:variant]) %>
11
+ <div class="flex items-start justify-between gap-3" x-data="{ copied: false }">
12
+ <div class="min-w-0 flex-1 space-y-1">
13
+ <h4 class="type-h4 text-heading" data-trick-name><%= t.name %></h4>
14
+ <p class="text-sm text-body" data-trick-note><%= t.note %></p>
15
+ <code class="block font-mono text-2xs text-muted break-words" data-trick-token><%= t.token %></code>
16
+ <% if t.variants.any? %>
17
+ <%# The variants by name, so "Sheen · loop" is on the card and not only on the
18
+ clipboard. %>
19
+ <ul class="space-y-0.5" data-trick-variants>
20
+ <% t.variants.each do |v| %>
21
+ <li class="text-2xs leading-snug"><span class="text-body font-semibold" data-trick-variant-name><%= v.name %></span> <code class="font-mono text-muted break-words"><%= v.token %></code></li>
22
+ <% end %>
23
+ </ul>
24
+ <% end %>
25
+ </div>
26
+ <button type="button"
27
+ class="btn btn-neutral btn-sm shrink-0"
28
+ aria-label="Copy the name for an agent"
29
+ data-trick-copy="<%= Studio::Tricks.agent_line(trick, local_assigns[:variant]) %>"
30
+ @click="navigator.clipboard?.writeText($el.dataset.trickCopy); copied = true; setTimeout(() => copied = false, 1200)">
31
+ <span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
32
+ </button>
33
+ </div>