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
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# The trick names: one short name for every specimen on /admin/style, so Alex McRitchie
|
|
2
|
+
# can say "add a Sheen" and an agent knows it means `.sheen` on a `.btn`.
|
|
3
|
+
#
|
|
4
|
+
# THE ONE SOURCE OF TRUTH. The style guide's cards read their name, note and token from
|
|
5
|
+
# here by key (render layout: "style/specimen", locals: { trick: "sheen" }), and
|
|
6
|
+
# docs/TRICKS.md is generated from it: run bin/tricks-doc after an edit.
|
|
7
|
+
# test/lib/tricks_registry_test.rb fails when the doc drifts, a name repeats, or a token
|
|
8
|
+
# names a class, helper, partial or script the engine does not ship.
|
|
9
|
+
#
|
|
10
|
+
# THE NAMING RULE. A name is one or two Title-case words, the noun a person would say
|
|
11
|
+
# ("Sheen", "Border Glow"). A variant is the name, a middle dot and one lower-case word
|
|
12
|
+
# or two ("Sheen · loop"); its key here is that word, with _ for a space. Every name and
|
|
13
|
+
# every variant name is unique across the guide.
|
|
14
|
+
#
|
|
15
|
+
# Each entry:
|
|
16
|
+
# name - the trick's name.
|
|
17
|
+
# group - the style guide group it is staged in (the h3 above its card).
|
|
18
|
+
# token - what to write: the class, helper, partial or call.
|
|
19
|
+
# note - one short sentence on what it does.
|
|
20
|
+
# resolve - what the registry test proves exists: css:<class> (engine CSS),
|
|
21
|
+
# helper:<method>, partial:<path>, js:<text> (app/javascript),
|
|
22
|
+
# config:<Studio setting>.
|
|
23
|
+
# variants - optional, keyed by the variant word; each has token, note, resolve.
|
|
24
|
+
|
|
25
|
+
button:
|
|
26
|
+
name: Button
|
|
27
|
+
group: Buttons
|
|
28
|
+
token: ".btn .btn-primary (or -secondary, -success, -danger, -warning, -neutral, -outline, -google)"
|
|
29
|
+
note: "A button: .btn plus one role."
|
|
30
|
+
resolve: [css:btn, css:btn-primary, css:btn-secondary, css:btn-success, css:btn-danger, css:btn-warning, css:btn-neutral, css:btn-outline, css:btn-google]
|
|
31
|
+
variants:
|
|
32
|
+
size:
|
|
33
|
+
token: ".btn-sm / .btn-lg"
|
|
34
|
+
note: "Smaller or larger; the default size needs no class."
|
|
35
|
+
resolve: [css:btn-sm, css:btn-lg]
|
|
36
|
+
|
|
37
|
+
edge_glow:
|
|
38
|
+
name: Edge Glow
|
|
39
|
+
group: Button tricks
|
|
40
|
+
token: ".edge-rotate-glow"
|
|
41
|
+
note: "Adds a glow that rotates around the button's edge."
|
|
42
|
+
resolve: [css:edge-rotate-glow]
|
|
43
|
+
variants:
|
|
44
|
+
two-color:
|
|
45
|
+
token: "--edge-rotate-glow-color / --edge-rotate-glow-color-b"
|
|
46
|
+
note: "Two colors chase each other round the ring."
|
|
47
|
+
resolve: [css:edge-rotate-glow]
|
|
48
|
+
|
|
49
|
+
border_glow:
|
|
50
|
+
name: Border Glow
|
|
51
|
+
group: Button tricks
|
|
52
|
+
token: ".studio-border-glow"
|
|
53
|
+
note: "Adds a moving rainbow border with a soft halo."
|
|
54
|
+
resolve: [css:studio-border-glow]
|
|
55
|
+
variants:
|
|
56
|
+
clear:
|
|
57
|
+
token: ".studio-border-glow--clear"
|
|
58
|
+
note: "The same ring and halo on a clear button."
|
|
59
|
+
resolve: [css:studio-border-glow--clear]
|
|
60
|
+
|
|
61
|
+
sheen:
|
|
62
|
+
name: Sheen
|
|
63
|
+
group: Button tricks
|
|
64
|
+
token: ".sheen"
|
|
65
|
+
note: "Adds a band of light that sweeps across the button."
|
|
66
|
+
resolve: [css:sheen]
|
|
67
|
+
variants:
|
|
68
|
+
loop:
|
|
69
|
+
token: ".sheen--loop"
|
|
70
|
+
note: "Repeats the sweep."
|
|
71
|
+
resolve: [css:sheen--loop]
|
|
72
|
+
|
|
73
|
+
pulse:
|
|
74
|
+
name: Pulse
|
|
75
|
+
group: Button tricks
|
|
76
|
+
token: ".pulse-cta"
|
|
77
|
+
note: "Adds a slow beat with a ring that spreads from the edge."
|
|
78
|
+
resolve: [css:pulse-cta]
|
|
79
|
+
variants:
|
|
80
|
+
color:
|
|
81
|
+
token: "--pulse-cta-color (and --pulse-cta-speed, --pulse-cta-scale)"
|
|
82
|
+
note: "Sets the ring's color, speed and size."
|
|
83
|
+
resolve: [css:pulse-cta]
|
|
84
|
+
|
|
85
|
+
fizz:
|
|
86
|
+
name: Fizz
|
|
87
|
+
group: Button tricks
|
|
88
|
+
token: "render layout: \"studio/fizz\" around the button"
|
|
89
|
+
note: "Adds bubbles that rise from behind the button."
|
|
90
|
+
resolve: [partial:studio/fizz, css:fizz-stack]
|
|
91
|
+
variants:
|
|
92
|
+
colors:
|
|
93
|
+
token: "colors: [...]"
|
|
94
|
+
note: "Picks the bubbles' colors."
|
|
95
|
+
resolve: [partial:studio/fizz]
|
|
96
|
+
|
|
97
|
+
hold:
|
|
98
|
+
name: Hold
|
|
99
|
+
group: Button tricks
|
|
100
|
+
token: "render \"studio/hold_button\""
|
|
101
|
+
note: "Makes the button confirm only after a two-second hold."
|
|
102
|
+
resolve: [partial:studio/hold_button, css:hold-btn]
|
|
103
|
+
variants:
|
|
104
|
+
recolor:
|
|
105
|
+
token: "--hold-* on a parent"
|
|
106
|
+
note: "Recolors a hold button from any parent element."
|
|
107
|
+
resolve: [css:hold-btn]
|
|
108
|
+
|
|
109
|
+
burst:
|
|
110
|
+
name: Burst
|
|
111
|
+
group: Button tricks
|
|
112
|
+
token: "window.studioConfetti.burst(el)"
|
|
113
|
+
note: "Fires confetti out from behind the button."
|
|
114
|
+
resolve: [js:burst(]
|
|
115
|
+
variants:
|
|
116
|
+
cannons:
|
|
117
|
+
token: "window.studioConfetti.cannons()"
|
|
118
|
+
note: "Fires confetti in from both sides of the screen."
|
|
119
|
+
resolve: [js:cannons(]
|
|
120
|
+
|
|
121
|
+
page_header:
|
|
122
|
+
name: Page Header
|
|
123
|
+
group: Page header
|
|
124
|
+
token: "render \"studio/page_header\""
|
|
125
|
+
note: "Crumbs above the title, tabs on the right, links under it."
|
|
126
|
+
resolve: [partial:studio/page_header, css:page-header]
|
|
127
|
+
variants:
|
|
128
|
+
stuck:
|
|
129
|
+
token: "[data-stuck]"
|
|
130
|
+
note: "How it looks once pinned under the navbar."
|
|
131
|
+
resolve: [css:page-header]
|
|
132
|
+
controls:
|
|
133
|
+
token: "tabs: or anchors:, menu:, action:, tools_html:"
|
|
134
|
+
note: "Tabs or anchors, a Links menu, one action, then tools, at one height."
|
|
135
|
+
resolve: [partial:studio/page_header, css:page-header-menu, css:page-header-menu-list, css:page-header-tools]
|
|
136
|
+
|
|
137
|
+
section_header:
|
|
138
|
+
name: Section Header
|
|
139
|
+
group: Section header
|
|
140
|
+
token: "render \"studio/section_header\""
|
|
141
|
+
note: "A section's title that sticks until the next one takes its place."
|
|
142
|
+
resolve: [partial:studio/section_header, css:section-header]
|
|
143
|
+
variants:
|
|
144
|
+
stuck:
|
|
145
|
+
token: "[data-stuck]"
|
|
146
|
+
note: "How it looks once pinned under the page row."
|
|
147
|
+
resolve: [css:section-header]
|
|
148
|
+
|
|
149
|
+
carousel:
|
|
150
|
+
name: Carousel
|
|
151
|
+
group: Carousel
|
|
152
|
+
token: "render layout: \"studio/carousel\""
|
|
153
|
+
note: "Shows a set too long to grid, a few slides at a time."
|
|
154
|
+
resolve: [partial:studio/carousel, css:carousel]
|
|
155
|
+
|
|
156
|
+
spotlight:
|
|
157
|
+
name: Spotlight Carousel
|
|
158
|
+
group: Carousel
|
|
159
|
+
token: "render layout: \"studio/carousel\", style: :spotlight"
|
|
160
|
+
note: "Centres one slide with its caption; its neighbours shrink and fade."
|
|
161
|
+
resolve: [partial:studio/carousel, css:carousel-spotlight]
|
|
162
|
+
|
|
163
|
+
card:
|
|
164
|
+
name: Card
|
|
165
|
+
group: "Surfaces, text & form"
|
|
166
|
+
token: ".card"
|
|
167
|
+
note: "The standard surface."
|
|
168
|
+
resolve: [css:card]
|
|
169
|
+
variants:
|
|
170
|
+
hover:
|
|
171
|
+
token: ".card-hover"
|
|
172
|
+
note: "A card that lifts under the pointer."
|
|
173
|
+
resolve: [css:card-hover]
|
|
174
|
+
|
|
175
|
+
badge:
|
|
176
|
+
name: Badge
|
|
177
|
+
group: "Surfaces, text & form"
|
|
178
|
+
token: ".badge"
|
|
179
|
+
note: "A small outlined pill; set its color and border-color for a status."
|
|
180
|
+
resolve: [css:badge]
|
|
181
|
+
|
|
182
|
+
input:
|
|
183
|
+
name: Input
|
|
184
|
+
group: "Surfaces, text & form"
|
|
185
|
+
token: ".input-field"
|
|
186
|
+
note: "The text field."
|
|
187
|
+
resolve: [css:input-field]
|
|
188
|
+
|
|
189
|
+
label:
|
|
190
|
+
name: Label
|
|
191
|
+
group: "Surfaces, text & form"
|
|
192
|
+
token: ".label-upper"
|
|
193
|
+
note: "Small upper-case text above a group."
|
|
194
|
+
resolve: [css:label-upper]
|
|
195
|
+
|
|
196
|
+
empty_state:
|
|
197
|
+
name: Empty State
|
|
198
|
+
group: "Surfaces, text & form"
|
|
199
|
+
token: ".empty-state"
|
|
200
|
+
note: "The box a list shows when it has nothing in it."
|
|
201
|
+
resolve: [css:empty-state]
|
|
202
|
+
|
|
203
|
+
glass:
|
|
204
|
+
name: Glass
|
|
205
|
+
group: Glass
|
|
206
|
+
token: "config.surface_style = :glass"
|
|
207
|
+
note: "Frosts the cards and lights the controls from above; opt-in per app."
|
|
208
|
+
resolve: [config:surface_style, css:surface-glass-blur]
|
|
209
|
+
variants:
|
|
210
|
+
lit:
|
|
211
|
+
token: ".btn, .badge, .input-field under surface_style = :glass"
|
|
212
|
+
note: "Buttons, badges and inputs lit from above, in every state."
|
|
213
|
+
resolve: [config:surface_style]
|
|
214
|
+
|
|
215
|
+
glass_sidebar:
|
|
216
|
+
name: Glass Sidebar
|
|
217
|
+
group: Glass
|
|
218
|
+
token: "config.sidebar_style = :glass"
|
|
219
|
+
note: "A frosted link sidebar; opt-in per app."
|
|
220
|
+
resolve: [config:sidebar_style]
|
|
221
|
+
|
|
222
|
+
icon:
|
|
223
|
+
name: Icon
|
|
224
|
+
group: Glass
|
|
225
|
+
token: "studio_icon(\"rocket\", size: 22)"
|
|
226
|
+
note: "A line icon by name."
|
|
227
|
+
resolve: [helper:studio_icon]
|
|
228
|
+
|
|
229
|
+
scrim:
|
|
230
|
+
name: Scrim
|
|
231
|
+
group: "Tints & shades"
|
|
232
|
+
token: ".scrim"
|
|
233
|
+
note: "Theme-aware: a tint in light mode, a shade in dark."
|
|
234
|
+
resolve: [css:scrim, css:scrim-ink]
|
|
235
|
+
variants:
|
|
236
|
+
soft:
|
|
237
|
+
token: ".scrim .scrim-soft"
|
|
238
|
+
note: "A lighter wash: more of the media shows."
|
|
239
|
+
resolve: [css:scrim-soft]
|
|
240
|
+
strong:
|
|
241
|
+
token: ".scrim .scrim-strong"
|
|
242
|
+
note: "A heavier wash, for long or small text."
|
|
243
|
+
resolve: [css:scrim-strong]
|
|
244
|
+
|
|
245
|
+
tint:
|
|
246
|
+
name: Tint
|
|
247
|
+
group: "Tints & shades"
|
|
248
|
+
token: ".scrim-tint .scrim"
|
|
249
|
+
note: "Forced tint, in either theme. Dark ink."
|
|
250
|
+
resolve: [css:scrim-tint]
|
|
251
|
+
|
|
252
|
+
shade:
|
|
253
|
+
name: Shade
|
|
254
|
+
group: "Tints & shades"
|
|
255
|
+
token: ".scrim-shade .scrim"
|
|
256
|
+
note: "Forced shade, in either theme. Light ink."
|
|
257
|
+
resolve: [css:scrim-shade]
|
|
258
|
+
|
|
259
|
+
time_stamp:
|
|
260
|
+
name: Time Stamp
|
|
261
|
+
group: Time stamps
|
|
262
|
+
token: "at_time_tag(time)"
|
|
263
|
+
note: "When it happened, on the reader's clock; the date joins only when not today."
|
|
264
|
+
resolve: [helper:at_time_tag]
|
|
265
|
+
variants:
|
|
266
|
+
bare:
|
|
267
|
+
token: "at_time_tag(time, prefix: nil)"
|
|
268
|
+
note: "A bare clock, for a cell that already says at."
|
|
269
|
+
resolve: [helper:at_time_tag]
|
|
270
|
+
abroad:
|
|
271
|
+
token: "added in the browser, outside the US"
|
|
272
|
+
note: "Outside the US a flag trails the clock; inside, nothing."
|
|
273
|
+
resolve: [helper:at_time_tag]
|
|
274
|
+
|
|
275
|
+
loader:
|
|
276
|
+
name: Loader
|
|
277
|
+
group: Branded loader
|
|
278
|
+
token: "studio_loader"
|
|
279
|
+
note: "This app's icon in its theme colors."
|
|
280
|
+
resolve: [helper:studio_loader, css:studio-loader]
|
|
281
|
+
variants:
|
|
282
|
+
breathe:
|
|
283
|
+
token: "config.loader = :breathe"
|
|
284
|
+
note: "The icon breathes in and out."
|
|
285
|
+
resolve: [helper:studio_loader]
|
|
286
|
+
liquid:
|
|
287
|
+
token: "config.loader = :liquid"
|
|
288
|
+
note: "The icon fills like a glass."
|
|
289
|
+
resolve: [helper:studio_loader]
|
|
290
|
+
orbit:
|
|
291
|
+
token: "config.loader = :orbit"
|
|
292
|
+
note: "A dot circles the icon."
|
|
293
|
+
resolve: [helper:studio_loader]
|
|
294
|
+
steel:
|
|
295
|
+
token: "config.loader = :steel"
|
|
296
|
+
note: "A light sweeps across the icon."
|
|
297
|
+
resolve: [helper:studio_loader]
|
|
298
|
+
weld_bead:
|
|
299
|
+
token: "config.loader = { style: :weld_bead, art: \"/brand/loader-weld_bead.svg\" }"
|
|
300
|
+
note: "A signature style: needs this app's own art file."
|
|
301
|
+
resolve: [helper:studio_loader]
|
|
302
|
+
sparkle:
|
|
303
|
+
token: "config.loader = { style: :sparkle, art: \"/brand/loader-sparkle.svg\" }"
|
|
304
|
+
note: "A signature style: needs this app's own art file."
|
|
305
|
+
resolve: [helper:studio_loader]
|
|
306
|
+
|
|
307
|
+
spinner:
|
|
308
|
+
name: Spinner
|
|
309
|
+
group: Loaders
|
|
310
|
+
token: "studio_loader style: :spinner"
|
|
311
|
+
note: "A small ring for a short wait beside text or in a control."
|
|
312
|
+
resolve: [helper:studio_loader, css:spinner]
|
|
313
|
+
variants:
|
|
314
|
+
css:
|
|
315
|
+
token: ".spinner (--spinner-size, --spinner-color)"
|
|
316
|
+
note: "The bare ring, tuned inline."
|
|
317
|
+
resolve: [css:spinner]
|
|
318
|
+
|
|
319
|
+
skeleton:
|
|
320
|
+
name: Skeleton
|
|
321
|
+
group: Loaders
|
|
322
|
+
token: "studio_skeleton :lines (or :avatar, :card, :media)"
|
|
323
|
+
note: "Grey shapes of the content to come, so the page keeps its layout."
|
|
324
|
+
resolve: [helper:studio_skeleton]
|
|
325
|
+
|
|
326
|
+
progress_bar:
|
|
327
|
+
name: Progress Bar
|
|
328
|
+
group: Loaders
|
|
329
|
+
token: "studio_loader style: :bar, percent: 62"
|
|
330
|
+
note: "A bar for a wait you can measure; with no percent it sweeps."
|
|
331
|
+
resolve: [helper:studio_loader, css:progress-meter]
|
|
332
|
+
variants:
|
|
333
|
+
css:
|
|
334
|
+
token: ".progress-meter (--progress)"
|
|
335
|
+
note: "The bare bar, with its label inked both sides of the fill."
|
|
336
|
+
resolve: [css:progress-meter]
|
|
337
|
+
|
|
338
|
+
typing_dots:
|
|
339
|
+
name: Typing Dots
|
|
340
|
+
group: Loaders
|
|
341
|
+
token: "studio_loader style: :dots"
|
|
342
|
+
note: "Three dots for a reply on its way, such as a chat answer."
|
|
343
|
+
resolve: [helper:studio_loader, css:loading-dots]
|
|
344
|
+
variants:
|
|
345
|
+
css:
|
|
346
|
+
token: ".loading-dots with 3 span children"
|
|
347
|
+
note: "The bare dots, tuned inline."
|
|
348
|
+
resolve: [css:loading-dots]
|
|
349
|
+
|
|
350
|
+
busy_button:
|
|
351
|
+
name: Busy Button
|
|
352
|
+
group: Loaders
|
|
353
|
+
token: "studio_loader_pending inside the .btn"
|
|
354
|
+
note: "The button keeps its place and shows that it is working."
|
|
355
|
+
resolve: [helper:studio_loader_pending]
|
|
356
|
+
|
|
357
|
+
overlay:
|
|
358
|
+
name: Overlay
|
|
359
|
+
group: Loaders
|
|
360
|
+
token: "window.StudioLoader.show(\"Caption\") / .hide()"
|
|
361
|
+
note: "The branded full-screen loader, for a page that takes longer than 350ms."
|
|
362
|
+
resolve: [helper:studio_loader_overlay, js:StudioLoader]
|
|
363
|
+
|
|
364
|
+
pull_refresh:
|
|
365
|
+
name: Pull Refresh
|
|
366
|
+
group: Loaders
|
|
367
|
+
token: "studio_pull_to_refresh"
|
|
368
|
+
note: "On a phone, pull down from the top of the page to reload it."
|
|
369
|
+
resolve: [helper:studio_pull_to_refresh]
|
|
370
|
+
|
|
371
|
+
infinite_scroll:
|
|
372
|
+
name: Infinite Scroll
|
|
373
|
+
group: Loaders
|
|
374
|
+
token: "studio_infinite_scroll id:, page:, next_url:"
|
|
375
|
+
note: "The next page loads as the list's end comes near."
|
|
376
|
+
resolve: [helper:studio_infinite_scroll]
|
|
377
|
+
|
|
378
|
+
ping:
|
|
379
|
+
name: Ping
|
|
380
|
+
group: Motion primitives
|
|
381
|
+
token: ".ping .ping--dot"
|
|
382
|
+
note: "A dot with a ring that pings out from it."
|
|
383
|
+
resolve: [css:ping, css:ping--dot]
|
|
384
|
+
|
|
385
|
+
edge_fade:
|
|
386
|
+
name: Edge Fade
|
|
387
|
+
group: Motion primitives
|
|
388
|
+
token: ".fade-edge-y (or -t, -b, -l, -r, -x)"
|
|
389
|
+
note: "Fades a scroller's content out at its edges."
|
|
390
|
+
resolve: [css:fade-edge-y]
|
|
391
|
+
|
|
392
|
+
gradient_text:
|
|
393
|
+
name: Gradient Text
|
|
394
|
+
group: Effects
|
|
395
|
+
token: ".text-gradient"
|
|
396
|
+
note: "Fills the text with a gradient."
|
|
397
|
+
resolve: [css:text-gradient]
|
|
398
|
+
|
|
399
|
+
glow:
|
|
400
|
+
name: Glow
|
|
401
|
+
group: Effects
|
|
402
|
+
token: ".studio-glow"
|
|
403
|
+
note: "A soft colored halo around the element."
|
|
404
|
+
resolve: [css:studio-glow]
|
|
405
|
+
variants:
|
|
406
|
+
pulse:
|
|
407
|
+
token: ".studio-glow--pulse"
|
|
408
|
+
note: "The halo breathes."
|
|
409
|
+
resolve: [css:studio-glow--pulse]
|
|
410
|
+
|
|
411
|
+
frost:
|
|
412
|
+
name: Frost
|
|
413
|
+
group: Effects
|
|
414
|
+
token: ".surface-glass"
|
|
415
|
+
note: "A frosted panel over whatever is behind it."
|
|
416
|
+
resolve: [css:surface-glass]
|
|
417
|
+
|
|
418
|
+
conic_wash:
|
|
419
|
+
name: Conic Wash
|
|
420
|
+
group: Effects
|
|
421
|
+
token: ".conic-surface"
|
|
422
|
+
note: "A slow rotating gradient behind the content."
|
|
423
|
+
resolve: [css:conic-surface]
|
|
424
|
+
|
|
425
|
+
level_badge:
|
|
426
|
+
name: Level Badge
|
|
427
|
+
group: Leveling
|
|
428
|
+
token: ".level-badge .level-badge-1 … .level-badge-10"
|
|
429
|
+
note: "The ten level pills, plainest to holographic."
|
|
430
|
+
resolve: [css:level-badge, css:level-badge-10]
|
|
431
|
+
|
|
432
|
+
level_pop:
|
|
433
|
+
name: Level Pop
|
|
434
|
+
group: Leveling
|
|
435
|
+
token: ".level-up-pop"
|
|
436
|
+
note: "Pops a level badge with a glow when it changes."
|
|
437
|
+
resolve: [css:level-up-pop]
|
|
438
|
+
|
|
439
|
+
site_footer:
|
|
440
|
+
name: Site Footer
|
|
441
|
+
group: Site footer and booking
|
|
442
|
+
token: "studio_site_footer"
|
|
443
|
+
note: "The public footer, in one line at the end of the layout body."
|
|
444
|
+
resolve: [helper:studio_site_footer]
|
|
445
|
+
|
|
446
|
+
booking_link:
|
|
447
|
+
name: Booking Link
|
|
448
|
+
group: Site footer and booking
|
|
449
|
+
token: "studio_booking_link \"Schedule a call\""
|
|
450
|
+
note: "Opens the booking calendar in a popup."
|
|
451
|
+
resolve: [helper:studio_booking_link]
|
|
452
|
+
|
|
453
|
+
booking_frame:
|
|
454
|
+
name: Booking Frame
|
|
455
|
+
group: Site footer and booking
|
|
456
|
+
token: "studio_booking_frame"
|
|
457
|
+
note: "The booking calendar on a page, whole at rest."
|
|
458
|
+
resolve: [helper:studio_booking_frame]
|
|
459
|
+
variants:
|
|
460
|
+
cropped:
|
|
461
|
+
token: "config.booking_crop = { top:, bottom:, frame_height: }"
|
|
462
|
+
note: "Cropped to the slot picker at rest; bin/booking-crop-measure prints yours."
|
|
463
|
+
resolve: [config:booking_crop]
|
|
464
|
+
|
|
465
|
+
board:
|
|
466
|
+
name: Board
|
|
467
|
+
group: Tasks
|
|
468
|
+
token: "render \"studio/board/board\", columns:, card_partial:"
|
|
469
|
+
note: "Columns of cards to drag between; a move saves itself."
|
|
470
|
+
resolve: [partial:studio/board/board]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# studio/_page_header's one piece of Ruby: the deprecation note for its old free-HTML
|
|
5
|
+
# slot (docs/PAGE_HEADER.md, "The four kinds of control").
|
|
6
|
+
#
|
|
7
|
+
# actions_html still renders for one release. In development only, the first render
|
|
8
|
+
# that passes it writes one warning to the log per call site, so a host sees each
|
|
9
|
+
# page it has to move and no page repeats.
|
|
10
|
+
module PageHeader
|
|
11
|
+
ACTIONS_HTML_WARNING =
|
|
12
|
+
"studio/_page_header: actions_html is deprecated and goes in the next release. " \
|
|
13
|
+
"Pass tabs: or anchors:, menu:, action: and tools_html: instead (docs/PAGE_HEADER.md)."
|
|
14
|
+
|
|
15
|
+
@warned = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# site: the caller's "path:line", so each page warns once. env and logger default to
|
|
20
|
+
# Rails' and are injectable for a test.
|
|
21
|
+
def deprecate_actions_html(site: nil, env: rails_env, logger: rails_logger)
|
|
22
|
+
return false unless warning_enabled?(env: env) && logger
|
|
23
|
+
|
|
24
|
+
key = site.to_s
|
|
25
|
+
first = @mutex.synchronize { @warned[key] ? false : (@warned[key] = true) }
|
|
26
|
+
return false unless first
|
|
27
|
+
|
|
28
|
+
logger.warn([ACTIONS_HTML_WARNING, (" Called from #{site}." if site)].compact.join)
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Whether a render should look for its call site at all: development only, so no
|
|
33
|
+
# other environment pays for the caller walk.
|
|
34
|
+
def warning_enabled?(env: rails_env)
|
|
35
|
+
env.to_s == "development"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reset_warnings!
|
|
39
|
+
@mutex.synchronize { @warned.clear }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def rails_env
|
|
45
|
+
defined?(::Rails) && ::Rails.respond_to?(:env) ? ::Rails.env : nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def rails_logger
|
|
49
|
+
defined?(::Rails) && ::Rails.respond_to?(:logger) ? ::Rails.logger : nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Studio
|
|
6
|
+
# The trick names on /admin/style (config/studio_tricks.yml). Alex McRitchie, 2026-11-06:
|
|
7
|
+
# "I really just need to have a good naming convention so I can convey an idea to my
|
|
8
|
+
# agents." Every specimen card reads its name, note and token from here, and
|
|
9
|
+
# docs/TRICKS.md, the agent-facing glossary, is generated from the same file
|
|
10
|
+
# (bin/tricks-doc), so the page and the doc cannot say different things.
|
|
11
|
+
#
|
|
12
|
+
# Studio::Tricks.fetch("sheen") # => the Sheen trick
|
|
13
|
+
# Studio::Tricks.fetch("sheen", "loop") # => "Sheen · loop", its own card's trick
|
|
14
|
+
# Studio::Tricks.agent_line("sheen") # => "Sheen — .sheen; Sheen · loop — .sheen--loop"
|
|
15
|
+
module Tricks
|
|
16
|
+
PATH = File.expand_path("../../config/studio_tricks.yml", __dir__)
|
|
17
|
+
DOC_PATH = File.expand_path("../../docs/TRICKS.md", __dir__)
|
|
18
|
+
SEPARATOR = " · "
|
|
19
|
+
|
|
20
|
+
# One trick, or one variant of one (base is then its parent).
|
|
21
|
+
Trick = Struct.new(:key, :variant, :name, :group, :token, :note, :resolve, :variants, :base,
|
|
22
|
+
keyword_init: true)
|
|
23
|
+
|
|
24
|
+
class UnknownTrick < KeyError; end
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Every top-level trick, in file order (which is page order).
|
|
29
|
+
def all
|
|
30
|
+
@all ||= load_all
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reload!
|
|
34
|
+
@all = nil
|
|
35
|
+
all
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def fetch(key, variant = nil)
|
|
39
|
+
trick = all.find { |t| t.key == key.to_s } or
|
|
40
|
+
raise UnknownTrick, "no trick #{key.inspect} in config/studio_tricks.yml"
|
|
41
|
+
return trick if variant.nil?
|
|
42
|
+
|
|
43
|
+
trick.variants.find { |v| v.variant == variant.to_s } or
|
|
44
|
+
raise UnknownTrick, "trick #{key.inspect} has no variant #{variant.inspect}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Every name on the guide, variants included.
|
|
48
|
+
def names
|
|
49
|
+
all.flat_map { |t| [t.name, *t.variants.map(&:name)] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# What the card's one Copy puts on the clipboard: the name and what to write, so it
|
|
53
|
+
# can be pasted straight to an agent. A trick's line carries its variants; a
|
|
54
|
+
# variant's line is its own.
|
|
55
|
+
def agent_line(key, variant = nil)
|
|
56
|
+
trick = fetch(key, variant)
|
|
57
|
+
[trick, *trick.variants].map { |t| "#{t.name} — #{t.token}" }.join("; ")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def markdown
|
|
61
|
+
lines = []
|
|
62
|
+
lines << "# Tricks"
|
|
63
|
+
lines << ""
|
|
64
|
+
lines << "<!-- Generated from config/studio_tricks.yml by bin/tricks-doc. Edit the YAML, then run it. -->"
|
|
65
|
+
lines << ""
|
|
66
|
+
lines << "The names of the specimens on `/admin/style`. Say the name and an agent writes the"
|
|
67
|
+
lines << "token: \"add a Sheen\" means `.sheen` on a `.btn`; \"make it Sheen · loop\" adds"
|
|
68
|
+
lines << "`.sheen--loop`."
|
|
69
|
+
lines << ""
|
|
70
|
+
lines << "A name is one or two Title-case words. A variant is the name, a middle dot and a"
|
|
71
|
+
lines << "lower-case word (`Border Glow · clear`). Every name is unique across the guide."
|
|
72
|
+
all.group_by(&:group).each do |group, tricks|
|
|
73
|
+
lines << ""
|
|
74
|
+
lines << "## #{group}"
|
|
75
|
+
lines << ""
|
|
76
|
+
lines << "| Name | Write | What it does |"
|
|
77
|
+
lines << "|------|-------|--------------|"
|
|
78
|
+
tricks.each do |trick|
|
|
79
|
+
lines << row("**#{trick.name}**", trick)
|
|
80
|
+
trick.variants.each { |v| lines << row(v.name, v) }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
lines << ""
|
|
84
|
+
lines.join("\n")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def row(label, trick)
|
|
88
|
+
"| #{label} | `#{trick.token}` | #{trick.note} |"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def load_all
|
|
92
|
+
YAML.safe_load_file(PATH).map do |key, entry|
|
|
93
|
+
base = build(key: key, name: entry.fetch("name"), group: entry.fetch("group"), entry: entry)
|
|
94
|
+
base.variants = (entry["variants"] || {}).map do |word, v|
|
|
95
|
+
build(key: key, variant: word, name: "#{base.name}#{SEPARATOR}#{word.tr('_', ' ')}",
|
|
96
|
+
group: base.group, entry: v, base: base)
|
|
97
|
+
end
|
|
98
|
+
base
|
|
99
|
+
end.freeze
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build(key:, name:, group:, entry:, variant: nil, base: nil)
|
|
103
|
+
Trick.new(key: key, variant: variant, name: name, group: group,
|
|
104
|
+
token: entry.fetch("token"), note: entry.fetch("note"),
|
|
105
|
+
resolve: Array(entry["resolve"]), variants: [], base: base)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private_class_method :row, :load_all, :build
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -45,6 +45,7 @@ require "studio/navbar_identity"
|
|
|
45
45
|
require "studio/navbar_wordmark"
|
|
46
46
|
require "studio/navbar_brand"
|
|
47
47
|
require "studio/loader"
|
|
48
|
+
require "studio/tricks"
|
|
48
49
|
require "studio/auth_labels"
|
|
49
50
|
require "studio/profile_sections"
|
|
50
51
|
require "studio/profile_image"
|
|
@@ -78,6 +79,7 @@ require "studio/survey"
|
|
|
78
79
|
require "studio/survey/breakdown"
|
|
79
80
|
require "studio/survey/export"
|
|
80
81
|
require "studio/crumbs"
|
|
82
|
+
require "studio/page_header"
|
|
81
83
|
|
|
82
84
|
module Studio
|
|
83
85
|
mattr_accessor :app_name, default: "Studio"
|
|
@@ -2130,10 +2132,10 @@ module Studio
|
|
|
2130
2132
|
# the OAuth popup DO stay app-side.
|
|
2131
2133
|
#
|
|
2132
2134
|
# The gate is auth_methods, NOT auth_methods && feature?(:web3), even though
|
|
2133
|
-
# an app's auth modal computes its wallet button from both (
|
|
2134
|
-
#
|
|
2135
|
-
#
|
|
2136
|
-
#
|
|
2135
|
+
# an app's auth modal computes its wallet button from both (the engine's
|
|
2136
|
+
# standard card is studio/auth/_sign_in_modal, which takes the wallet
|
|
2137
|
+
# button as an extras: partial; until turf-monster adopts it, its own
|
|
2138
|
+
# app/views/modals/_auth.html.erb computes the button). Deliberate: auth_methods says
|
|
2137
2139
|
# which CREDENTIALS this app accepts, features gates PRODUCT SURFACES, and
|
|
2138
2140
|
# these three paths are the credential exchange itself. phantom_callback is
|
|
2139
2141
|
# the mobile deep-link RETURN url — a wallet app that declared :wallet but
|