studio-engine 0.14.0 → 0.16.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: 8540d767e024bc8314d5471788d3f430e79a1e17c2dc49ba5c28a61f84f74646
4
- data.tar.gz: ba45685b2e743504e7c60a59f35a6a41074d72b8ef1c60460f3f6b65d34d65f6
3
+ metadata.gz: 382ba5d1064ff3ef73e0d5d7622b44da80477a9915a7a619a08aad9caebe40ca
4
+ data.tar.gz: 041e44a225267824d720079316e66dbd598402f2710dc5e668e5302cb75a47ed
5
5
  SHA512:
6
- metadata.gz: 6dabad0d6e3946fb10b990a4b525cd3e03c248eabfebf1408cd790cfaba678df19fe115d7928d8eac247ada917cf9479e7857afe08418d9e9944b0ac7a5ed975
7
- data.tar.gz: fbd613547387db6bd201d0f03d66685accffcba10adaa8f68349b3fbe785ea275e5c4c6a3f0dff2d15bdc7884294fa995066c7b20830319e54831219bdf6c10c
6
+ metadata.gz: 7d2a59ad47d5ee598deaf0f81a9fce457ef81a183afa109d9ab5322845140fe5e27920e893fb29df45b6d8b3c70b52bfcd1b6ead57981652466455ed8037ba3c
7
+ data.tar.gz: 630d7bf20e4d5a881a7efc790f4fb4ced77f6832b0608bae449a8256b129caa3b3b3b19d1b52b4b7128e5901324683514c84ffaedea6eaf593403fb62af0f22f
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
4
4
 
5
+ ## 0.16.0 — 2026-07-26
6
+
7
+ ### Added
8
+
9
+ - **Motion / effect primitive layer** — a new, OPT-IN stylesheet at
10
+ `app/assets/tailwind/studio_engine/engine-motion.css` consolidating seven
11
+ reusable animation + visual-effect primitives that had drifted across the MS
12
+ and TM apps: `.studio-border-glow` (mask-composite rainbow border),
13
+ `.spinner` (one canonical border spinner), `.loading-dots` (three bouncing
14
+ dots), `.sheen` (sweep-of-light), `.ping` (expanding pulse ring),
15
+ `.fade-edge` (static mask-image edge fade), and `.progress-meter` (a
16
+ dual-layer color-flip progress bar). Plain `.class` + `@keyframes` (not
17
+ `@utility`), themed through the 7-role CSS custom properties, every knob a
18
+ tunable CSS var. It does NOT auto-bundle: `tailwindcss:engines` only
19
+ generates an entry for the file literally named `engine.css`, so consumers
20
+ adopt this layer deliberately with a single `@import` (see the file header).
21
+ App-side de-fork of the local copies is a follow-up task.
22
+
5
23
  ## 0.14.0 — 2026-07-22
6
24
 
7
25
  ### Added
@@ -0,0 +1,440 @@
1
+ /*
2
+ studio-engine MOTION / EFFECT primitives — the shared animation + visual-effect
3
+ layer. Seven reusable primitives, consolidated from the per-app copies that
4
+ drifted across mcritchie-studio (MS) and turf-monster (TM):
5
+
6
+ .studio-border-glow mask-composite rainbow border glow (extracted, MS)
7
+ .spinner one canonical border spinner (reconciled, MS+TM)
8
+ .loading-dots three bouncing dots (extracted, TM)
9
+ .sheen sweep-of-light across any element (reconciled, TM)
10
+ .ping expanding pulse ring (extracted, TM)
11
+ .fade-edge static mask-image edge fade (authored fresh)
12
+ .progress-meter dual-layer color-flip progress bar (authored fresh)
13
+
14
+ ─────────────────────────────────────────────────────────────────────────────
15
+ OPT-IN — this layer does NOT auto-bundle.
16
+
17
+ tailwindcss-rails' `tailwindcss:engines` task (Tailwindcss::Engines.bundle)
18
+ generates a `builds/tailwind/<engine>.css` entry ONLY for the file named
19
+ exactly `engine.css`. This file is `engine-motion.css`, so that matcher skips
20
+ it BY CONSTRUCTION — it never rides the auto-generated `studio_engine.css`
21
+ entry that `engine.css` uses, and consumers must adopt it deliberately.
22
+
23
+ Adopt with ONE line in the consuming app's app/assets/tailwind/application.css,
24
+ AFTER `@import "tailwindcss";` (and after your `../builds/tailwind/studio_engine`
25
+ import so the theme role vars are in scope):
26
+
27
+ @import "STUDIO_ENGINE_PATH/app/assets/tailwind/studio_engine/engine-motion.css";
28
+
29
+ Resolve STUDIO_ENGINE_PATH the same way config/tailwind.config.js already
30
+ resolves the shared preset — `bundle show studio-engine`. A one-time helper the
31
+ app-side de-fork task can wire (mirrors the engines contract):
32
+
33
+ # config/tailwind.config.js already computes this:
34
+ # const studioPath = execSync('bundle show studio-engine').toString().trim()
35
+ # emit an absolute @import into your app CSS (or a tiny generated entry).
36
+
37
+ App-side wiring + removing the forked local copies lands in the follow-up
38
+ adoption task; until then the MS/TM local copies stay canonical.
39
+
40
+ ─────────────────────────────────────────────────────────────────────────────
41
+ DESIGN RULES
42
+
43
+ - Plain `.class` + `@keyframes`, NOT `@utility`. A Tailwind v4 `@utility`
44
+ redeclaration is ADDITIVE (both definitions emit), which is the wrong shape
45
+ for net-new primitives a consumer might transiently co-load with a local
46
+ copy. Plain classes sidestep that entirely.
47
+ - Color flows through the 7-role theme CSS custom properties emitted by
48
+ studio_theme_css_tag (`--color-cta`, `--color-surface`, `--color-inset`,
49
+ `--color-text`, `--color-border-strong`, `--color-primary`, ...), so every
50
+ primitive restyles itself in dark AND light mode with zero extra work.
51
+ - Every tuning knob (duration, size, color, %, delay) is a CSS custom property
52
+ with a sensible themed default, so a caller retunes via `style="--x: ..."`
53
+ or a `:root` override WITHOUT forking the rule.
54
+ - Namespacing: the two extracted primitives keep their source names
55
+ (`.studio-border-glow`, `.loading-dots`) so the follow-up de-fork is a pure
56
+ delete-the-local-copy swap with no view churn; their gem rules are byte-
57
+ compatible with the app copies, so a transient co-load is a harmless
58
+ duplicate. The reconciled/fresh primitives use fresh generic names
59
+ (`.spinner`, `.sheen`, `.ping`, `.fade-edge`, `.progress-meter`) that no app
60
+ class currently uses. Animation names that would clash with a Tailwind
61
+ built-in of DIFFERENT content are namespaced (`ping-pulse`, not `ping`);
62
+ `spin` is kept because it is byte-identical to Tailwind's built-in.
63
+ */
64
+
65
+ /* =============================================================================
66
+ 1. studio-border-glow — mask-composite rainbow border glow
67
+ Extracted verbatim from mcritchie-studio app/assets/tailwind/application.css.
68
+ A ::before draws the crisp animated rainbow border (masked to the padding
69
+ ring via mask-composite), a ::after draws a blurred halo of the same gradient.
70
+
71
+ Usage: <div class="studio-border-glow rounded-xl"> ... </div>
72
+ Tune: --studio-border-glow-duration / -opacity / -halo-opacity /
73
+ -halo-blur / -angle / -offset / -gradient
74
+ ========================================================================== */
75
+ .studio-border-glow {
76
+ --studio-border-glow-opacity: 0.82;
77
+ --studio-border-glow-halo-opacity: 0.28;
78
+ --studio-border-glow-halo-blur: 18px;
79
+ --studio-border-glow-angle: 45deg;
80
+ --studio-border-glow-offset: 0%;
81
+ --studio-border-glow-duration: 20s;
82
+ --studio-border-glow-animation: studioBorderGlowSteam var(--studio-border-glow-duration) linear infinite;
83
+ --studio-border-glow-gradient:
84
+ linear-gradient(
85
+ var(--studio-border-glow-angle, 45deg),
86
+ #fb0094,
87
+ #00c4ff,
88
+ #34d399,
89
+ #fbbf24,
90
+ #ff2d55,
91
+ #fb0094,
92
+ #00c4ff,
93
+ #34d399,
94
+ #fbbf24,
95
+ #ff2d55
96
+ );
97
+ isolation: isolate;
98
+ position: relative;
99
+ }
100
+
101
+ .studio-border-glow::before,
102
+ .studio-border-glow::after {
103
+ content: "";
104
+ position: absolute;
105
+ pointer-events: none;
106
+ border-radius: inherit;
107
+ background: var(--studio-border-glow-gradient);
108
+ background-size: 400%;
109
+ animation: var(--studio-border-glow-animation);
110
+ }
111
+
112
+ .studio-border-glow::before {
113
+ -webkit-mask:
114
+ linear-gradient(#000 0 0) content-box,
115
+ linear-gradient(#000 0 0);
116
+ -webkit-mask-composite: xor;
117
+ mask:
118
+ linear-gradient(#000 0 0) content-box,
119
+ linear-gradient(#000 0 0);
120
+ mask-composite: exclude;
121
+ inset: -2px;
122
+ z-index: -1;
123
+ padding: 2px;
124
+ opacity: var(--studio-border-glow-opacity);
125
+ }
126
+
127
+ .studio-border-glow::after {
128
+ -webkit-mask:
129
+ linear-gradient(#000 0 0) content-box,
130
+ linear-gradient(#000 0 0);
131
+ -webkit-mask-composite: xor;
132
+ mask:
133
+ linear-gradient(#000 0 0) content-box,
134
+ linear-gradient(#000 0 0);
135
+ mask-composite: exclude;
136
+ inset: -10px;
137
+ z-index: -2;
138
+ padding: 10px;
139
+ filter: blur(var(--studio-border-glow-halo-blur));
140
+ opacity: var(--studio-border-glow-halo-opacity);
141
+ transform: translateZ(0);
142
+ }
143
+
144
+ @keyframes studioBorderGlowSteam {
145
+ 0% {
146
+ background-position: var(--studio-border-glow-offset, 0%) 0;
147
+ }
148
+ 100% {
149
+ background-position: calc(var(--studio-border-glow-offset, 0%) + 400%) 0;
150
+ }
151
+ }
152
+
153
+ /* =============================================================================
154
+ 2. spinner — one canonical border spinner
155
+ Reconciles MS .hb-spinner / .aa-spinner (heartbeat.css) and TM .cta-spinner /
156
+ hold-spinner into a single generic border spinner. Sized + colored entirely
157
+ through CSS vars; defaults to the CTA role so it reads on any surface.
158
+
159
+ Usage: <span class="spinner"></span>
160
+ <span class="spinner" style="--spinner-size: 2rem"></span>
161
+ on a colored button: <span class="spinner" style="--spinner-color: currentColor"></span>
162
+ Tune: --spinner-size / --spinner-thickness / --spinner-track /
163
+ --spinner-color / --spinner-speed
164
+ ========================================================================== */
165
+ .spinner {
166
+ display: inline-block;
167
+ width: var(--spinner-size, 1rem);
168
+ height: var(--spinner-size, 1rem);
169
+ border: var(--spinner-thickness, 2px) solid var(--spinner-track, var(--color-border-strong));
170
+ border-top-color: var(--spinner-color, var(--color-cta));
171
+ border-radius: 9999px;
172
+ animation: spin var(--spinner-speed, 0.7s) linear infinite;
173
+ }
174
+
175
+ @keyframes spin {
176
+ to {
177
+ transform: rotate(360deg);
178
+ }
179
+ }
180
+
181
+ /* =============================================================================
182
+ 3. loading-dots — three bouncing dots
183
+ Extracted verbatim from turf-monster (the modal card-header "we're working on
184
+ it" idiom). Emit exactly three child <span>s.
185
+
186
+ Usage: <span class="loading-dots"><span></span><span></span><span></span></span>
187
+ Tune: --loading-dots-color / --loading-dots-size / --loading-dots-gap /
188
+ --loading-dots-speed
189
+ ========================================================================== */
190
+ .loading-dots {
191
+ display: inline-flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ gap: var(--loading-dots-gap, 6px);
195
+ }
196
+
197
+ .loading-dots span {
198
+ width: var(--loading-dots-size, 8px);
199
+ height: var(--loading-dots-size, 8px);
200
+ border-radius: 9999px;
201
+ background: var(--loading-dots-color, var(--color-primary));
202
+ animation: loading-dots-bounce var(--loading-dots-speed, 1.2s) ease-in-out infinite both;
203
+ }
204
+
205
+ .loading-dots span:nth-child(1) { animation-delay: -0.32s; }
206
+ .loading-dots span:nth-child(2) { animation-delay: -0.16s; }
207
+ .loading-dots span:nth-child(3) { animation-delay: 0s; }
208
+
209
+ @keyframes loading-dots-bounce {
210
+ 0%, 80%, 100% { transform: translateY(0) scale(0.85); opacity: 0.45; }
211
+ 40% { transform: translateY(-6px) scale(1); opacity: 1; }
212
+ }
213
+
214
+ /* =============================================================================
215
+ 4. sheen — a sweep-of-light across any element
216
+ Reconciles TM's badge-sheen-sweep (a one-shot diagonal wink) and seedsShimmer
217
+ (a repeating sweep) into one wrapper primitive. A ::after paints a moving band
218
+ of light clipped to the element's own radius. One-shot by default; add
219
+ .sheen--loop for a continuous shimmer.
220
+
221
+ Usage: <span class="sheen"> ... </span> (one wink on render)
222
+ <span class="sheen sheen--loop"> ... </span> (continuous)
223
+ Tune: --sheen-color / --sheen-duration / --sheen-delay / --sheen-width
224
+ ========================================================================== */
225
+ .sheen {
226
+ position: relative;
227
+ overflow: hidden;
228
+ }
229
+
230
+ .sheen::after {
231
+ content: "";
232
+ position: absolute;
233
+ inset: 0;
234
+ border-radius: inherit;
235
+ pointer-events: none;
236
+ background: linear-gradient(
237
+ 90deg,
238
+ transparent calc(50% - var(--sheen-width, 15%)),
239
+ var(--sheen-color, rgba(255, 255, 255, 0.45)) 50%,
240
+ transparent calc(50% + var(--sheen-width, 15%))
241
+ );
242
+ background-size: 250% 100%;
243
+ background-repeat: no-repeat;
244
+ background-position: 200% 0;
245
+ animation: sheen-sweep var(--sheen-duration, 2.5s) ease-in-out var(--sheen-delay, 0s) 1 both;
246
+ }
247
+
248
+ .sheen.sheen--loop::after {
249
+ animation-iteration-count: infinite;
250
+ }
251
+
252
+ @keyframes sheen-sweep {
253
+ from { background-position: 200% 0; }
254
+ to { background-position: -100% 0; }
255
+ }
256
+
257
+ /* =============================================================================
258
+ 5. ping — an expanding pulse ring
259
+ Extracted from TM's chat-live-ping ("Live" radar ping). A ring that scales up
260
+ and fades, richer than Tailwind's animate-ping. Place it as a sibling ring
261
+ behind a solid dot, or use the wrapper form for a dot + ring in one element.
262
+
263
+ Usage (ring only): <span class="ping"></span>
264
+ Usage (dot + ring): <span class="ping ping--dot"></span>
265
+ Tune: --ping-color / --ping-size / --ping-scale / --ping-speed /
266
+ --ping-opacity
267
+ NB: keyframe is `ping-pulse` (NOT `ping`) — Tailwind's built-in `ping`
268
+ keyframe has different content, so we namespace to avoid an override.
269
+ ========================================================================== */
270
+ .ping {
271
+ display: inline-block;
272
+ width: var(--ping-size, 0.625rem);
273
+ height: var(--ping-size, 0.625rem);
274
+ border-radius: 9999px;
275
+ background: var(--ping-color, var(--color-cta));
276
+ animation: ping-pulse var(--ping-speed, 1.8s) cubic-bezier(0, 0, 0.2, 1) infinite;
277
+ }
278
+
279
+ /* Dot + ring: a solid center dot with the expanding ring animating behind it. */
280
+ .ping.ping--dot {
281
+ position: relative;
282
+ animation: none;
283
+ }
284
+
285
+ .ping.ping--dot::after {
286
+ content: "";
287
+ position: absolute;
288
+ inset: 0;
289
+ border-radius: 9999px;
290
+ background: var(--ping-color, var(--color-cta));
291
+ animation: ping-pulse var(--ping-speed, 1.8s) cubic-bezier(0, 0, 0.2, 1) infinite;
292
+ }
293
+
294
+ @keyframes ping-pulse {
295
+ 0% { transform: scale(1); opacity: var(--ping-opacity, 0.6); }
296
+ 70%, 100% { transform: scale(var(--ping-scale, 2.6)); opacity: 0; }
297
+ }
298
+
299
+ /* =============================================================================
300
+ 6. fade-edge — static mask-image edge fade (authored fresh)
301
+ Softens the edge of a scroll container so content fades out instead of hard-
302
+ clipping. Pure mask, no animation. Directional variants; -x / -y fade both
303
+ ends of an axis.
304
+
305
+ Usage: <div class="fade-edge-b overflow-y-auto"> ...long content... </div>
306
+ <div class="fade-edge-y overflow-y-auto"> ... </div>
307
+ Tune: --fade-edge-size (how far the fade reaches in from the edge)
308
+ ========================================================================== */
309
+ .fade-edge-t,
310
+ .fade-edge-b,
311
+ .fade-edge-l,
312
+ .fade-edge-r,
313
+ .fade-edge-x,
314
+ .fade-edge-y {
315
+ --fade-edge-size: 2rem;
316
+ }
317
+
318
+ .fade-edge-t {
319
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-edge-size));
320
+ mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-edge-size));
321
+ }
322
+ .fade-edge-b {
323
+ -webkit-mask-image: linear-gradient(to top, transparent 0, #000 var(--fade-edge-size));
324
+ mask-image: linear-gradient(to top, transparent 0, #000 var(--fade-edge-size));
325
+ }
326
+ .fade-edge-l {
327
+ -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-edge-size));
328
+ mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-edge-size));
329
+ }
330
+ .fade-edge-r {
331
+ -webkit-mask-image: linear-gradient(to left, transparent 0, #000 var(--fade-edge-size));
332
+ mask-image: linear-gradient(to left, transparent 0, #000 var(--fade-edge-size));
333
+ }
334
+ .fade-edge-y {
335
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-edge-size), #000 calc(100% - var(--fade-edge-size)), transparent 100%);
336
+ mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-edge-size), #000 calc(100% - var(--fade-edge-size)), transparent 100%);
337
+ }
338
+ .fade-edge-x {
339
+ -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-edge-size), #000 calc(100% - var(--fade-edge-size)), transparent 100%);
340
+ mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-edge-size), #000 calc(100% - var(--fade-edge-size)), transparent 100%);
341
+ }
342
+
343
+ /* =============================================================================
344
+ 7. progress-meter — dual-layer color-flip progress bar (authored fresh)
345
+ The label reads correctly over BOTH the track and the fill: two identical,
346
+ pixel-aligned text layers stack, and the FILL layer (with its on-fill label
347
+ color) is revealed only over the filled width via clip-path. Where fill covers
348
+ the label you see the on-fill color; where the track shows through you see the
349
+ on-track color — the text flips color exactly at the fill boundary. Drive fill
350
+ with the --progress custom property.
351
+
352
+ Usage:
353
+ <div class="progress-meter" style="--progress: 62%">
354
+ <div class="progress-meter-track"><span class="progress-meter-label">62%</span></div>
355
+ <div class="progress-meter-fill"><span class="progress-meter-label">62%</span></div>
356
+ </div>
357
+ Indeterminate (no known %): add .progress-meter--indeterminate.
358
+ Tune: --progress / --progress-fill / --progress-track /
359
+ --progress-label / --progress-label-on-fill /
360
+ --progress-height / --progress-radius
361
+ ========================================================================== */
362
+ .progress-meter {
363
+ position: relative;
364
+ height: var(--progress-height, 1.5rem);
365
+ border-radius: var(--progress-radius, 9999px);
366
+ overflow: hidden;
367
+ background: var(--progress-track, var(--color-inset));
368
+ }
369
+
370
+ .progress-meter-track,
371
+ .progress-meter-fill {
372
+ position: absolute;
373
+ inset: 0;
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ }
378
+
379
+ .progress-meter-track {
380
+ background: var(--progress-track, var(--color-inset));
381
+ }
382
+
383
+ .progress-meter-fill {
384
+ background: var(--progress-fill, var(--color-cta));
385
+ /* Reveal the fill layer only across the filled width. */
386
+ clip-path: inset(0 calc(100% - var(--progress, 0%)) 0 0);
387
+ transition: clip-path 0.35s ease;
388
+ }
389
+
390
+ .progress-meter-label {
391
+ font-weight: 700;
392
+ font-variant-numeric: tabular-nums;
393
+ line-height: 1;
394
+ white-space: nowrap;
395
+ }
396
+
397
+ .progress-meter-track .progress-meter-label {
398
+ color: var(--progress-label, var(--color-text));
399
+ }
400
+
401
+ .progress-meter-fill .progress-meter-label {
402
+ color: var(--progress-label-on-fill, #fff);
403
+ }
404
+
405
+ /* Indeterminate: sweep the fill band across the bar. */
406
+ .progress-meter--indeterminate .progress-meter-fill {
407
+ clip-path: none;
408
+ width: 40%;
409
+ inset: 0 auto 0 0;
410
+ animation: progress-indeterminate 1.4s ease-in-out infinite;
411
+ }
412
+
413
+ .progress-meter--indeterminate .progress-meter-label {
414
+ display: none;
415
+ }
416
+
417
+ @keyframes progress-indeterminate {
418
+ 0% { transform: translateX(-100%); }
419
+ 100% { transform: translateX(250%); }
420
+ }
421
+
422
+ /* =============================================================================
423
+ Reduced-motion: honor the user preference. Animated primitives settle into a
424
+ sensible static resting state; the static primitives (fade-edge) are unaffected.
425
+ ========================================================================== */
426
+ @media (prefers-reduced-motion: reduce) {
427
+ .studio-border-glow::before,
428
+ .studio-border-glow::after,
429
+ .spinner,
430
+ .loading-dots span,
431
+ .sheen::after,
432
+ .ping,
433
+ .ping.ping--dot::after,
434
+ .progress-meter--indeterminate .progress-meter-fill {
435
+ animation: none;
436
+ }
437
+
438
+ .loading-dots span { opacity: 0.85; }
439
+ .sheen::after { display: none; }
440
+ }
@@ -62,6 +62,33 @@
62
62
 
63
63
  /* -- Button system (standardized across all Studio apps) ------------------- */
64
64
 
65
+ /*
66
+ Overridable variants — token contract.
67
+
68
+ Most filled variants sit on a theme role and darken on hover. Two of them
69
+ (btn-secondary, btn-neutral) are TOKEN-DRIVEN so a consuming app can retune
70
+ them WITHOUT redeclaring the @utility — a local `@utility` redeclaration is
71
+ ADDITIVE in Tailwind v4 (both definitions emit and the engine's rules leak in),
72
+ so forking is not a clean override. Set these on :root in the consumer's CSS:
73
+
74
+ --btn-secondary-bg base fill (default: var(--color-success))
75
+ --btn-secondary-bg-hover hover fill (default: same as base)
76
+ --btn-secondary-hover-filter hover filter (default: brightness(0.9))
77
+ --btn-neutral-bg base fill (default: var(--color-surface-alt))
78
+ --btn-neutral-bg-hover hover fill (default: var(--color-surface))
79
+ --btn-neutral-hover-filter hover filter (default: none)
80
+
81
+ With no tokens set, the compiled result is visually identical to the previous
82
+ hard-coded rules. Example — turf-monster's violet secondary + brightness neutral:
83
+
84
+ :root {
85
+ --btn-secondary-bg: #8e82fe; --btn-secondary-bg-hover: #7b6ef6;
86
+ --btn-secondary-hover-filter: none;
87
+ --btn-neutral-bg-hover: var(--color-surface-alt);
88
+ --btn-neutral-hover-filter: brightness(1.3);
89
+ }
90
+ */
91
+
65
92
  @utility btn {
66
93
  @apply inline-flex items-center justify-center font-bold rounded-lg transition px-6 py-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed;
67
94
  &:focus-visible {
@@ -78,14 +105,17 @@
78
105
  }
79
106
  }
80
107
 
81
- /* NOTE: btn-secondary and btn-success compile to IDENTICAL declarations (both
82
- sit on var(--color-success)) an intentional hub-canonical alias, kept as
83
- two names so existing markup in every consumer keeps working. */
108
+ /* btn-secondary DEFAULTS to the success role with no tokens set it compiles
109
+ like btn-success, preserving the hub-canonical alias every consumer relies on.
110
+ It is token-overridable (see the contract above) so an app whose secondary
111
+ action diverges from success (turf-monster's violet) retunes it via
112
+ --btn-secondary-* instead of forking the @utility. */
84
113
  @utility btn-secondary {
85
114
  @apply text-white;
86
- background-color: var(--color-success);
115
+ background-color: var(--btn-secondary-bg, var(--color-success));
87
116
  &:hover {
88
- filter: brightness(0.9);
117
+ background-color: var(--btn-secondary-bg-hover, var(--btn-secondary-bg, var(--color-success)));
118
+ filter: var(--btn-secondary-hover-filter, brightness(0.9));
89
119
  }
90
120
  }
91
121
 
@@ -130,10 +160,16 @@
130
160
  }
131
161
  }
132
162
 
163
+ /* btn-neutral DEFAULTS to the surface-alt base / surface hover pair, token-
164
+ overridable the same way (--btn-neutral-*): turf-monster keeps its
165
+ brightness(1.3) hover by setting --btn-neutral-hover-filter and pinning
166
+ --btn-neutral-bg-hover back to the base so the fill does not change. */
133
167
  @utility btn-neutral {
134
- @apply bg-surface-alt text-heading border border-strong;
168
+ @apply text-heading border border-strong;
169
+ background-color: var(--btn-neutral-bg, var(--color-surface-alt));
135
170
  &:hover {
136
- @apply bg-surface;
171
+ background-color: var(--btn-neutral-bg-hover, var(--color-surface));
172
+ filter: var(--btn-neutral-hover-filter, none);
137
173
  }
138
174
  }
139
175
 
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.14.0"
2
+ VERSION = "0.16.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-07-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -172,6 +171,7 @@ files:
172
171
  - app/assets/images/ses-favicon.png
173
172
  - app/assets/javascripts/studio/sticky_table_header.js
174
173
  - app/assets/stylesheets/studio/sticky_table_header.css
174
+ - app/assets/tailwind/studio_engine/engine-motion.css
175
175
  - app/assets/tailwind/studio_engine/engine.css
176
176
  - app/controllers/concerns/solana/session_auth.rb
177
177
  - app/controllers/concerns/studio/admin_models.rb
@@ -298,7 +298,6 @@ metadata:
298
298
  source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
299
299
  bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
300
300
  changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
301
- post_install_message:
302
301
  rdoc_options: []
303
302
  require_paths:
304
303
  - lib
@@ -313,8 +312,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
313
312
  - !ruby/object:Gem::Version
314
313
  version: '0'
315
314
  requirements: []
316
- rubygems_version: 3.5.22
317
- signing_key:
315
+ rubygems_version: 4.0.9
318
316
  specification_version: 4
319
317
  summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
320
318
  image caching