studio-engine 0.128.0 → 0.130.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 +86 -0
- data/README.md +5 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +148 -56
- data/app/assets/tailwind/studio_engine/engine.css +203 -19
- data/app/controllers/concerns/studio/error_handling.rb +17 -0
- data/app/helpers/studio/fizz_helper.rb +5 -0
- data/app/javascript/studio/agent_chat.js +74 -3
- data/app/javascript/studio/agent_chat_voice.js +17 -1
- data/app/javascript/studio/alpine_stores.js +80 -22
- data/app/javascript/studio/controllers/style_section_nav_controller.js +4 -4
- data/app/javascript/studio/pinned_stack.js +112 -4
- data/app/views/registrations/new.html.erb +4 -3
- data/app/views/sessions/new.html.erb +4 -3
- data/app/views/studio/_fizz.html.erb +26 -0
- data/app/views/studio/_fizz_layer.html.erb +4 -1
- data/app/views/studio/_hold_button.html.erb +1 -2
- data/app/views/studio/_page_header.html.erb +3 -2
- data/app/views/studio/_section_header.html.erb +23 -0
- data/app/views/studio/agent_chat/_chat.html.erb +2 -1
- data/app/views/style/_components.html.erb +3 -7
- data/app/views/style/_glass.html.erb +1 -1
- data/app/views/style/_host.html.erb +3 -14
- data/app/views/style/_modal_specimen.html.erb +5 -5
- data/app/views/style/_modals.html.erb +29 -224
- data/app/views/style/_scrims.html.erb +1 -8
- data/app/views/style/_site_footer.html.erb +5 -13
- data/app/views/style/_specimen.html.erb +4 -0
- data/app/views/style/_tasks.html.erb +17 -30
- data/app/views/style/_theme.html.erb +3 -9
- data/app/views/style/_tricks.html.erb +155 -216
- data/app/views/style/index.html.erb +7 -3
- data/lib/studio/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00ce81b9c4f82a619f86521fd4495d07e8a2a81f9f1eb5f15963e26f0cb038aa
|
|
4
|
+
data.tar.gz: ed8a7169b6322c577ca796c974f4561bd3c4a2e29fa7da917cd28aec84f401b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f0f640580ed2f77fdc6093e9a90f2edfc12bf2ba6925fc8ad6b24cd5587668c84d465090be866dcf19c5f536cb38ccbff123f078a52dbee52f8b2f7d73d302d
|
|
7
|
+
data.tar.gz: c02e3417b8f8eb1bb4b6b38706ddf79561959b61cacb7317a9465825c1724097c3b77fc5c4cf131f0011dff852b0c430c90f660277fd0067100a611cec10ec55
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,92 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.130.0 — 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Section headers that stack under the page header: `studio/_section_header`.** Render it
|
|
12
|
+
as the first child of a `<section>`. The title sticks directly under the page row, takes a
|
|
13
|
+
bar (solid in light and dark, glass under glass), and the next section's header takes its
|
|
14
|
+
place, by CSS sticky and no scroll script. `/admin/style` uses it on every section,
|
|
15
|
+
including a host's own. It joins the pinned stack as one layer, `section`: a new
|
|
16
|
+
`data-pin-section`, and `--pin-section-top`, `-h` and `-bottom`. Only the stuck header
|
|
17
|
+
counts, so `--pin-stack-bottom` and a table's sticky header sit under it. New classes:
|
|
18
|
+
`section-header`, `section-target` and `section-subtarget`, which
|
|
19
|
+
replace a hand-written `scroll-margin-top`. The publisher now reads the layers once more after
|
|
20
|
+
a settle and after a layer's top moves, so a layer under a layer does not keep a stale line.
|
|
21
|
+
Detail: `docs/PAGE_HEADER.md`.
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **A lit button's gradient centers on its own colour.** Every lit variant steps lightness 0.06 up
|
|
25
|
+
and 0.06 down from its fill, with chroma and hue unchanged, so the painted midpoint is the token.
|
|
26
|
+
The lit primary now centers on `--color-cta`, the violet a `text-primary` word paints, instead of
|
|
27
|
+
sitting darker and more saturated. White on the default primary is about 3.1:1 at the midpoint;
|
|
28
|
+
Alex McRitchie accepted that so the button and the brand word read as one purple. The semantic
|
|
29
|
+
fills still hold 4.5:1 there. `/lab/hero_button` shows a `text-primary` word beside the button.
|
|
30
|
+
|
|
31
|
+
- **Tricks starts with the buttons, then what you can add to them.** A new Button tricks group
|
|
32
|
+
sits directly under Buttons: rotating edge glow, border glow, sheen, pulse, fizz, hold to confirm,
|
|
33
|
+
confetti burst and cannons. Each sits on a real `.btn` or fires from one, with its code to copy.
|
|
34
|
+
The other groups keep their order, with one short sentence each at most.
|
|
35
|
+
- **`.studio-team-glow` is now `.edge-rotate-glow`, with `--edge-rotate-glow-*` knobs.** The old class
|
|
36
|
+
and knobs are a deprecated alias and paint the same, so no host has to change.
|
|
37
|
+
- **Fizz works around any button.** `render layout: "studio/fizz", locals: { seed: "save" }` wraps a
|
|
38
|
+
button in a `.fizz-stack`. The hold button's markup and selectors are unchanged. The only tie was
|
|
39
|
+
the `.hold-stack` selector.
|
|
40
|
+
- **Trial: a dark, full-size lit button's fill drops from 0.94 to 0.85 alpha** (`--lit-fill-alpha`).
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **The button tricks keep a lit button lit.** A lit button's `::after` closed the hole in the glow's
|
|
45
|
+
bloom, which washed its whole face. The sheen clipped the top highlight, and the pulse dropped the
|
|
46
|
+
lit shadow. `.studio-border-glow` on a button no longer ends its halo on a hard box edge.
|
|
47
|
+
|
|
48
|
+
## 0.129.0 — 2026-11-05
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- **The `/admin/style` copy is trimmed.** Each section keeps at most one short sentence of intro,
|
|
53
|
+
and the notes under specimens are cut the same way. Controls, specimens and error messages are
|
|
54
|
+
unchanged. The hidden copy-to-agent references on the modal cards are unchanged too. The Tricks
|
|
55
|
+
section is not part of this trim.
|
|
56
|
+
|
|
57
|
+
- **A bad or missing CSRF token on a JSON or `Accept: */*` request is now a 422 with no ErrorLog
|
|
58
|
+
row, on every controller using `Studio::ErrorHandling`.** The catch-all answered JSON 500 and
|
|
59
|
+
wrote a row. It now answers `422 {error, reason: "invalid_token"}` in every environment, and a
|
|
60
|
+
test on such a route that asserted `assert_raises(ActionController::InvalidAuthenticityToken)`
|
|
61
|
+
now asserts the 422. An HTML form is unchanged: the redirect home with its alert, and the row.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **A theme toggle is one crossfade.** `$store.theme.toggle()` added `theme-transition` for 300ms, and
|
|
66
|
+
in a host with that rule about 210 elements ran their own 200ms transition while gradients and
|
|
67
|
+
the page background snapped. It now flips the whole page in one 160ms view transition with no
|
|
68
|
+
movement. No element runs its own transition, and no named element keeps its own picture. A
|
|
69
|
+
browser with no view transitions, a reduced-motion reader, and a toggle during a Turbo visit
|
|
70
|
+
flip at once, and the visit's motion plays on. Under the crossfade the class, the store value
|
|
71
|
+
and localStorage change together in the transition's callback, so all three still read the old
|
|
72
|
+
theme for up to a frame after the call: a caller that reads them in the same tick must wait.
|
|
73
|
+
`toggleTheme` and `storedTheme` are no longer exported, and the store starts from the root's
|
|
74
|
+
class. `$store.theme.set("dark" | "light")` is the one setter, and `toggle()` stays. The
|
|
75
|
+
`theme-transition` rule in a host is now dead. Measured on the hub's `/deployments`, signed in,
|
|
76
|
+
2100 elements, Metal GPU: the largest step between frames fell from 35-65% of the brightness
|
|
77
|
+
change to 11%, and element transitions started by one toggle from about 1170 to 0.
|
|
78
|
+
Chromium sends every pointer hit to the root during a view transition, for about 160ms. A pointer
|
|
79
|
+
that stops in that time can show no `:hover` until it moves again, and a second click on the toggle
|
|
80
|
+
is dropped. A host spec that toggles and then hovers must wait for `data-theme-flip` to clear.
|
|
81
|
+
|
|
82
|
+
- **The agent chat at phone width.** Measured at 390x844 and 320x568, light and dark:
|
|
83
|
+
- No blank tail. A turn's `sr-only` name escaped the log, which had no positioned ancestor,
|
|
84
|
+
and stretched the document to 1933px where `main` ended at 977px. The log is now
|
|
85
|
+
`position: relative`.
|
|
86
|
+
- The newest turn shows above the composer, on load and after a new turn. The page scrolls
|
|
87
|
+
the thread's end clear of the stuck composer; not for an empty thread, not for a reader
|
|
88
|
+
scrolled up in the log or the page, and not during a hold, when it waits for the button
|
|
89
|
+
to be let go.
|
|
90
|
+
- A stale page says so. A refused token reads "This page has expired" with a reload hint on
|
|
91
|
+
the voice line and the typed error, and a refused send keeps its words in the box.
|
|
92
|
+
|
|
7
93
|
## 0.128.0 — 2026-11-05
|
|
8
94
|
|
|
9
95
|
### Fixed
|
data/README.md
CHANGED
|
@@ -294,6 +294,11 @@ An admin page's title: a plain header at rest, a bar once stuck under the navbar
|
|
|
294
294
|
in the pinned stack so a table's sticky header sits under it
|
|
295
295
|
([`docs/PAGE_HEADER.md`](docs/PAGE_HEADER.md)).
|
|
296
296
|
|
|
297
|
+
### The section header — `studio/_section_header`
|
|
298
|
+
|
|
299
|
+
A section's title that sticks under the page row until the next section's takes its place,
|
|
300
|
+
with a bar once stuck ([`docs/PAGE_HEADER.md`](docs/PAGE_HEADER.md#the-section-header)).
|
|
301
|
+
|
|
297
302
|
```erb
|
|
298
303
|
<%= render "studio/page_header", title: "Models", meta: "v1.2.3", label: "Models",
|
|
299
304
|
tabs: [{ label: "Users", href: users_path, current: "page" }, { label: "Teams", href: teams_path }] %>
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
Effect:
|
|
20
20
|
.text-gradient gradient-clipped text fill (authored fresh)
|
|
21
21
|
.studio-glow soft themed glow halo (authored fresh)
|
|
22
|
+
.edge-rotate-glow a glow rotating around the edge (ported, TM)
|
|
22
23
|
.surface-glass frosted translucent glass panel (authored fresh)
|
|
23
24
|
.conic-surface slow rotating conic-gradient wash (authored fresh)
|
|
24
25
|
.pulse-cta attention pulse/glow for a CTA (ported, TM)
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
Action:
|
|
27
28
|
.hold-btn press-and-hold confirm button (ported, TM)
|
|
28
29
|
.hold-stack/.fizz-bit the carbonation behind it (ported, TM)
|
|
30
|
+
.fizz-stack the same bubbles around any button (studio/_fizz)
|
|
29
31
|
.email-reject email field rejection shake + fade (extracted, TM)
|
|
30
32
|
|
|
31
33
|
Loader:
|
|
@@ -167,6 +169,16 @@
|
|
|
167
169
|
transform: translateZ(0);
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
/* On a button the halo needs room, as .edge-rotate-glow's bloom does. Mask applies
|
|
173
|
+
after filter, so the gradient stops at the padding box and the transparent border
|
|
174
|
+
is empty room for the blur to fade into. Without it the halo ends on a hard box. */
|
|
175
|
+
.btn.studio-border-glow::after {
|
|
176
|
+
inset: -46px;
|
|
177
|
+
padding: 10px;
|
|
178
|
+
border: 36px solid transparent;
|
|
179
|
+
background-clip: padding-box;
|
|
180
|
+
}
|
|
181
|
+
|
|
170
182
|
@keyframes studioBorderGlowSteam {
|
|
171
183
|
0% {
|
|
172
184
|
background-position: var(--studio-border-glow-offset, 0%) 0;
|
|
@@ -521,37 +533,32 @@
|
|
|
521
533
|
}
|
|
522
534
|
|
|
523
535
|
/* =============================================================================
|
|
524
|
-
9b.
|
|
525
|
-
Ported from turf-monster's
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
slides it between step cards as a modal's step machine advances).
|
|
542
|
-
|
|
543
|
-
THE HOST MAY CARRY ITS OWN BACKGROUND — the ring cuts the host's box out of
|
|
544
|
-
both wedge layers, so only the band around the edge paints. That hole is what
|
|
545
|
-
makes the primitive work in the two shapes its consumers need:
|
|
536
|
+
9b. edge-rotate-glow — a glow that rotates around an element's edge
|
|
537
|
+
Ported from turf-monster's selected-card glow (.holo-border-glow): a conic
|
|
538
|
+
gradient rotates around the element's edge, with a blurred bloom twin behind it.
|
|
539
|
+
DISTINCT from .studio-border-glow (an animated RAINBOW border) and .studio-glow
|
|
540
|
+
(a static soft box-shadow halo).
|
|
541
|
+
|
|
542
|
+
The color is a CSS var, default the CTA role. TWO opposed wedges travel the
|
|
543
|
+
edge: --edge-rotate-glow-color-b defaults to the first color, so a one-color
|
|
544
|
+
caller sees one color and a caller with a pair gets one per wedge. The wedges
|
|
545
|
+
are swept by animating a registered @property angle (rotating the GRADIENT, not
|
|
546
|
+
the element, so no oversized-child clipping trick is needed). Opacity is a var
|
|
547
|
+
with a transition, so a caller can fade the glow in and out (the living style
|
|
548
|
+
guide slides it between step cards as a modal's step machine advances).
|
|
549
|
+
|
|
550
|
+
THE HOST MAY CARRY ITS OWN BACKGROUND. The ring cuts the host's box out of both
|
|
551
|
+
wedge layers, so only the band around the edge paints. That hole is what makes
|
|
552
|
+
the primitive work in the two shapes its consumers need:
|
|
546
553
|
|
|
547
554
|
· the WRAPPER shape (TM's holo-wrap / holo-card split) — a bare host with an
|
|
548
555
|
opaque child in front. The engine's style/_modal_specimen and the hub's
|
|
549
556
|
release-phase-meter both ship this. Unchanged by the hole: the child was
|
|
550
557
|
already covering that interior.
|
|
551
|
-
· the CARD shape — the host IS the opaque surface.
|
|
552
|
-
card needs, because it cannot take a wrapper at all: Turbo targets
|
|
553
|
-
element for both replace and remove, so a wrapper is orphaned every
|
|
554
|
-
card leaves the board.
|
|
558
|
+
· the CARD shape — the host IS the opaque surface, as a .btn is. A live board
|
|
559
|
+
card needs this too, because it cannot take a wrapper at all: Turbo targets
|
|
560
|
+
the card element for both replace and remove, so a wrapper is orphaned every
|
|
561
|
+
time a card leaves the board.
|
|
555
562
|
|
|
556
563
|
Without the hole the card shape is impossible, and not for a tunable reason:
|
|
557
564
|
both layers are pseudo-elements at z-index -1 / -2, and CSS paints an element's
|
|
@@ -563,59 +570,87 @@
|
|
|
563
570
|
The host must still NOT be clipped by an overflow-hidden ancestor near its
|
|
564
571
|
border, or the ring is cut off with it.
|
|
565
572
|
|
|
566
|
-
Usage: <
|
|
573
|
+
Usage: <button class="btn btn-primary edge-rotate-glow">Save</button>
|
|
567
574
|
wrapper shape, when the face is its own element:
|
|
568
|
-
<div class="
|
|
575
|
+
<div class="edge-rotate-glow rounded-xl">
|
|
569
576
|
<div class="rounded-xl bg-surface p-5">Selected</div>
|
|
570
577
|
</div>
|
|
571
|
-
|
|
572
|
-
two
|
|
573
|
-
--
|
|
574
|
-
Tune: --
|
|
578
|
+
one color: style="--edge-rotate-glow-color: #E11D48"
|
|
579
|
+
two colors: style="--edge-rotate-glow-color: #F08030;
|
|
580
|
+
--edge-rotate-glow-color-b: #6890F0"
|
|
581
|
+
Tune: --edge-rotate-glow-color / -color-b / -opacity / -speed / -thickness /
|
|
575
582
|
-bloom
|
|
583
|
+
|
|
584
|
+
DEPRECATED ALIAS: .studio-team-glow and its --studio-team-glow-* knobs. Every
|
|
585
|
+
old name keeps working, because hub and Turf views, scripts and e2e use them on
|
|
586
|
+
their main branches. Remove the alias only after both have moved. An old knob
|
|
587
|
+
on the new class also still applies; a new knob on the old class is not
|
|
588
|
+
supported, so move the class and its knobs together.
|
|
576
589
|
========================================================================== */
|
|
577
590
|
}
|
|
578
|
-
@property --
|
|
591
|
+
@property --edge-rotate-glow-angle {
|
|
579
592
|
syntax: "<angle>";
|
|
580
593
|
inherits: false;
|
|
581
594
|
initial-value: 0deg;
|
|
582
595
|
}
|
|
583
596
|
@layer components {
|
|
597
|
+
.edge-rotate-glow {
|
|
598
|
+
/* Each default reads the old knob first, so a host that moves the class name
|
|
599
|
+
and leaves an old knob in its inline style keeps its value. */
|
|
600
|
+
--edge-rotate-glow-color: var(--studio-team-glow-color, var(--color-cta));
|
|
601
|
+
/* The SECOND wedge's color. Defaults to the first. Resolved on this element, so
|
|
602
|
+
an inline --edge-rotate-glow-color override feeds this default too. */
|
|
603
|
+
--edge-rotate-glow-color-b: var(--studio-team-glow-color-b, var(--edge-rotate-glow-color));
|
|
604
|
+
--edge-rotate-glow-opacity: var(--studio-team-glow-opacity, 1);
|
|
605
|
+
--edge-rotate-glow-speed: var(--studio-team-glow-speed, 4s);
|
|
606
|
+
--edge-rotate-glow-thickness: var(--studio-team-glow-thickness, 4px);
|
|
607
|
+
--edge-rotate-glow-bloom: var(--studio-team-glow-bloom, 10px);
|
|
608
|
+
}
|
|
609
|
+
/* DEPRECATED ALIAS. See the note above. The old knobs keep their old defaults on
|
|
610
|
+
the old class, at the old specificity, so a consumer's own rule or inline style
|
|
611
|
+
wins exactly as it did; the new knobs then read them. */
|
|
584
612
|
.studio-team-glow {
|
|
585
613
|
--studio-team-glow-color: var(--color-cta);
|
|
586
|
-
/* The SECOND wedge's color. Defaults to the first, so a one-color caller sees
|
|
587
|
-
no change; a caller with two colors to show (a Pokémon's two types, a pair of
|
|
588
|
-
brand colors) gets one per wedge. Resolved on this element, so an inline
|
|
589
|
-
--studio-team-glow-color override feeds this default too. */
|
|
590
614
|
--studio-team-glow-color-b: var(--studio-team-glow-color);
|
|
591
615
|
--studio-team-glow-opacity: 1;
|
|
592
616
|
--studio-team-glow-speed: 4s;
|
|
593
617
|
--studio-team-glow-thickness: 4px;
|
|
594
618
|
--studio-team-glow-bloom: 10px;
|
|
619
|
+
--edge-rotate-glow-color: var(--studio-team-glow-color);
|
|
620
|
+
--edge-rotate-glow-color-b: var(--studio-team-glow-color-b);
|
|
621
|
+
--edge-rotate-glow-opacity: var(--studio-team-glow-opacity);
|
|
622
|
+
--edge-rotate-glow-speed: var(--studio-team-glow-speed);
|
|
623
|
+
--edge-rotate-glow-thickness: var(--studio-team-glow-thickness);
|
|
624
|
+
--edge-rotate-glow-bloom: var(--studio-team-glow-bloom);
|
|
625
|
+
}
|
|
626
|
+
.edge-rotate-glow,
|
|
627
|
+
.studio-team-glow {
|
|
595
628
|
position: relative;
|
|
596
629
|
isolation: isolate;
|
|
597
630
|
}
|
|
631
|
+
.edge-rotate-glow::before,
|
|
632
|
+
.edge-rotate-glow::after,
|
|
598
633
|
.studio-team-glow::before,
|
|
599
634
|
.studio-team-glow::after {
|
|
600
635
|
content: "";
|
|
601
636
|
position: absolute;
|
|
602
|
-
inset: calc(-1 * var(--
|
|
637
|
+
inset: calc(-1 * var(--edge-rotate-glow-thickness));
|
|
603
638
|
border-radius: inherit;
|
|
604
639
|
z-index: -1;
|
|
605
640
|
pointer-events: none;
|
|
606
|
-
opacity: var(--
|
|
641
|
+
opacity: var(--edge-rotate-glow-opacity);
|
|
607
642
|
transition: opacity 0.4s ease;
|
|
608
643
|
/* Two opposed wedges so two stretches of the ring glow at once. */
|
|
609
644
|
background: conic-gradient(
|
|
610
|
-
from var(--
|
|
645
|
+
from var(--edge-rotate-glow-angle),
|
|
611
646
|
transparent 0%,
|
|
612
|
-
var(--
|
|
647
|
+
var(--edge-rotate-glow-color) 12%,
|
|
613
648
|
transparent 33%,
|
|
614
649
|
transparent 50%,
|
|
615
|
-
var(--
|
|
650
|
+
var(--edge-rotate-glow-color-b) 62%,
|
|
616
651
|
transparent 83%
|
|
617
652
|
);
|
|
618
|
-
animation:
|
|
653
|
+
animation: edge-rotate-glow-spin var(--edge-rotate-glow-speed) linear infinite;
|
|
619
654
|
/* THE HOST'S OWN BOX IS PUNCHED OUT of both wedge layers, so what paints is the
|
|
620
655
|
band around the edge and nothing else. `padding` makes each layer's CONTENT
|
|
621
656
|
box exactly the host's box; mask-composite then removes that box from the
|
|
@@ -625,7 +660,7 @@
|
|
|
625
660
|
itself for replace and remove, so a wrapper is orphaned on every remove. With
|
|
626
661
|
the hole cut here the ring rides the card directly. For a wrapper host this
|
|
627
662
|
changes nothing visible: the child was already covering that interior. */
|
|
628
|
-
padding: var(--
|
|
663
|
+
padding: var(--edge-rotate-glow-thickness);
|
|
629
664
|
-webkit-mask:
|
|
630
665
|
linear-gradient(#000 0 0) content-box,
|
|
631
666
|
linear-gradient(#000 0 0);
|
|
@@ -635,6 +670,7 @@
|
|
|
635
670
|
linear-gradient(#000 0 0);
|
|
636
671
|
mask-composite: exclude;
|
|
637
672
|
}
|
|
673
|
+
.edge-rotate-glow::after,
|
|
638
674
|
.studio-team-glow::after {
|
|
639
675
|
/* Blurred bloom twin, a touch larger: its wedges reach 4px PAST the ring, which
|
|
640
676
|
is what makes the bloom read as a halo, not a soft edge on the ring. Drop the
|
|
@@ -647,19 +683,21 @@
|
|
|
647
683
|
card's own or the wrapper child's). Mask applies AFTER filter, which is why
|
|
648
684
|
the reach has to out-measure the blur — without it the halo ends on a hard
|
|
649
685
|
line at the layer's own box. */
|
|
650
|
-
inset: calc(-1 * (var(--
|
|
651
|
-
padding: calc(var(--
|
|
652
|
-
border: calc(var(--
|
|
686
|
+
inset: calc(-1 * (var(--edge-rotate-glow-thickness) + 4px + var(--edge-rotate-glow-bloom) * 2));
|
|
687
|
+
padding: calc(var(--edge-rotate-glow-thickness) + 4px);
|
|
688
|
+
border: calc(var(--edge-rotate-glow-bloom) * 2) solid transparent;
|
|
653
689
|
/* The wedges stop at the padding box (host + ring + the 4px); the border is empty
|
|
654
690
|
room the blur is allowed to spread into. */
|
|
655
691
|
background-clip: padding-box;
|
|
656
692
|
z-index: -2;
|
|
657
|
-
filter: blur(var(--
|
|
693
|
+
filter: blur(var(--edge-rotate-glow-bloom)) saturate(1.35);
|
|
658
694
|
}
|
|
659
|
-
@keyframes
|
|
660
|
-
to { --
|
|
695
|
+
@keyframes edge-rotate-glow-spin {
|
|
696
|
+
to { --edge-rotate-glow-angle: 360deg; }
|
|
661
697
|
}
|
|
662
698
|
@media (prefers-reduced-motion: reduce) {
|
|
699
|
+
.edge-rotate-glow::before,
|
|
700
|
+
.edge-rotate-glow::after,
|
|
663
701
|
.studio-team-glow::before,
|
|
664
702
|
.studio-team-glow::after { animation: none; }
|
|
665
703
|
}
|
|
@@ -1262,7 +1300,7 @@ body.modal-open { overflow: visible; }
|
|
|
1262
1300
|
Named pulse-cta (not btn-pulse) so it stays a plain motion-layer effect, not a
|
|
1263
1301
|
.btn component variant the engine.css component contract would demand.
|
|
1264
1302
|
|
|
1265
|
-
Usage: <button class="btn btn-primary pulse-cta">
|
|
1303
|
+
Usage: <button class="btn btn-primary pulse-cta">Save</button>
|
|
1266
1304
|
green pulse: style="--pulse-cta-color: var(--color-success)"
|
|
1267
1305
|
Tune: --pulse-cta-color / --pulse-cta-speed / --pulse-cta-scale /
|
|
1268
1306
|
--pulse-cta-ring / --pulse-cta-strength
|
|
@@ -1278,19 +1316,19 @@ body.modal-open { overflow: visible; }
|
|
|
1278
1316
|
@keyframes pulse-cta {
|
|
1279
1317
|
0% {
|
|
1280
1318
|
transform: scale(1);
|
|
1281
|
-
box-shadow: 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) calc(var(--pulse-cta-strength) * 100%), transparent);
|
|
1319
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) calc(var(--pulse-cta-strength) * 100%), transparent);
|
|
1282
1320
|
}
|
|
1283
1321
|
10% {
|
|
1284
1322
|
transform: scale(var(--pulse-cta-scale));
|
|
1285
|
-
box-shadow: 0 0 0 calc(var(--pulse-cta-ring) * 0.6) color-mix(in srgb, var(--pulse-cta-color) calc(var(--pulse-cta-strength) * 50%), transparent);
|
|
1323
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 calc(var(--pulse-cta-ring) * 0.6) color-mix(in srgb, var(--pulse-cta-color) calc(var(--pulse-cta-strength) * 50%), transparent);
|
|
1286
1324
|
}
|
|
1287
1325
|
22% {
|
|
1288
1326
|
transform: scale(1);
|
|
1289
|
-
box-shadow: 0 0 0 var(--pulse-cta-ring) color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1327
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 var(--pulse-cta-ring) color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1290
1328
|
}
|
|
1291
1329
|
100% {
|
|
1292
1330
|
transform: scale(1);
|
|
1293
|
-
box-shadow: 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1331
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1294
1332
|
}
|
|
1295
1333
|
}
|
|
1296
1334
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1866,6 +1904,58 @@ body.modal-open { overflow: visible; }
|
|
|
1866
1904
|
opacity: 0;
|
|
1867
1905
|
}
|
|
1868
1906
|
|
|
1907
|
+
/* FIZZ AROUND ANY BUTTON (studio/_fizz). The bubbles have no tie to the hold: the
|
|
1908
|
+
only thing that held them to it was this section's .hold-stack selector, and the
|
|
1909
|
+
hold's own states. So .fizz-stack gives them the same stack without a hold, with
|
|
1910
|
+
a neutral name, and it rests and answers hover as a hold button does. */
|
|
1911
|
+
.fizz-stack {
|
|
1912
|
+
position: relative;
|
|
1913
|
+
display: inline-flex;
|
|
1914
|
+
isolation: isolate;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
.fizz-stack > .fizz-layer {
|
|
1918
|
+
position: absolute;
|
|
1919
|
+
inset: 0;
|
|
1920
|
+
z-index: 0;
|
|
1921
|
+
pointer-events: none;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
.fizz-stack > :not(.fizz-layer) {
|
|
1925
|
+
position: relative;
|
|
1926
|
+
z-index: 1;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
.fizz-stack .fizz-bit {
|
|
1930
|
+
position: absolute;
|
|
1931
|
+
width: var(--fs, 4px);
|
|
1932
|
+
height: var(--fs, 4px);
|
|
1933
|
+
margin-top: calc(var(--fs, 4px) / -2);
|
|
1934
|
+
margin-left: calc(var(--fs, 4px) / -2);
|
|
1935
|
+
border-radius: 100px;
|
|
1936
|
+
background: var(--fc, hsl(292 92% 70%));
|
|
1937
|
+
box-shadow:
|
|
1938
|
+
0 0 6px var(--fc, hsl(292 92% 70%)),
|
|
1939
|
+
0 0 0 0.5px rgb(255 255 255 / 0.35);
|
|
1940
|
+
opacity: 0;
|
|
1941
|
+
will-change: transform, opacity;
|
|
1942
|
+
animation: fizz-simmer var(--ft, 1.8s) linear var(--fd, 0s) infinite;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
.fizz-stack:not(.fizz-lively):hover .fizz-bit,
|
|
1946
|
+
.fizz-stack.fizz-lively .fizz-bit {
|
|
1947
|
+
animation: fizz-boil calc(var(--ft, 1.8s) * 0.62) linear calc(var(--fd, 0s) * 0.35) infinite;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
.fizz-stack > .fizz-layer-extra {
|
|
1951
|
+
opacity: 0;
|
|
1952
|
+
transition: opacity 0.45s ease;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
.fizz-stack.fizz-lively:hover > .fizz-layer-extra {
|
|
1956
|
+
opacity: 1;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1869
1959
|
/* THE PORTAL (studio/hold_button, fizz_portal: true). A button inside an
|
|
1870
1960
|
overflow-hidden card has its bubbles cut off at the card's edge, because the
|
|
1871
1961
|
layer is absolutely positioned inside the card's clip. With the option set,
|
|
@@ -1967,7 +2057,9 @@ body.modal-open { overflow: visible; }
|
|
|
1967
2057
|
.hold-stack .fizz-bit,
|
|
1968
2058
|
.hold-stack .hold-fizz-extra,
|
|
1969
2059
|
.hold-fizz-portal .fizz-bit,
|
|
1970
|
-
.hold-fizz-portal .hold-fizz-extra
|
|
2060
|
+
.hold-fizz-portal .hold-fizz-extra,
|
|
2061
|
+
.fizz-stack .fizz-bit,
|
|
2062
|
+
.fizz-stack .fizz-layer-extra {
|
|
1971
2063
|
animation: none !important;
|
|
1972
2064
|
opacity: 0 !important;
|
|
1973
2065
|
transition: none !important;
|