studio-engine 0.132.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -0
- data/README.md +52 -24
- data/app/assets/tailwind/studio_engine/engine.css +141 -0
- data/app/javascript/studio/application.js +1 -0
- data/app/javascript/studio/carousel.js +66 -1
- data/app/javascript/studio/controllers/carousel_controller.js +139 -8
- data/app/javascript/studio/controllers/page_header_controller.js +64 -1
- data/app/javascript/studio/controllers/style_section_nav_controller.js +26 -0
- data/app/javascript/studio/in_page_links.js +308 -0
- data/app/javascript/studio/page_header.js +17 -0
- data/app/views/studio/_carousel.html.erb +33 -5
- data/app/views/studio/_page_header.html.erb +64 -12
- data/app/views/studio/auth/_sign_in_body.html.erb +236 -0
- data/app/views/studio/auth/_sign_in_card.html.erb +50 -0
- data/app/views/studio/auth/_sign_in_modal.html.erb +36 -0
- data/app/views/studio/modals/auth/_resend_footer.html.erb +6 -0
- data/app/views/style/_glass.html.erb +4 -8
- data/app/views/style/_loaders.html.erb +11 -35
- data/app/views/style/_modals.html.erb +29 -39
- data/app/views/style/_scrims.html.erb +10 -20
- data/app/views/style/_site_footer.html.erb +10 -18
- data/app/views/style/_specimen.html.erb +13 -41
- data/app/views/style/_tasks.html.erb +4 -22
- data/app/views/style/_trick_label.html.erb +33 -0
- data/app/views/style/_tricks.html.erb +127 -207
- data/app/views/style/index.html.erb +2 -2
- data/app/views/style/modals/_wallet_connect.html.erb +2 -1
- data/config/studio_tricks.yml +470 -0
- data/lib/studio/page_header.rb +53 -0
- data/lib/studio/tricks.rb +110 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +6 -4
- metadata +9 -3
- data/app/views/style/modals/_ds_email_field.html.erb +0 -58
- data/app/views/style/modals/_ds_resend_footer.html.erb +0 -70
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
progress bar, pulsing dots, button loader, full-screen loader, pull to
|
|
6
6
|
refresh, infinite scroll. Every specimen is the engine's own helper
|
|
7
7
|
(Studio::LoaderHelper, docs/LOADER.md), so what is on this page is what a
|
|
8
|
-
view gets
|
|
8
|
+
view gets, under its trick name (config/studio_tricks.yml): Spinner, Skeleton,
|
|
9
|
+
Progress Bar, Typing Dots, Busy Button, Overlay, Pull Refresh, Infinite Scroll.
|
|
10
|
+
The branded styles themselves are under Tricks.
|
|
9
11
|
|
|
10
12
|
The infinite-scroll specimen is live: its pages come from feed_path, the
|
|
11
13
|
admin-only demo feed (StyleController#loaders_feed) unless a caller names
|
|
@@ -16,18 +18,13 @@
|
|
|
16
18
|
# path Studio.routes draws.
|
|
17
19
|
feed_path = local_assigns[:feed_path] ||
|
|
18
20
|
(respond_to?(:admin_style_loaders_feed_path) ? admin_style_loaders_feed_path : "/admin/style/loaders_feed")
|
|
19
|
-
erb = ->(code) { "<" + "%= " + code + " %" + ">" }
|
|
20
|
-
erb_end = "<" + "% end %" + ">"
|
|
21
21
|
%>
|
|
22
22
|
<section id="loaders" class="section-target space-y-6" data-style-loaders>
|
|
23
23
|
<%= render "studio/section_header", title: "Loaders" %>
|
|
24
24
|
<p class="text-muted text-sm max-w-2xl">Eight ways to say wait. Match the loader to the wait.</p>
|
|
25
25
|
|
|
26
26
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
27
|
-
<%= render layout: "style/specimen",
|
|
28
|
-
locals: { klass: "studio_loader style: :spinner",
|
|
29
|
-
note: "A small ring for a short wait beside text or in a control.",
|
|
30
|
-
usage: erb.call("studio_loader style: :spinner") } do %>
|
|
27
|
+
<%= render layout: "style/specimen", locals: { trick: "spinner" } do %>
|
|
31
28
|
<div data-loader-specimen="spinner" class="flex items-center gap-4">
|
|
32
29
|
<%= studio_loader style: :spinner %>
|
|
33
30
|
<%= studio_loader style: :spinner, size: 32 %>
|
|
@@ -35,40 +32,28 @@
|
|
|
35
32
|
</div>
|
|
36
33
|
<% end %>
|
|
37
34
|
|
|
38
|
-
<%= render layout: "style/specimen",
|
|
39
|
-
locals: { klass: "studio_skeleton :avatar / :card / :lines / :media",
|
|
40
|
-
note: "Grey shapes of the content to come, so the page keeps its layout.",
|
|
41
|
-
usage: [erb.call("studio_skeleton :avatar, count: 3"), erb.call('studio_skeleton :card, count: 3, class: "sm:grid-cols-3"')].join("\n") } do %>
|
|
35
|
+
<%= render layout: "style/specimen", locals: { trick: "skeleton" } do %>
|
|
42
36
|
<div data-loader-specimen="skeleton" class="grid w-full gap-4">
|
|
43
37
|
<%= studio_skeleton :avatar, count: 2, label: "Loading people" %>
|
|
44
38
|
<%= studio_skeleton :lines, lines: 2, label: "Loading notes" %>
|
|
45
39
|
</div>
|
|
46
40
|
<% end %>
|
|
47
41
|
|
|
48
|
-
<%= render layout: "style/specimen",
|
|
49
|
-
locals: { klass: "studio_loader style: :bar, percent:",
|
|
50
|
-
note: "A bar for a wait you can measure; with no percent it sweeps.",
|
|
51
|
-
usage: [erb.call("studio_loader style: :bar, percent: 62"), erb.call("studio_loader style: :bar"), 'StudioLoader.setPercent(el, 80)'].join("\n") } do %>
|
|
42
|
+
<%= render layout: "style/specimen", locals: { trick: "progress_bar" } do %>
|
|
52
43
|
<div data-loader-specimen="bar" class="grid w-full gap-4">
|
|
53
44
|
<%= studio_loader style: :bar, percent: 62, label: "Uploading" %>
|
|
54
45
|
<%= studio_loader style: :bar, label: "Importing" %>
|
|
55
46
|
</div>
|
|
56
47
|
<% end %>
|
|
57
48
|
|
|
58
|
-
<%= render layout: "style/specimen",
|
|
59
|
-
locals: { klass: "studio_loader style: :dots",
|
|
60
|
-
note: "Three dots for a reply on its way, such as a chat answer.",
|
|
61
|
-
usage: erb.call("studio_loader style: :dots") } do %>
|
|
49
|
+
<%= render layout: "style/specimen", locals: { trick: "typing_dots" } do %>
|
|
62
50
|
<div data-loader-specimen="dots" class="flex items-center gap-6">
|
|
63
51
|
<%= studio_loader style: :dots %>
|
|
64
52
|
<span class="inline-flex items-center gap-2 rounded-2xl bg-inset px-3 py-2 text-sm text-body"><%= studio_loader style: :dots, size: 24, label: "Alex is typing" %></span>
|
|
65
53
|
</div>
|
|
66
54
|
<% end %>
|
|
67
55
|
|
|
68
|
-
<%= render layout: "style/specimen",
|
|
69
|
-
locals: { klass: "studio_loader_pending (inside a button)",
|
|
70
|
-
note: "The button keeps its place and shows that it is working.",
|
|
71
|
-
usage: ["<button class=\"btn btn-primary\">", " <span data-loader-idle>Save</span>", " #{erb.call('studio_loader_pending style: :spinner')}", " <span data-loader-busy>Saving…</span>", "</button>"].join("\n") } do %>
|
|
56
|
+
<%= render layout: "style/specimen", locals: { trick: "busy_button" } do %>
|
|
72
57
|
<div data-loader-specimen="button" class="flex flex-wrap items-center gap-3" x-data="{ busy: false }">
|
|
73
58
|
<button type="button" class="btn btn-primary" x-bind:aria-busy="busy ? 'true' : 'false'" aria-busy="false"
|
|
74
59
|
x-on:click="busy = true; setTimeout(() => busy = false, 2000)" data-test="style-loader-save">
|
|
@@ -81,10 +66,7 @@
|
|
|
81
66
|
</div>
|
|
82
67
|
<% end %>
|
|
83
68
|
|
|
84
|
-
<%= render layout: "style/specimen",
|
|
85
|
-
locals: { klass: "studio_loader_overlay (full screen)",
|
|
86
|
-
note: "The branded overlay, for a page that takes longer than 350ms.",
|
|
87
|
-
usage: ['window.StudioLoader.show("Caption")', "window.StudioLoader.hide()"].join("\n") } do %>
|
|
69
|
+
<%= render layout: "style/specimen", locals: { trick: "overlay" } do %>
|
|
88
70
|
<div data-loader-specimen="overlay">
|
|
89
71
|
<button type="button" class="btn btn-secondary" data-test="style-loaders-overlay"
|
|
90
72
|
x-data x-on:click="window.StudioLoader && window.StudioLoader.show('Loading your page…'); setTimeout(() => window.StudioLoader && window.StudioLoader.hide(), 2000)">
|
|
@@ -93,10 +75,7 @@
|
|
|
93
75
|
</div>
|
|
94
76
|
<% end %>
|
|
95
77
|
|
|
96
|
-
<%= render layout: "style/specimen",
|
|
97
|
-
locals: { klass: "studio_pull_to_refresh (phones only)",
|
|
98
|
-
note: "On a phone, pull down from the top of the page to reload it.",
|
|
99
|
-
usage: [erb.call('studio_pull_to_refresh frame: "feed" do'), " ...", erb_end].join("\n") } do %>
|
|
78
|
+
<%= render layout: "style/specimen", locals: { trick: "pull_refresh" } do %>
|
|
100
79
|
<%# Drawn still, mid-refresh: this wrapper has no controller, so it never arms. %>
|
|
101
80
|
<div data-loader-specimen="pull" class="studio-pull-refresh w-40 h-40 overflow-hidden rounded-2xl border border-subtle bg-surface pt-20 px-3" data-pull-state="refreshing">
|
|
102
81
|
<span class="studio-pull-refresh-indicator" aria-hidden="true">
|
|
@@ -106,10 +85,7 @@
|
|
|
106
85
|
</div>
|
|
107
86
|
<% end %>
|
|
108
87
|
|
|
109
|
-
<%= render layout: "style/specimen",
|
|
110
|
-
locals: { klass: "studio_infinite_scroll id:, page:, next_url:",
|
|
111
|
-
note: "The next page loads as the list's end comes near, and Load more stays.",
|
|
112
|
-
usage: [erb.call('studio_infinite_scroll id: "posts", page: @page, next_url: @next_url do'), " ...one page of items...", erb_end].join("\n") } do %>
|
|
88
|
+
<%= render layout: "style/specimen", locals: { trick: "infinite_scroll" } do %>
|
|
113
89
|
<div data-loader-specimen="infinite" class="w-full">
|
|
114
90
|
<%= render "style/loaders_feed_page", page: 1, feed_path: feed_path %>
|
|
115
91
|
</div>
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
card behind ours. So this section owns a tiny, self-contained store + overlay
|
|
15
15
|
and registers all specimen content itself.
|
|
16
16
|
|
|
17
|
-
Groups: Auth (the engine
|
|
18
|
-
|
|
17
|
+
Groups: Auth (the engine's standard sign-in card, studio/auth/_sign_in_modal,
|
|
18
|
+
in demo mode), Eligibility and entry (the
|
|
19
19
|
app-supplied-policy age-gate DOB primitive, gated by Studio.feature?(:age_gate)),
|
|
20
20
|
Profile (crop / upload, opened
|
|
21
21
|
through studio/cropper_assets), Web3 (wallet-connect and web3-step-up now
|
|
@@ -97,7 +97,8 @@
|
|
|
97
97
|
# THE AUTH METHOD-TOGGLE DEFAULTS USED TO LIVE HERE (ml_default / g_default /
|
|
98
98
|
# w_default / auth_toggles) and are gone with the card they configured. They
|
|
99
99
|
# belonged to style/modals/_auth, a MIRROR of turf-monster's sign-in modal,
|
|
100
|
-
# retired on 2026-09-09. The engine
|
|
100
|
+
# retired on 2026-09-09. The engine's own card (studio/auth/_sign_in_modal,
|
|
101
|
+
# 2026-11-06) has no toggles: its specimen runs it in demo mode as shipped.
|
|
101
102
|
#
|
|
102
103
|
# The rule they were built to enforce OUTLIVES them and is worth keeping: a
|
|
103
104
|
# TRIGGER must carry at least the gate its target's REGISTRATION carries, or
|
|
@@ -202,17 +203,15 @@
|
|
|
202
203
|
<div class="bg-surface rounded-xl border border-subtle shadow-2xl p-6 w-full"
|
|
203
204
|
:class="$store.dsModals.cardClasses()">
|
|
204
205
|
|
|
205
|
-
<%# --- Auth
|
|
206
|
-
<%#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<template x-if="$store.dsModals.current().id === 'ds-resend-footer'">
|
|
215
|
-
<div><%= render "style/modals/ds_resend_footer" %></div>
|
|
206
|
+
<%# --- Auth --- %>
|
|
207
|
+
<%# The engine's standard sign-in card (studio/auth/_sign_in_modal), the
|
|
208
|
+
REAL partial in demo mode: Google and Send post nothing, and the
|
|
209
|
+
link-sent step and resend cooldown play. It folds in the two pieces
|
|
210
|
+
that used to be carded alone here, studio/modals/shared/_email_field
|
|
211
|
+
and studio/modals/auth/_resend_footer (Alex McRitchie, 2026-11-06:
|
|
212
|
+
"this should use the standard auth component with Google and Email"). %>
|
|
213
|
+
<template x-if="$store.dsModals.current().id === 'ds-sign-in'">
|
|
214
|
+
<div><%= render "studio/auth/sign_in_modal", modal_store: "dsModals", demo: true %></div>
|
|
216
215
|
</template>
|
|
217
216
|
|
|
218
217
|
<%# --- Onboarding step (its card now lives in Profile) --- %>
|
|
@@ -452,36 +451,27 @@
|
|
|
452
451
|
<section id="modals-auth" class="space-y-5">
|
|
453
452
|
<div class="space-y-1">
|
|
454
453
|
<h3 class="type-h4 text-heading">Auth</h3>
|
|
455
|
-
<p class="text-muted text-sm">The engine
|
|
454
|
+
<p class="text-muted text-sm">The engine's standard sign-in card: Google, or, email magic link.</p>
|
|
456
455
|
</div>
|
|
457
456
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
458
|
-
<%# email field — the one piece every app's auth card renders. %>
|
|
459
457
|
<%= render layout: "style/modal_specimen", locals: {
|
|
460
|
-
label: "
|
|
461
|
-
reference: %(the
|
|
462
|
-
open_expr: "$store.dsModals.open('ds-
|
|
463
|
-
glow_when: ds_glow.call("ds-
|
|
458
|
+
label: "Sign in",
|
|
459
|
+
reference: %(the engine's standard sign-in card as a modal (studio/auth/_sign_in_modal; the page form is studio/auth/_sign_in_card) — Google, then "or", then the email magic link, and on send the link-sent step with the shared resend footer (studio/modals/auth/_resend_footer) under the shared email field (studio/modals/shared/_email_field). Locals: heading, subheading (default "Sign in to" the app name), return_to (kept to an in-app path, rides both Google and the magic link), preamble and extras (partials above Google and after it, for age attestation or a Solana wallet button), google, magic_link, modal_store, demo. Real use posts Google to /auth/google_oauth2 (button_to POST, Turbo off) and the email to POST /magic_link. THIS SPECIMEN IS demo: true, SO NOTHING POSTS: Google is an inert button, Send waits on a timer, and the resend cooldown is 5s instead of 60s. Open with $store.dsModals.open('ds-sign-in')),
|
|
460
|
+
open_expr: "$store.dsModals.open('ds-sign-in')",
|
|
461
|
+
glow_when: ds_glow.call("ds-sign-in") } do %>
|
|
464
462
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
465
|
-
<span class="block h-
|
|
466
|
-
<span class="
|
|
467
|
-
|
|
468
|
-
<span class="block h-1.5
|
|
469
|
-
<span class="block w-3 h-3 rounded-full ml-1" style="background: var(--color-success); opacity: .55"></span>
|
|
463
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
464
|
+
<span class="flex items-center justify-center gap-1 h-6 w-full rounded border" style="border-color: var(--color-border-strong)">
|
|
465
|
+
<span class="block w-2.5 h-2.5 rounded-full" style="background: conic-gradient(#EA4335 0 25%, #FBBC05 0 50%, #34A853 0 75%, #4285F4 0)"></span>
|
|
466
|
+
<span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .2"></span>
|
|
470
467
|
</span>
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
open_expr: "$store.dsModals.open('ds-resend-footer')",
|
|
479
|
-
glow_when: ds_glow.call("ds-resend-footer") } do %>
|
|
480
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
481
|
-
<div class="text-2xl leading-none">📬</div>
|
|
482
|
-
<span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
483
|
-
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-primary); opacity: .7"></span>
|
|
484
|
-
<span class="block h-1.5 w-10 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
468
|
+
<span class="flex items-center gap-1.5">
|
|
469
|
+
<span class="block h-px flex-1" style="background: var(--color-border-strong)"></span>
|
|
470
|
+
<span class="block h-1 w-2 rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
471
|
+
<span class="block h-px flex-1" style="background: var(--color-border-strong)"></span>
|
|
472
|
+
</span>
|
|
473
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
474
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-primary)"></span>
|
|
485
475
|
</div>
|
|
486
476
|
<% end %>
|
|
487
477
|
</div>
|
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
{ key: "
|
|
25
|
-
|
|
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
|
-
|
|
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[:
|
|
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
|
-
|
|
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,
|
|
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: {
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 %>" <%=
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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>
|