studio-engine 0.129.0 → 0.131.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 +192 -0
- data/README.md +48 -6
- data/app/assets/tailwind/studio_engine/engine-motion.css +322 -56
- data/app/assets/tailwind/studio_engine/engine.css +737 -19
- data/app/assets/tailwind/studio_engine/theme.css +4 -0
- data/app/controllers/concerns/studio/error_handling.rb +53 -15
- data/app/controllers/concerns/studio/magic_link_issuing.rb +4 -0
- data/app/controllers/magic_links_controller.rb +25 -2
- data/app/controllers/registrations_controller.rb +58 -22
- data/app/controllers/style_controller.rb +6 -0
- data/app/helpers/studio/agent_chat_helper.rb +14 -1
- data/app/helpers/studio/crumbs_helper.rb +34 -0
- data/app/helpers/studio/fizz_helper.rb +5 -0
- data/app/helpers/studio/loader_helper.rb +79 -2
- data/app/javascript/studio/agent_chat.js +294 -6
- data/app/javascript/studio/carousel.js +99 -0
- data/app/javascript/studio/controllers/carousel_controller.js +138 -0
- data/app/javascript/studio/controllers/infinite_scroll_controller.js +17 -0
- data/app/javascript/studio/controllers/pull_to_refresh_controller.js +18 -0
- data/app/javascript/studio/controllers/style_section_nav_controller.js +4 -4
- data/app/javascript/studio/controllers/talk_to_agent_controller.js +20 -0
- data/app/javascript/studio/infinite_scroll.js +142 -0
- data/app/javascript/studio/loader.js +6 -1
- data/app/javascript/studio/page_transitions.js +65 -8
- data/app/javascript/studio/pinned_stack.js +112 -4
- data/app/javascript/studio/pull_to_refresh.js +147 -0
- data/app/javascript/studio/stimulus.js +12 -1
- data/app/javascript/studio/talk_to_agent.js +116 -0
- data/app/views/components/_geo_signpost.html.erb +2 -2
- data/app/views/components/_link_sidebar.html.erb +2 -2
- data/app/views/components/_link_sidebar_trigger.html.erb +1 -1
- data/app/views/components/_sidebar_panel.html.erb +1 -1
- data/app/views/error_logs/index.html.erb +2 -8
- data/app/views/studio/_carousel.html.erb +97 -0
- 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 +55 -13
- data/app/views/studio/_section_header.html.erb +23 -0
- data/app/views/studio/agent_chat/_chat.html.erb +7 -4
- data/app/views/studio/agent_chat/_talk_modal.html.erb +58 -0
- data/app/views/studio/agent_chat/_turn.html.erb +1 -1
- data/app/views/studio/loader/_infinite_scroll.html.erb +42 -0
- data/app/views/studio/loader/_loader.html.erb +20 -3
- data/app/views/studio/loader/_pull_to_refresh.html.erb +20 -0
- data/app/views/studio/loader/_skeleton.html.erb +37 -0
- data/app/views/style/_components.html.erb +3 -5
- data/app/views/style/_host.html.erb +3 -6
- data/app/views/style/_loaders.html.erb +118 -0
- data/app/views/style/_loaders_feed_page.html.erb +16 -0
- data/app/views/style/_modal_specimen.html.erb +5 -5
- data/app/views/style/_modals.html.erb +15 -5
- data/app/views/style/_site_footer.html.erb +1 -1
- data/app/views/style/_specimen.html.erb +21 -10
- data/app/views/style/_tasks.html.erb +3 -5
- data/app/views/style/_theme.html.erb +2 -2
- data/app/views/style/_tricks.html.erb +218 -217
- data/app/views/style/index.html.erb +21 -11
- data/app/views/style/loaders_feed.html.erb +7 -0
- data/lib/studio/crumbs.rb +150 -0
- data/lib/studio/loader.rb +30 -2
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +3 -0
- metadata +22 -2
|
@@ -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,11 +27,15 @@
|
|
|
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:
|
|
32
34
|
.studio-loader the app's icon as a branded loader (Loader Lab picks)
|
|
33
35
|
.studio-loader-overlay the page-load overlay around it (docs/LOADER.md)
|
|
36
|
+
.studio-skeleton the content's shape while it loads (loader family)
|
|
37
|
+
.studio-infinite-scroll the next page near the end (loader family)
|
|
38
|
+
.studio-pull-refresh pull down to reload, phones only (loader family)
|
|
34
39
|
|
|
35
40
|
Leveling:
|
|
36
41
|
.level-badge(-1..-10) progressive level pill ladder (ported, TM)
|
|
@@ -167,6 +172,16 @@
|
|
|
167
172
|
transform: translateZ(0);
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
/* On a button the halo needs room, as .edge-rotate-glow's bloom does. Mask applies
|
|
176
|
+
after filter, so the gradient stops at the padding box and the transparent border
|
|
177
|
+
is empty room for the blur to fade into. Without it the halo ends on a hard box. */
|
|
178
|
+
.btn.studio-border-glow::after {
|
|
179
|
+
inset: -46px;
|
|
180
|
+
padding: 10px;
|
|
181
|
+
border: 36px solid transparent;
|
|
182
|
+
background-clip: padding-box;
|
|
183
|
+
}
|
|
184
|
+
|
|
170
185
|
@keyframes studioBorderGlowSteam {
|
|
171
186
|
0% {
|
|
172
187
|
background-position: var(--studio-border-glow-offset, 0%) 0;
|
|
@@ -521,37 +536,32 @@
|
|
|
521
536
|
}
|
|
522
537
|
|
|
523
538
|
/* =============================================================================
|
|
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:
|
|
539
|
+
9b. edge-rotate-glow — a glow that rotates around an element's edge
|
|
540
|
+
Ported from turf-monster's selected-card glow (.holo-border-glow): a conic
|
|
541
|
+
gradient rotates around the element's edge, with a blurred bloom twin behind it.
|
|
542
|
+
DISTINCT from .studio-border-glow (an animated RAINBOW border) and .studio-glow
|
|
543
|
+
(a static soft box-shadow halo).
|
|
544
|
+
|
|
545
|
+
The color is a CSS var, default the CTA role. TWO opposed wedges travel the
|
|
546
|
+
edge: --edge-rotate-glow-color-b defaults to the first color, so a one-color
|
|
547
|
+
caller sees one color and a caller with a pair gets one per wedge. The wedges
|
|
548
|
+
are swept by animating a registered @property angle (rotating the GRADIENT, not
|
|
549
|
+
the element, so no oversized-child clipping trick is needed). Opacity is a var
|
|
550
|
+
with a transition, so a caller can fade the glow in and out (the living style
|
|
551
|
+
guide slides it between step cards as a modal's step machine advances).
|
|
552
|
+
|
|
553
|
+
THE HOST MAY CARRY ITS OWN BACKGROUND. The ring cuts the host's box out of both
|
|
554
|
+
wedge layers, so only the band around the edge paints. That hole is what makes
|
|
555
|
+
the primitive work in the two shapes its consumers need:
|
|
546
556
|
|
|
547
557
|
· the WRAPPER shape (TM's holo-wrap / holo-card split) — a bare host with an
|
|
548
558
|
opaque child in front. The engine's style/_modal_specimen and the hub's
|
|
549
559
|
release-phase-meter both ship this. Unchanged by the hole: the child was
|
|
550
560
|
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.
|
|
561
|
+
· the CARD shape — the host IS the opaque surface, as a .btn is. A live board
|
|
562
|
+
card needs this too, because it cannot take a wrapper at all: Turbo targets
|
|
563
|
+
the card element for both replace and remove, so a wrapper is orphaned every
|
|
564
|
+
time a card leaves the board.
|
|
555
565
|
|
|
556
566
|
Without the hole the card shape is impossible, and not for a tunable reason:
|
|
557
567
|
both layers are pseudo-elements at z-index -1 / -2, and CSS paints an element's
|
|
@@ -563,59 +573,87 @@
|
|
|
563
573
|
The host must still NOT be clipped by an overflow-hidden ancestor near its
|
|
564
574
|
border, or the ring is cut off with it.
|
|
565
575
|
|
|
566
|
-
Usage: <
|
|
576
|
+
Usage: <button class="btn btn-primary edge-rotate-glow">Save</button>
|
|
567
577
|
wrapper shape, when the face is its own element:
|
|
568
|
-
<div class="
|
|
578
|
+
<div class="edge-rotate-glow rounded-xl">
|
|
569
579
|
<div class="rounded-xl bg-surface p-5">Selected</div>
|
|
570
580
|
</div>
|
|
571
|
-
|
|
572
|
-
two
|
|
573
|
-
--
|
|
574
|
-
Tune: --
|
|
581
|
+
one color: style="--edge-rotate-glow-color: #E11D48"
|
|
582
|
+
two colors: style="--edge-rotate-glow-color: #F08030;
|
|
583
|
+
--edge-rotate-glow-color-b: #6890F0"
|
|
584
|
+
Tune: --edge-rotate-glow-color / -color-b / -opacity / -speed / -thickness /
|
|
575
585
|
-bloom
|
|
586
|
+
|
|
587
|
+
DEPRECATED ALIAS: .studio-team-glow and its --studio-team-glow-* knobs. Every
|
|
588
|
+
old name keeps working, because hub and Turf views, scripts and e2e use them on
|
|
589
|
+
their main branches. Remove the alias only after both have moved. An old knob
|
|
590
|
+
on the new class also still applies; a new knob on the old class is not
|
|
591
|
+
supported, so move the class and its knobs together.
|
|
576
592
|
========================================================================== */
|
|
577
593
|
}
|
|
578
|
-
@property --
|
|
594
|
+
@property --edge-rotate-glow-angle {
|
|
579
595
|
syntax: "<angle>";
|
|
580
596
|
inherits: false;
|
|
581
597
|
initial-value: 0deg;
|
|
582
598
|
}
|
|
583
599
|
@layer components {
|
|
600
|
+
.edge-rotate-glow {
|
|
601
|
+
/* Each default reads the old knob first, so a host that moves the class name
|
|
602
|
+
and leaves an old knob in its inline style keeps its value. */
|
|
603
|
+
--edge-rotate-glow-color: var(--studio-team-glow-color, var(--color-cta));
|
|
604
|
+
/* The SECOND wedge's color. Defaults to the first. Resolved on this element, so
|
|
605
|
+
an inline --edge-rotate-glow-color override feeds this default too. */
|
|
606
|
+
--edge-rotate-glow-color-b: var(--studio-team-glow-color-b, var(--edge-rotate-glow-color));
|
|
607
|
+
--edge-rotate-glow-opacity: var(--studio-team-glow-opacity, 1);
|
|
608
|
+
--edge-rotate-glow-speed: var(--studio-team-glow-speed, 4s);
|
|
609
|
+
--edge-rotate-glow-thickness: var(--studio-team-glow-thickness, 4px);
|
|
610
|
+
--edge-rotate-glow-bloom: var(--studio-team-glow-bloom, 10px);
|
|
611
|
+
}
|
|
612
|
+
/* DEPRECATED ALIAS. See the note above. The old knobs keep their old defaults on
|
|
613
|
+
the old class, at the old specificity, so a consumer's own rule or inline style
|
|
614
|
+
wins exactly as it did; the new knobs then read them. */
|
|
584
615
|
.studio-team-glow {
|
|
585
616
|
--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
617
|
--studio-team-glow-color-b: var(--studio-team-glow-color);
|
|
591
618
|
--studio-team-glow-opacity: 1;
|
|
592
619
|
--studio-team-glow-speed: 4s;
|
|
593
620
|
--studio-team-glow-thickness: 4px;
|
|
594
621
|
--studio-team-glow-bloom: 10px;
|
|
622
|
+
--edge-rotate-glow-color: var(--studio-team-glow-color);
|
|
623
|
+
--edge-rotate-glow-color-b: var(--studio-team-glow-color-b);
|
|
624
|
+
--edge-rotate-glow-opacity: var(--studio-team-glow-opacity);
|
|
625
|
+
--edge-rotate-glow-speed: var(--studio-team-glow-speed);
|
|
626
|
+
--edge-rotate-glow-thickness: var(--studio-team-glow-thickness);
|
|
627
|
+
--edge-rotate-glow-bloom: var(--studio-team-glow-bloom);
|
|
628
|
+
}
|
|
629
|
+
.edge-rotate-glow,
|
|
630
|
+
.studio-team-glow {
|
|
595
631
|
position: relative;
|
|
596
632
|
isolation: isolate;
|
|
597
633
|
}
|
|
634
|
+
.edge-rotate-glow::before,
|
|
635
|
+
.edge-rotate-glow::after,
|
|
598
636
|
.studio-team-glow::before,
|
|
599
637
|
.studio-team-glow::after {
|
|
600
638
|
content: "";
|
|
601
639
|
position: absolute;
|
|
602
|
-
inset: calc(-1 * var(--
|
|
640
|
+
inset: calc(-1 * var(--edge-rotate-glow-thickness));
|
|
603
641
|
border-radius: inherit;
|
|
604
642
|
z-index: -1;
|
|
605
643
|
pointer-events: none;
|
|
606
|
-
opacity: var(--
|
|
644
|
+
opacity: var(--edge-rotate-glow-opacity);
|
|
607
645
|
transition: opacity 0.4s ease;
|
|
608
646
|
/* Two opposed wedges so two stretches of the ring glow at once. */
|
|
609
647
|
background: conic-gradient(
|
|
610
|
-
from var(--
|
|
648
|
+
from var(--edge-rotate-glow-angle),
|
|
611
649
|
transparent 0%,
|
|
612
|
-
var(--
|
|
650
|
+
var(--edge-rotate-glow-color) 12%,
|
|
613
651
|
transparent 33%,
|
|
614
652
|
transparent 50%,
|
|
615
|
-
var(--
|
|
653
|
+
var(--edge-rotate-glow-color-b) 62%,
|
|
616
654
|
transparent 83%
|
|
617
655
|
);
|
|
618
|
-
animation:
|
|
656
|
+
animation: edge-rotate-glow-spin var(--edge-rotate-glow-speed) linear infinite;
|
|
619
657
|
/* THE HOST'S OWN BOX IS PUNCHED OUT of both wedge layers, so what paints is the
|
|
620
658
|
band around the edge and nothing else. `padding` makes each layer's CONTENT
|
|
621
659
|
box exactly the host's box; mask-composite then removes that box from the
|
|
@@ -625,7 +663,7 @@
|
|
|
625
663
|
itself for replace and remove, so a wrapper is orphaned on every remove. With
|
|
626
664
|
the hole cut here the ring rides the card directly. For a wrapper host this
|
|
627
665
|
changes nothing visible: the child was already covering that interior. */
|
|
628
|
-
padding: var(--
|
|
666
|
+
padding: var(--edge-rotate-glow-thickness);
|
|
629
667
|
-webkit-mask:
|
|
630
668
|
linear-gradient(#000 0 0) content-box,
|
|
631
669
|
linear-gradient(#000 0 0);
|
|
@@ -635,6 +673,7 @@
|
|
|
635
673
|
linear-gradient(#000 0 0);
|
|
636
674
|
mask-composite: exclude;
|
|
637
675
|
}
|
|
676
|
+
.edge-rotate-glow::after,
|
|
638
677
|
.studio-team-glow::after {
|
|
639
678
|
/* Blurred bloom twin, a touch larger: its wedges reach 4px PAST the ring, which
|
|
640
679
|
is what makes the bloom read as a halo, not a soft edge on the ring. Drop the
|
|
@@ -647,19 +686,21 @@
|
|
|
647
686
|
card's own or the wrapper child's). Mask applies AFTER filter, which is why
|
|
648
687
|
the reach has to out-measure the blur — without it the halo ends on a hard
|
|
649
688
|
line at the layer's own box. */
|
|
650
|
-
inset: calc(-1 * (var(--
|
|
651
|
-
padding: calc(var(--
|
|
652
|
-
border: calc(var(--
|
|
689
|
+
inset: calc(-1 * (var(--edge-rotate-glow-thickness) + 4px + var(--edge-rotate-glow-bloom) * 2));
|
|
690
|
+
padding: calc(var(--edge-rotate-glow-thickness) + 4px);
|
|
691
|
+
border: calc(var(--edge-rotate-glow-bloom) * 2) solid transparent;
|
|
653
692
|
/* The wedges stop at the padding box (host + ring + the 4px); the border is empty
|
|
654
693
|
room the blur is allowed to spread into. */
|
|
655
694
|
background-clip: padding-box;
|
|
656
695
|
z-index: -2;
|
|
657
|
-
filter: blur(var(--
|
|
696
|
+
filter: blur(var(--edge-rotate-glow-bloom)) saturate(1.35);
|
|
658
697
|
}
|
|
659
|
-
@keyframes
|
|
660
|
-
to { --
|
|
698
|
+
@keyframes edge-rotate-glow-spin {
|
|
699
|
+
to { --edge-rotate-glow-angle: 360deg; }
|
|
661
700
|
}
|
|
662
701
|
@media (prefers-reduced-motion: reduce) {
|
|
702
|
+
.edge-rotate-glow::before,
|
|
703
|
+
.edge-rotate-glow::after,
|
|
663
704
|
.studio-team-glow::before,
|
|
664
705
|
.studio-team-glow::after { animation: none; }
|
|
665
706
|
}
|
|
@@ -1262,7 +1303,7 @@ body.modal-open { overflow: visible; }
|
|
|
1262
1303
|
Named pulse-cta (not btn-pulse) so it stays a plain motion-layer effect, not a
|
|
1263
1304
|
.btn component variant the engine.css component contract would demand.
|
|
1264
1305
|
|
|
1265
|
-
Usage: <button class="btn btn-primary pulse-cta">
|
|
1306
|
+
Usage: <button class="btn btn-primary pulse-cta">Save</button>
|
|
1266
1307
|
green pulse: style="--pulse-cta-color: var(--color-success)"
|
|
1267
1308
|
Tune: --pulse-cta-color / --pulse-cta-speed / --pulse-cta-scale /
|
|
1268
1309
|
--pulse-cta-ring / --pulse-cta-strength
|
|
@@ -1278,19 +1319,19 @@ body.modal-open { overflow: visible; }
|
|
|
1278
1319
|
@keyframes pulse-cta {
|
|
1279
1320
|
0% {
|
|
1280
1321
|
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);
|
|
1322
|
+
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
1323
|
}
|
|
1283
1324
|
10% {
|
|
1284
1325
|
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);
|
|
1326
|
+
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
1327
|
}
|
|
1287
1328
|
22% {
|
|
1288
1329
|
transform: scale(1);
|
|
1289
|
-
box-shadow: 0 0 0 var(--pulse-cta-ring) color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1330
|
+
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
1331
|
}
|
|
1291
1332
|
100% {
|
|
1292
1333
|
transform: scale(1);
|
|
1293
|
-
box-shadow: 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1334
|
+
box-shadow: var(--btn-lit-rest, 0 0 #0000), 0 0 0 0 color-mix(in srgb, var(--pulse-cta-color) 0%, transparent);
|
|
1294
1335
|
}
|
|
1295
1336
|
}
|
|
1296
1337
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1866,6 +1907,58 @@ body.modal-open { overflow: visible; }
|
|
|
1866
1907
|
opacity: 0;
|
|
1867
1908
|
}
|
|
1868
1909
|
|
|
1910
|
+
/* FIZZ AROUND ANY BUTTON (studio/_fizz). The bubbles have no tie to the hold: the
|
|
1911
|
+
only thing that held them to it was this section's .hold-stack selector, and the
|
|
1912
|
+
hold's own states. So .fizz-stack gives them the same stack without a hold, with
|
|
1913
|
+
a neutral name, and it rests and answers hover as a hold button does. */
|
|
1914
|
+
.fizz-stack {
|
|
1915
|
+
position: relative;
|
|
1916
|
+
display: inline-flex;
|
|
1917
|
+
isolation: isolate;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
.fizz-stack > .fizz-layer {
|
|
1921
|
+
position: absolute;
|
|
1922
|
+
inset: 0;
|
|
1923
|
+
z-index: 0;
|
|
1924
|
+
pointer-events: none;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
.fizz-stack > :not(.fizz-layer) {
|
|
1928
|
+
position: relative;
|
|
1929
|
+
z-index: 1;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
.fizz-stack .fizz-bit {
|
|
1933
|
+
position: absolute;
|
|
1934
|
+
width: var(--fs, 4px);
|
|
1935
|
+
height: var(--fs, 4px);
|
|
1936
|
+
margin-top: calc(var(--fs, 4px) / -2);
|
|
1937
|
+
margin-left: calc(var(--fs, 4px) / -2);
|
|
1938
|
+
border-radius: 100px;
|
|
1939
|
+
background: var(--fc, hsl(292 92% 70%));
|
|
1940
|
+
box-shadow:
|
|
1941
|
+
0 0 6px var(--fc, hsl(292 92% 70%)),
|
|
1942
|
+
0 0 0 0.5px rgb(255 255 255 / 0.35);
|
|
1943
|
+
opacity: 0;
|
|
1944
|
+
will-change: transform, opacity;
|
|
1945
|
+
animation: fizz-simmer var(--ft, 1.8s) linear var(--fd, 0s) infinite;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
.fizz-stack:not(.fizz-lively):hover .fizz-bit,
|
|
1949
|
+
.fizz-stack.fizz-lively .fizz-bit {
|
|
1950
|
+
animation: fizz-boil calc(var(--ft, 1.8s) * 0.62) linear calc(var(--fd, 0s) * 0.35) infinite;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
.fizz-stack > .fizz-layer-extra {
|
|
1954
|
+
opacity: 0;
|
|
1955
|
+
transition: opacity 0.45s ease;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
.fizz-stack.fizz-lively:hover > .fizz-layer-extra {
|
|
1959
|
+
opacity: 1;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1869
1962
|
/* THE PORTAL (studio/hold_button, fizz_portal: true). A button inside an
|
|
1870
1963
|
overflow-hidden card has its bubbles cut off at the card's edge, because the
|
|
1871
1964
|
layer is absolutely positioned inside the card's clip. With the option set,
|
|
@@ -1967,7 +2060,9 @@ body.modal-open { overflow: visible; }
|
|
|
1967
2060
|
.hold-stack .fizz-bit,
|
|
1968
2061
|
.hold-stack .hold-fizz-extra,
|
|
1969
2062
|
.hold-fizz-portal .fizz-bit,
|
|
1970
|
-
.hold-fizz-portal .hold-fizz-extra
|
|
2063
|
+
.hold-fizz-portal .hold-fizz-extra,
|
|
2064
|
+
.fizz-stack .fizz-bit,
|
|
2065
|
+
.fizz-stack .fizz-layer-extra {
|
|
1971
2066
|
animation: none !important;
|
|
1972
2067
|
opacity: 0 !important;
|
|
1973
2068
|
transition: none !important;
|
|
@@ -2390,10 +2485,55 @@ body.modal-open { overflow: visible; }
|
|
|
2390
2485
|
animation-delay: calc(var(--studio-loader-i, 0) * 0.6s - var(--studio-loader-sparks, 5) * 0.6s);
|
|
2391
2486
|
}
|
|
2392
2487
|
|
|
2488
|
+
/* ---- The plain styles: no icon, only the theme's colours ----------------
|
|
2489
|
+
spinner, dots and bar wrap the canonical .spinner, .loading-dots and
|
|
2490
|
+
.progress-meter rules (sections 2, 3 and 7 above) and only set their knobs,
|
|
2491
|
+
so the family and those classes stay one set of rules. */
|
|
2492
|
+
/* Drawn on the page like ink, so in the theme's contrast-checked primary
|
|
2493
|
+
ink (theme_resolver), not the raw primary, which can sink into a dark
|
|
2494
|
+
page. A mono loader keeps currentColor. */
|
|
2495
|
+
.studio-loader:is([data-loader-style="spinner"], [data-loader-style="dots"], [data-loader-style="bar"]):not([data-loader-mono]) {
|
|
2496
|
+
--studio-loader-color: var(--color-primary-ink, var(--color-primary));
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
/* :where() keeps the family's selectors from ending in the bare spinner class: a host
|
|
2500
|
+
counts that one rule to prove the motion layer is imported once
|
|
2501
|
+
(turf-monster test/lib/tailwind_css_first_test.rb). */
|
|
2502
|
+
.studio-loader[data-loader-style="spinner"] :where(.spinner) {
|
|
2503
|
+
--spinner-size: var(--studio-loader-size);
|
|
2504
|
+
--spinner-thickness: max(2px, calc(var(--studio-loader-size) * 0.1));
|
|
2505
|
+
--spinner-color: var(--studio-loader-color);
|
|
2506
|
+
--spinner-track: color-mix(in srgb, var(--studio-loader-color) 22%, transparent);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
.studio-loader[data-loader-style="dots"] { height: calc(var(--studio-loader-size) * 0.5); }
|
|
2510
|
+
.studio-loader[data-loader-style="dots"] .loading-dots {
|
|
2511
|
+
--loading-dots-size: calc(var(--studio-loader-size) * 0.22);
|
|
2512
|
+
--loading-dots-gap: calc(var(--studio-loader-size) * 0.11);
|
|
2513
|
+
--loading-dots-color: var(--studio-loader-color);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
.studio-loader[data-loader-style="bar"] {
|
|
2517
|
+
display: block;
|
|
2518
|
+
height: var(--studio-loader-bar-height, 6px);
|
|
2519
|
+
max-width: 100%;
|
|
2520
|
+
}
|
|
2521
|
+
.studio-loader[data-loader-style="bar"] .progress-meter {
|
|
2522
|
+
--progress-height: 100%;
|
|
2523
|
+
--progress: calc(var(--studio-loader-percent, 0) * 1%);
|
|
2524
|
+
--progress-fill: var(--studio-loader-color);
|
|
2525
|
+
--progress-track: color-mix(in srgb, var(--studio-loader-color) 16%, transparent);
|
|
2526
|
+
display: block;
|
|
2527
|
+
width: 100%;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2393
2530
|
/* ---- Pending: inside a button, shown only while it or its form is busy --- */
|
|
2394
2531
|
.studio-loader[data-loader-pending] { display: none; }
|
|
2395
2532
|
:where(form, button)[aria-busy="true"] .studio-loader[data-loader-pending] { display: inline-grid; }
|
|
2396
2533
|
:where(form, button)[aria-busy="true"] [data-loader-idle] { display: none; }
|
|
2534
|
+
/* The words a busy button says instead ("Saving…"): shown only while busy. */
|
|
2535
|
+
[data-loader-busy] { display: none; }
|
|
2536
|
+
:where(form, button)[aria-busy="true"] [data-loader-busy] { display: inline; margin-inline-start: 0.5rem; }
|
|
2397
2537
|
.studio-loader[data-loader-pending] .studio-loader-halo { display: none; }
|
|
2398
2538
|
.studio-loader[data-loader-pending] .studio-loader-icon { width: 100%; height: 100%; animation-duration: 1.4s; }
|
|
2399
2539
|
|
|
@@ -2440,6 +2580,9 @@ body.modal-open { overflow: visible; }
|
|
|
2440
2580
|
.studio-loader-orb,
|
|
2441
2581
|
.studio-loader-art { display: none; }
|
|
2442
2582
|
.studio-loader-fill { --studio-loader-level: 0.45; }
|
|
2583
|
+
/* A still band would read as a percent; a still bar with no percent is a
|
|
2584
|
+
faint full fill instead. */
|
|
2585
|
+
.studio-loader[data-loader-style="bar"] .progress-meter--indeterminate .progress-meter-fill { width: 100%; opacity: 0.35; }
|
|
2443
2586
|
}
|
|
2444
2587
|
|
|
2445
2588
|
@keyframes studio-loader-breathe {
|
|
@@ -2486,3 +2629,126 @@ body.modal-open { overflow: visible; }
|
|
|
2486
2629
|
26%, 100% { transform: scale(0.8); opacity: 0; }
|
|
2487
2630
|
}
|
|
2488
2631
|
}
|
|
2632
|
+
|
|
2633
|
+
/* =============================================================================
|
|
2634
|
+
LOADER FAMILY — skeleton, infinite scroll and pull to refresh (docs/LOADER.md).
|
|
2635
|
+
The branded loader above draws a wait; these three hold a page's shape while
|
|
2636
|
+
it waits. Markup: app/views/studio/loader/_skeleton, _infinite_scroll and
|
|
2637
|
+
_pull_to_refresh, through Studio::LoaderHelper. Behaviour:
|
|
2638
|
+
studio/infinite_scroll and studio/pull_to_refresh (lazy controllers).
|
|
2639
|
+
|
|
2640
|
+
Tune: --studio-skeleton-bone a bone's fill (default text at 9%)
|
|
2641
|
+
--studio-skeleton-shine the shimmer's highlight (default text at 7%)
|
|
2642
|
+
--studio-skeleton-width one line's width (the helper varies them)
|
|
2643
|
+
--studio-skeleton-speed one shimmer pass (default 1.6s)
|
|
2644
|
+
|
|
2645
|
+
The bones are tints of --color-text over whatever sits behind them, so one
|
|
2646
|
+
rule reads on a dark, light or glass surface. Under prefers-reduced-motion a
|
|
2647
|
+
skeleton is still and the pull indicator does not spin.
|
|
2648
|
+
========================================================================== */
|
|
2649
|
+
@layer components {
|
|
2650
|
+
.studio-skeleton {
|
|
2651
|
+
--studio-skeleton-bone: color-mix(in srgb, var(--color-text) 9%, transparent);
|
|
2652
|
+
--studio-skeleton-shine: color-mix(in srgb, var(--color-text) 7%, transparent);
|
|
2653
|
+
--studio-skeleton-speed: 1.6s;
|
|
2654
|
+
position: relative;
|
|
2655
|
+
display: grid;
|
|
2656
|
+
gap: 1rem;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
.studio-skeleton-item { display: grid; gap: 0.6rem; align-content: start; }
|
|
2660
|
+
.studio-skeleton-item[data-skeleton-kind="avatar"] {
|
|
2661
|
+
grid-template-columns: auto 1fr;
|
|
2662
|
+
align-items: center;
|
|
2663
|
+
column-gap: 0.85rem;
|
|
2664
|
+
}
|
|
2665
|
+
.studio-skeleton-item[data-skeleton-kind="avatar"] > .studio-skeleton-lines { display: grid; gap: 0.5rem; }
|
|
2666
|
+
.studio-skeleton-lines { display: grid; gap: 0.55rem; }
|
|
2667
|
+
|
|
2668
|
+
.studio-skeleton-bone {
|
|
2669
|
+
display: block;
|
|
2670
|
+
border-radius: 0.375rem;
|
|
2671
|
+
background:
|
|
2672
|
+
linear-gradient(90deg, transparent 0%, var(--studio-skeleton-shine) 50%, transparent 100%) 0 0 / 200% 100% no-repeat,
|
|
2673
|
+
var(--studio-skeleton-bone);
|
|
2674
|
+
background-position: 150% 0, 0 0;
|
|
2675
|
+
animation: studio-skeleton-shimmer var(--studio-skeleton-speed) ease-in-out infinite;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
.studio-skeleton-bone[data-bone="line"] {
|
|
2679
|
+
height: 0.75rem;
|
|
2680
|
+
width: var(--studio-skeleton-width, 100%);
|
|
2681
|
+
}
|
|
2682
|
+
.studio-skeleton-bone[data-bone="title"] { height: 1.1rem; width: var(--studio-skeleton-width, 55%); }
|
|
2683
|
+
.studio-skeleton-bone[data-bone="avatar"] { width: 2.75rem; height: 2.75rem; border-radius: 9999px; }
|
|
2684
|
+
.studio-skeleton-bone[data-bone="media"] { width: 100%; aspect-ratio: 16 / 9; border-radius: 0.75rem; }
|
|
2685
|
+
|
|
2686
|
+
.studio-skeleton-item[data-skeleton-kind="card"] {
|
|
2687
|
+
padding: 0.85rem;
|
|
2688
|
+
border-radius: 1rem;
|
|
2689
|
+
border: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent);
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
@keyframes studio-skeleton-shimmer {
|
|
2693
|
+
from { background-position: 150% 0, 0 0; }
|
|
2694
|
+
to { background-position: -50% 0, 0 0; }
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/* ---- Infinite scroll ----------------------------------------------------
|
|
2698
|
+
Each page arrives inside a <turbo-frame> nested in the one before. The
|
|
2699
|
+
frames take no box (display: contents), so the items flow into the
|
|
2700
|
+
list's own grid or stack as if they were one list. */
|
|
2701
|
+
.studio-infinite-scroll turbo-frame { display: contents; }
|
|
2702
|
+
.studio-infinite-scroll-more,
|
|
2703
|
+
.studio-infinite-scroll-end {
|
|
2704
|
+
grid-column: 1 / -1;
|
|
2705
|
+
display: flex;
|
|
2706
|
+
justify-content: center;
|
|
2707
|
+
padding-block: 1rem;
|
|
2708
|
+
}
|
|
2709
|
+
.studio-infinite-scroll-more .studio-loader { margin-inline-start: 0.5rem; }
|
|
2710
|
+
.studio-infinite-scroll-end { margin: 0; font-size: 0.875rem; color: var(--color-text-secondary); }
|
|
2711
|
+
/* Turbo marks the frame aria-busy while it fetches: the button keeps its
|
|
2712
|
+
place and its words, and shows a spinner beside them (a [data-loader-idle]
|
|
2713
|
+
child would hide while busy; "Load more" has none). */
|
|
2714
|
+
.studio-infinite-scroll-next[aria-busy="true"] .studio-loader[data-loader-pending] { display: inline-grid; }
|
|
2715
|
+
.studio-infinite-scroll-next[aria-busy="true"] [data-loader-idle] { display: none; }
|
|
2716
|
+
|
|
2717
|
+
/* ---- Pull to refresh ----------------------------------------------------
|
|
2718
|
+
Phone widths and touch only: the controller arms itself there and nowhere
|
|
2719
|
+
else, so on a desktop the indicator never shows. --studio-pull is how far
|
|
2720
|
+
the finger has pulled, 0..1 of the threshold. */
|
|
2721
|
+
.studio-pull-refresh { position: relative; }
|
|
2722
|
+
.studio-pull-refresh-indicator {
|
|
2723
|
+
--studio-pull: 0;
|
|
2724
|
+
position: absolute;
|
|
2725
|
+
top: 0;
|
|
2726
|
+
left: 50%;
|
|
2727
|
+
z-index: var(--z-raised, 10);
|
|
2728
|
+
display: grid;
|
|
2729
|
+
place-items: center;
|
|
2730
|
+
width: 2.5rem;
|
|
2731
|
+
height: 2.5rem;
|
|
2732
|
+
border-radius: 9999px;
|
|
2733
|
+
background: var(--color-surface);
|
|
2734
|
+
box-shadow: 0 2px 10px color-mix(in srgb, var(--color-text) 18%, transparent);
|
|
2735
|
+
color: var(--color-primary-ink, var(--color-primary));
|
|
2736
|
+
opacity: min(1, calc(var(--studio-pull) * 1.4));
|
|
2737
|
+
transform: translate(-50%, calc(var(--studio-pull) * 4.5rem - 3rem)) rotate(calc(var(--studio-pull) * 270deg));
|
|
2738
|
+
pointer-events: none;
|
|
2739
|
+
}
|
|
2740
|
+
.studio-pull-refresh-indicator[hidden] { display: none; }
|
|
2741
|
+
.studio-pull-refresh[data-pull-state="settling"] .studio-pull-refresh-indicator { transition: transform 0.2s ease, opacity 0.2s ease; }
|
|
2742
|
+
.studio-pull-refresh[data-pull-state="refreshing"] .studio-pull-refresh-indicator {
|
|
2743
|
+
--studio-pull: 1;
|
|
2744
|
+
transform: translate(-50%, 1.5rem);
|
|
2745
|
+
}
|
|
2746
|
+
.studio-pull-refresh-indicator .studio-pull-refresh-spinner { animation-play-state: paused; }
|
|
2747
|
+
.studio-pull-refresh[data-pull-state="refreshing"] .studio-pull-refresh-indicator .studio-pull-refresh-spinner { animation-play-state: running; }
|
|
2748
|
+
|
|
2749
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2750
|
+
.studio-skeleton-bone { animation: none; background-position: 150% 0, 0 0; }
|
|
2751
|
+
.studio-pull-refresh-indicator,
|
|
2752
|
+
.studio-pull-refresh-indicator * { transition: none !important; animation: none !important; }
|
|
2753
|
+
}
|
|
2754
|
+
}
|