studio-engine 0.17.0 → 0.18.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 +67 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +620 -5
- data/app/controllers/style_controller.rb +25 -0
- data/app/views/studio/modals/_crop_photo.html.erb +5 -1
- data/app/views/studio/modals/_host.html.erb +5 -0
- data/app/views/studio/modals/_image_upload.html.erb +16 -5
- data/app/views/studio/modals/_saving.html.erb +6 -2
- data/app/views/studio/modals/auth/_resend_footer.html.erb +25 -0
- data/app/views/studio/modals/blocks/_card_header.html.erb +83 -0
- data/app/views/studio/modals/blocks/_cta_redirect.html.erb +73 -0
- data/app/views/studio/modals/blocks/_onchain_success.html.erb +52 -0
- data/app/views/studio/modals/blocks/_progress_pill.html.erb +23 -0
- data/app/views/studio/modals/blocks/_shell.html.erb +35 -0
- data/app/views/studio/modals/blocks/_solana_tx_link.html.erb +42 -0
- data/app/views/studio/modals/shared/_age_attestation.html.erb +34 -0
- data/app/views/studio/modals/shared/_email_field.html.erb +70 -0
- data/app/views/studio/modals/templates/_action.html.erb +25 -0
- data/app/views/studio/modals/templates/_form.html.erb +25 -0
- data/app/views/studio/modals/templates/_status.html.erb +45 -0
- data/app/views/studio/modals/templates/_success.html.erb +23 -0
- data/app/views/studio/modals/templates/_wizard.html.erb +75 -0
- data/app/views/style/_modal_specimen.html.erb +120 -0
- data/app/views/style/_modals.html.erb +690 -0
- data/app/views/style/_specimen.html.erb +64 -0
- data/app/views/style/_tasks.html.erb +112 -0
- data/app/views/style/_theme.html.erb +222 -0
- data/app/views/style/_tricks.html.erb +323 -0
- data/app/views/style/index.html.erb +60 -0
- data/app/views/style/modals/_auth.html.erb +252 -0
- data/app/views/style/modals/_onchain_tx.html.erb +62 -0
- data/app/views/style/modals/_wallet_connect.html.erb +138 -0
- data/app/views/style/modals/_wallet_deposit.html.erb +56 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +27 -1
- metadata +32 -6
- data/app/controllers/design_system_controller.rb +0 -12
- data/app/views/design_system/_specimen.html.erb +0 -27
- data/app/views/design_system/index.html.erb +0 -232
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
studio-engine MOTION / EFFECT primitives — the shared animation
|
|
3
|
-
layer.
|
|
4
|
-
|
|
2
|
+
studio-engine MOTION / EFFECT / LEVELING primitives — the shared animation,
|
|
3
|
+
visual-effect, and leveling layer. Reusable primitives in three families. The
|
|
4
|
+
MOTION seven were consolidated from the per-app copies that drifted across
|
|
5
|
+
mcritchie-studio (MS) and turf-monster (TM); the EFFECT four are the reusable
|
|
6
|
+
visual treatments the admin/style page showcases beyond motion; the LEVELING
|
|
7
|
+
ladder is TM's polished .level-badge tiers, lifted verbatim so any consumer with
|
|
8
|
+
Studio.feature?(:leveling) ships them.
|
|
5
9
|
|
|
10
|
+
Motion:
|
|
6
11
|
.studio-border-glow mask-composite rainbow border glow (extracted, MS)
|
|
7
12
|
.spinner one canonical border spinner (reconciled, MS+TM)
|
|
8
13
|
.loading-dots three bouncing dots (extracted, TM)
|
|
@@ -11,6 +16,18 @@
|
|
|
11
16
|
.fade-edge static mask-image edge fade (authored fresh)
|
|
12
17
|
.progress-meter dual-layer color-flip progress bar (authored fresh)
|
|
13
18
|
|
|
19
|
+
Effect:
|
|
20
|
+
.text-gradient gradient-clipped text fill (authored fresh)
|
|
21
|
+
.studio-glow soft themed glow halo (authored fresh)
|
|
22
|
+
.surface-glass frosted translucent glass panel (authored fresh)
|
|
23
|
+
.conic-surface slow rotating conic-gradient wash (authored fresh)
|
|
24
|
+
|
|
25
|
+
Leveling:
|
|
26
|
+
.level-badge(-1..-10) progressive level pill ladder (ported, TM)
|
|
27
|
+
.level-up-pop level-up pop + glow burst (ported, TM)
|
|
28
|
+
.nav-level-pop nav level-up bounce (ported, TM)
|
|
29
|
+
.badge-with-sheen one-shot diagonal sheen wrapper (ported, TM)
|
|
30
|
+
|
|
14
31
|
─────────────────────────────────────────────────────────────────────────────
|
|
15
32
|
OPT-IN — this layer does NOT auto-bundle.
|
|
16
33
|
|
|
@@ -419,9 +436,501 @@
|
|
|
419
436
|
100% { transform: translateX(250%); }
|
|
420
437
|
}
|
|
421
438
|
|
|
439
|
+
/* =============================================================================
|
|
440
|
+
EFFECT family — static (and one slow-animated) visual treatments, beyond the
|
|
441
|
+
seven motion primitives above. Same design rules: plain classes, themed knobs
|
|
442
|
+
with sensible defaults, namespaced keyframes.
|
|
443
|
+
========================================================================== */
|
|
444
|
+
|
|
445
|
+
/* =============================================================================
|
|
446
|
+
8. text-gradient — gradient-clipped text fill (authored fresh)
|
|
447
|
+
Paints text with a themed gradient instead of a flat color via
|
|
448
|
+
background-clip: text. Defaults to a CTA -> danger sweep; retune the two stops
|
|
449
|
+
or replace the whole gradient image. Static — no animation. Apply to the text
|
|
450
|
+
element itself (a heading, a span); it needs a bold/large glyph to read well.
|
|
451
|
+
|
|
452
|
+
Usage: <h1 class="text-gradient">Headline</h1>
|
|
453
|
+
Tune: --text-gradient-from / --text-gradient-to / --text-gradient-angle /
|
|
454
|
+
--text-gradient-image (override the whole gradient)
|
|
455
|
+
========================================================================== */
|
|
456
|
+
.text-gradient {
|
|
457
|
+
--text-gradient-from: var(--color-cta);
|
|
458
|
+
--text-gradient-to: var(--color-danger);
|
|
459
|
+
--text-gradient-angle: 90deg;
|
|
460
|
+
--text-gradient-image: linear-gradient(
|
|
461
|
+
var(--text-gradient-angle),
|
|
462
|
+
var(--text-gradient-from),
|
|
463
|
+
var(--text-gradient-to)
|
|
464
|
+
);
|
|
465
|
+
background-image: var(--text-gradient-image);
|
|
466
|
+
-webkit-background-clip: text;
|
|
467
|
+
background-clip: text;
|
|
468
|
+
-webkit-text-fill-color: transparent;
|
|
469
|
+
color: transparent;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* =============================================================================
|
|
473
|
+
9. studio-glow — soft themed glow halo (authored fresh)
|
|
474
|
+
A single-color bloom around any element via box-shadow, tinted with the CTA
|
|
475
|
+
role by default. Static; add .studio-glow--pulse to breathe. Distinct from
|
|
476
|
+
.studio-border-glow (an animated rainbow BORDER ring) — this is a plain halo
|
|
477
|
+
for emphasis on a button, avatar, or card.
|
|
478
|
+
|
|
479
|
+
Usage: <button class="btn btn-primary studio-glow">Launch</button>
|
|
480
|
+
Tune: --studio-glow-color / --studio-glow-size / --studio-glow-spread /
|
|
481
|
+
--studio-glow-opacity / --studio-glow-speed (pulse only)
|
|
482
|
+
========================================================================== */
|
|
483
|
+
.studio-glow {
|
|
484
|
+
--studio-glow-color: var(--color-cta);
|
|
485
|
+
--studio-glow-size: 1.4rem;
|
|
486
|
+
--studio-glow-spread: -0.2rem;
|
|
487
|
+
--studio-glow-opacity: 0.6;
|
|
488
|
+
box-shadow: 0 0 var(--studio-glow-size) var(--studio-glow-spread)
|
|
489
|
+
color-mix(in srgb, var(--studio-glow-color) calc(var(--studio-glow-opacity) * 100%), transparent);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.studio-glow--pulse {
|
|
493
|
+
animation: studio-glow-pulse var(--studio-glow-speed, 2.4s) ease-in-out infinite;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
@keyframes studio-glow-pulse {
|
|
497
|
+
0%, 100% {
|
|
498
|
+
box-shadow: 0 0 var(--studio-glow-size) var(--studio-glow-spread)
|
|
499
|
+
color-mix(in srgb, var(--studio-glow-color) calc(var(--studio-glow-opacity) * 100%), transparent);
|
|
500
|
+
}
|
|
501
|
+
50% {
|
|
502
|
+
box-shadow: 0 0 calc(var(--studio-glow-size) * 2) calc(var(--studio-glow-spread) + 0.2rem)
|
|
503
|
+
color-mix(in srgb, var(--studio-glow-color) calc(var(--studio-glow-opacity) * 100%), transparent);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* =============================================================================
|
|
508
|
+
9b. studio-team-glow — single-color rotating "selected card" ring glow
|
|
509
|
+
Ported from turf-monster's per-team selected-card glow (.holo-border-glow):
|
|
510
|
+
a single-color conic-gradient ring rotates around the element's edge, with a
|
|
511
|
+
blurred bloom twin behind it. DISTINCT from .studio-border-glow (an animated
|
|
512
|
+
RAINBOW border) and .studio-glow (a static soft box-shadow halo) — this is the
|
|
513
|
+
"this card is selected / live" ring, tinted one color.
|
|
514
|
+
|
|
515
|
+
The color is a CSS var (default the CTA role, so purple on McRitchie Studio),
|
|
516
|
+
just like TM's per-team --glow-c. The wedge is swept by animating a registered
|
|
517
|
+
@property angle (rotating the GRADIENT, not the element — so no oversized-child
|
|
518
|
+
clipping trick is needed). Opacity is a var (--studio-team-glow-opacity) with a
|
|
519
|
+
transition, so a consumer can fade the glow in/out (the living style guide
|
|
520
|
+
slides it between step cards as a modal's step machine advances).
|
|
521
|
+
|
|
522
|
+
Host needs an opaque background (the ring shows only around the edge) and must
|
|
523
|
+
NOT be clipped by an overflow-hidden ancestor near its border.
|
|
524
|
+
|
|
525
|
+
Usage: <div class="studio-team-glow rounded-xl bg-surface p-5">Selected</div>
|
|
526
|
+
red glow: style="--studio-team-glow-color: #E11D48"
|
|
527
|
+
Tune: --studio-team-glow-color / -opacity / -speed / -thickness / -bloom
|
|
528
|
+
========================================================================== */
|
|
529
|
+
@property --studio-team-glow-angle {
|
|
530
|
+
syntax: "<angle>";
|
|
531
|
+
inherits: false;
|
|
532
|
+
initial-value: 0deg;
|
|
533
|
+
}
|
|
534
|
+
.studio-team-glow {
|
|
535
|
+
--studio-team-glow-color: var(--color-cta);
|
|
536
|
+
--studio-team-glow-opacity: 1;
|
|
537
|
+
--studio-team-glow-speed: 4s;
|
|
538
|
+
--studio-team-glow-thickness: 4px;
|
|
539
|
+
--studio-team-glow-bloom: 10px;
|
|
540
|
+
position: relative;
|
|
541
|
+
isolation: isolate;
|
|
542
|
+
}
|
|
543
|
+
.studio-team-glow::before,
|
|
544
|
+
.studio-team-glow::after {
|
|
545
|
+
content: "";
|
|
546
|
+
position: absolute;
|
|
547
|
+
inset: calc(-1 * var(--studio-team-glow-thickness));
|
|
548
|
+
border-radius: inherit;
|
|
549
|
+
z-index: -1;
|
|
550
|
+
pointer-events: none;
|
|
551
|
+
opacity: var(--studio-team-glow-opacity);
|
|
552
|
+
transition: opacity 0.4s ease;
|
|
553
|
+
/* Two opposed wedges so two stretches of the ring glow at once. */
|
|
554
|
+
background: conic-gradient(
|
|
555
|
+
from var(--studio-team-glow-angle),
|
|
556
|
+
transparent 0%,
|
|
557
|
+
var(--studio-team-glow-color) 12%,
|
|
558
|
+
transparent 33%,
|
|
559
|
+
transparent 50%,
|
|
560
|
+
var(--studio-team-glow-color) 62%,
|
|
561
|
+
transparent 83%
|
|
562
|
+
);
|
|
563
|
+
animation: studio-team-glow-spin var(--studio-team-glow-speed) linear infinite;
|
|
564
|
+
}
|
|
565
|
+
.studio-team-glow::after {
|
|
566
|
+
/* Blurred bloom twin, a touch larger. */
|
|
567
|
+
inset: calc(-1 * (var(--studio-team-glow-thickness) + 4px));
|
|
568
|
+
z-index: -2;
|
|
569
|
+
filter: blur(var(--studio-team-glow-bloom)) saturate(1.35);
|
|
570
|
+
}
|
|
571
|
+
@keyframes studio-team-glow-spin {
|
|
572
|
+
to { --studio-team-glow-angle: 360deg; }
|
|
573
|
+
}
|
|
574
|
+
@media (prefers-reduced-motion: reduce) {
|
|
575
|
+
.studio-team-glow::before,
|
|
576
|
+
.studio-team-glow::after { animation: none; }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* =============================================================================
|
|
580
|
+
10. surface-glass — frosted translucent glass panel (authored fresh)
|
|
581
|
+
A translucent, blurred surface for overlay chrome (modal shells, sticky bars,
|
|
582
|
+
toasts floated over content). The tint is the surface role mixed toward
|
|
583
|
+
transparent, so it reads in dark AND light; backdrop-filter blurs whatever
|
|
584
|
+
sits behind it. Give it a radius + padding at the call site.
|
|
585
|
+
|
|
586
|
+
Usage: <div class="surface-glass rounded-xl p-4"> ... </div>
|
|
587
|
+
Tune: --surface-glass-tint / --surface-glass-opacity / --surface-glass-blur /
|
|
588
|
+
--surface-glass-saturate / --surface-glass-border
|
|
589
|
+
========================================================================== */
|
|
590
|
+
.surface-glass {
|
|
591
|
+
--surface-glass-tint: var(--color-surface);
|
|
592
|
+
--surface-glass-opacity: 0.6;
|
|
593
|
+
--surface-glass-blur: 12px;
|
|
594
|
+
--surface-glass-saturate: 1.4;
|
|
595
|
+
background: color-mix(in srgb, var(--surface-glass-tint) calc(var(--surface-glass-opacity) * 100%), transparent);
|
|
596
|
+
-webkit-backdrop-filter: blur(var(--surface-glass-blur)) saturate(var(--surface-glass-saturate));
|
|
597
|
+
backdrop-filter: blur(var(--surface-glass-blur)) saturate(var(--surface-glass-saturate));
|
|
598
|
+
border: 1px solid var(--surface-glass-border, color-mix(in srgb, var(--color-border-strong) 70%, transparent));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/* =============================================================================
|
|
602
|
+
11. conic-surface — slow rotating conic-gradient wash (authored fresh)
|
|
603
|
+
A subtle animated ambient background for hero panels / empty states: a conic
|
|
604
|
+
gradient sweeps between two themed stops (CTA -> surface by default) on a slow
|
|
605
|
+
rotation. The gradient lives on a ::before that rotates, so content kept above
|
|
606
|
+
it stays crisp. The element's radius clips the wash; give it one.
|
|
607
|
+
|
|
608
|
+
Usage: <div class="conic-surface rounded-2xl p-8"> ...content... </div>
|
|
609
|
+
Tune: --conic-surface-from / --conic-surface-to / --conic-surface-duration
|
|
610
|
+
========================================================================== */
|
|
611
|
+
.conic-surface {
|
|
612
|
+
position: relative;
|
|
613
|
+
isolation: isolate;
|
|
614
|
+
overflow: hidden;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.conic-surface::before {
|
|
618
|
+
content: "";
|
|
619
|
+
position: absolute;
|
|
620
|
+
inset: -50%;
|
|
621
|
+
z-index: -1;
|
|
622
|
+
background: conic-gradient(
|
|
623
|
+
from 0deg,
|
|
624
|
+
var(--conic-surface-from, var(--color-cta)),
|
|
625
|
+
var(--conic-surface-to, var(--color-surface)),
|
|
626
|
+
var(--conic-surface-from, var(--color-cta))
|
|
627
|
+
);
|
|
628
|
+
animation: conic-surface-spin var(--conic-surface-duration, 16s) linear infinite;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
@keyframes conic-surface-spin {
|
|
632
|
+
to { transform: rotate(360deg); }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/* =============================================================================
|
|
636
|
+
LEVELING family — the .level-badge tier ladder (1–10) + the level-up burst.
|
|
637
|
+
Ported from turf-monster's application.css so the ENGINE ships the polished
|
|
638
|
+
leveling badges (was app-provided). Gate the surface with
|
|
639
|
+
Studio.feature?(:leveling); the admin/style Tricks section renders every tier
|
|
640
|
+
disabled-but-present when the host has leveling off.
|
|
641
|
+
|
|
642
|
+
Tiers: L1 outline starter · L2–3 filled green · L4–6 lifted/glowed/beveled ·
|
|
643
|
+
L7 two-tone green→mint · L8 mint · L9 animated mint · L10 holographic.
|
|
644
|
+
|
|
645
|
+
Tokens:
|
|
646
|
+
--color-primary-500-rgb the engine primary scale (emitted by
|
|
647
|
+
studio_theme_css_tag), space-separated "R G B", so
|
|
648
|
+
the badges restyle per app. ALWAYS the modern slash
|
|
649
|
+
form rgb(var(...) / A) — never legacy
|
|
650
|
+
rgba(var(...), A), which browsers silently drop for
|
|
651
|
+
a space-separated var. (TM's levelGlow shipped the
|
|
652
|
+
broken comma form; this port fixes it to slash.)
|
|
653
|
+
--level-mint / --level-mint-rgb the fixed mint accent (#06d6a0) the green→
|
|
654
|
+
mint arc lands on — a knob with a sensible default
|
|
655
|
+
so a consumer can retune the arc without forking.
|
|
656
|
+
========================================================================== */
|
|
657
|
+
.level-badge {
|
|
658
|
+
--level-mint: #06d6a0;
|
|
659
|
+
--level-mint-rgb: 6 214 160;
|
|
660
|
+
display: inline-flex;
|
|
661
|
+
align-items: center;
|
|
662
|
+
justify-content: center;
|
|
663
|
+
padding: 2px 10px;
|
|
664
|
+
border-radius: 9999px;
|
|
665
|
+
font-size: 10px;
|
|
666
|
+
font-weight: 700;
|
|
667
|
+
line-height: 1.4;
|
|
668
|
+
letter-spacing: 0.02em;
|
|
669
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
670
|
+
white-space: nowrap;
|
|
671
|
+
border: 1px solid transparent;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.level-badge-1 {
|
|
675
|
+
background: rgb(var(--color-primary-500-rgb) / 0.15);
|
|
676
|
+
color: var(--color-cta);
|
|
677
|
+
border-color: rgb(var(--color-primary-500-rgb) / 0.3);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.level-badge-2 {
|
|
681
|
+
background: linear-gradient(
|
|
682
|
+
180deg,
|
|
683
|
+
rgb(var(--color-primary-500-rgb) / 0.55),
|
|
684
|
+
rgb(var(--color-primary-500-rgb) / 0.35)
|
|
685
|
+
);
|
|
686
|
+
color: #fff;
|
|
687
|
+
border-color: rgb(var(--color-primary-500-rgb) / 0.6);
|
|
688
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.25);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.level-badge-3 {
|
|
692
|
+
/* L2 fill plus inset top highlight + inset bottom shadow line ("polished pin"). */
|
|
693
|
+
background: linear-gradient(
|
|
694
|
+
180deg,
|
|
695
|
+
rgb(var(--color-primary-500-rgb) / 0.6),
|
|
696
|
+
rgb(var(--color-primary-500-rgb) / 0.4)
|
|
697
|
+
);
|
|
698
|
+
color: #fff;
|
|
699
|
+
border-color: rgb(var(--color-primary-500-rgb) / 0.7);
|
|
700
|
+
box-shadow:
|
|
701
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.4),
|
|
702
|
+
inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
|
703
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.level-badge-4 {
|
|
707
|
+
/* L2 fill plus an outer drop shadow — lifted off the surface. */
|
|
708
|
+
background: linear-gradient(
|
|
709
|
+
180deg,
|
|
710
|
+
rgb(var(--color-primary-500-rgb) / 0.6),
|
|
711
|
+
rgb(var(--color-primary-500-rgb) / 0.4)
|
|
712
|
+
);
|
|
713
|
+
color: #fff;
|
|
714
|
+
border-color: rgb(var(--color-primary-500-rgb) / 0.7);
|
|
715
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
|
|
716
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.level-badge-5 {
|
|
720
|
+
/* L4 fill with a static medium green glow around the perimeter. */
|
|
721
|
+
background: linear-gradient(
|
|
722
|
+
180deg,
|
|
723
|
+
rgb(var(--color-primary-500-rgb) / 0.6),
|
|
724
|
+
rgb(var(--color-primary-500-rgb) / 0.4)
|
|
725
|
+
);
|
|
726
|
+
color: #fff;
|
|
727
|
+
border-color: rgb(var(--color-primary-500-rgb) / 0.85);
|
|
728
|
+
box-shadow:
|
|
729
|
+
0 0 8px rgb(var(--color-primary-500-rgb) / 0.7),
|
|
730
|
+
0 0 16px rgb(var(--color-primary-500-rgb) / 0.4);
|
|
731
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.level-badge-6 {
|
|
735
|
+
/* L5's glow plus a beveled-coin treatment (inset highlight/shadow + drop). */
|
|
736
|
+
background: linear-gradient(
|
|
737
|
+
180deg,
|
|
738
|
+
rgb(var(--color-primary-500-rgb) / 0.7),
|
|
739
|
+
rgb(var(--color-primary-500-rgb) / 0.45)
|
|
740
|
+
);
|
|
741
|
+
color: #fff;
|
|
742
|
+
border-color: rgb(var(--color-primary-500-rgb) / 0.85);
|
|
743
|
+
box-shadow:
|
|
744
|
+
inset 0 2px 0 rgba(255, 255, 255, 0.5),
|
|
745
|
+
inset 0 -2px 0 rgba(0, 0, 0, 0.25),
|
|
746
|
+
0 1px 2px rgba(0, 0, 0, 0.35),
|
|
747
|
+
0 0 8px rgb(var(--color-primary-500-rgb) / 0.7),
|
|
748
|
+
0 0 16px rgb(var(--color-primary-500-rgb) / 0.4);
|
|
749
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.level-badge-7 {
|
|
753
|
+
/* L6's beveled face with the fill swapped to a diagonal primary → mint gradient. */
|
|
754
|
+
background: linear-gradient(
|
|
755
|
+
135deg,
|
|
756
|
+
rgb(var(--color-primary-500-rgb) / 0.8),
|
|
757
|
+
rgb(var(--level-mint-rgb, 6 214 160) / 0.7)
|
|
758
|
+
);
|
|
759
|
+
color: #fff;
|
|
760
|
+
border-color: rgb(var(--color-primary-500-rgb) / 0.85);
|
|
761
|
+
box-shadow:
|
|
762
|
+
inset 0 2px 0 rgba(255, 255, 255, 0.5),
|
|
763
|
+
inset 0 -2px 0 rgba(0, 0, 0, 0.25),
|
|
764
|
+
0 1px 2px rgba(0, 0, 0, 0.35),
|
|
765
|
+
0 0 8px rgb(var(--color-primary-500-rgb) / 0.7),
|
|
766
|
+
0 0 16px rgb(var(--color-primary-500-rgb) / 0.4);
|
|
767
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.level-badge-8 {
|
|
771
|
+
/* Full transition to mint — the green-family arc's destination color. */
|
|
772
|
+
background: linear-gradient(135deg, #16e5b6, var(--level-mint, #06d6a0) 60%, #028a6f);
|
|
773
|
+
color: #fff;
|
|
774
|
+
border-color: rgb(var(--level-mint-rgb, 6 214 160) / 0.85);
|
|
775
|
+
box-shadow:
|
|
776
|
+
inset 0 2px 0 rgba(255, 255, 255, 0.5),
|
|
777
|
+
inset 0 -2px 0 rgba(0, 0, 0, 0.25),
|
|
778
|
+
0 1px 2px rgba(0, 0, 0, 0.35),
|
|
779
|
+
0 0 10px rgb(var(--level-mint-rgb, 6 214 160) / 0.7),
|
|
780
|
+
0 0 20px rgb(var(--level-mint-rgb, 6 214 160) / 0.4);
|
|
781
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
|
|
782
|
+
font-weight: 800;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.level-badge-9 {
|
|
786
|
+
/* Animated green ↔ mint gradient (level-shimmer sweeps the mint peak across). */
|
|
787
|
+
background: linear-gradient(
|
|
788
|
+
135deg,
|
|
789
|
+
rgb(var(--color-primary-500-rgb) / 0.9),
|
|
790
|
+
rgb(var(--level-mint-rgb, 6 214 160) / 0.9) 50%,
|
|
791
|
+
rgb(var(--color-primary-500-rgb) / 0.9)
|
|
792
|
+
);
|
|
793
|
+
background-size: 200% 200%;
|
|
794
|
+
color: #fff;
|
|
795
|
+
border-color: rgb(var(--level-mint-rgb, 6 214 160) / 0.85);
|
|
796
|
+
box-shadow:
|
|
797
|
+
inset 0 2px 0 rgba(255, 255, 255, 0.5),
|
|
798
|
+
inset 0 -2px 0 rgba(0, 0, 0, 0.25),
|
|
799
|
+
0 1px 2px rgba(0, 0, 0, 0.35),
|
|
800
|
+
0 0 10px rgb(var(--level-mint-rgb, 6 214 160) / 0.7),
|
|
801
|
+
0 0 20px rgb(var(--level-mint-rgb, 6 214 160) / 0.4);
|
|
802
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
803
|
+
animation: level-shimmer 2.5s ease-in-out infinite;
|
|
804
|
+
font-weight: 800;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.level-badge-10 {
|
|
808
|
+
/* Legendary — holographic 8-stop spectrum on a 400% pan. The rainbow is a fixed
|
|
809
|
+
decorative spectrum (not theme-driven), so its stops stay literal. */
|
|
810
|
+
background: linear-gradient(
|
|
811
|
+
135deg,
|
|
812
|
+
#ff0080,
|
|
813
|
+
#ff8c00,
|
|
814
|
+
#ffd700,
|
|
815
|
+
#06d6a0,
|
|
816
|
+
#4baf50,
|
|
817
|
+
#00d4ff,
|
|
818
|
+
#8e82fe,
|
|
819
|
+
#ff0080
|
|
820
|
+
);
|
|
821
|
+
background-size: 400% 400%;
|
|
822
|
+
color: #fff;
|
|
823
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
824
|
+
box-shadow:
|
|
825
|
+
0 0 24px rgba(255, 255, 255, 0.55),
|
|
826
|
+
0 0 48px rgba(255, 215, 0, 0.35),
|
|
827
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
|
828
|
+
text-shadow:
|
|
829
|
+
0 0 6px rgba(255, 255, 255, 0.85),
|
|
830
|
+
0 1px 2px rgba(0, 0, 0, 0.5);
|
|
831
|
+
animation: level-holographic 4s ease infinite;
|
|
832
|
+
font-weight: 900;
|
|
833
|
+
letter-spacing: 0.04em;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
@keyframes level-shimmer {
|
|
837
|
+
0%, 100% { background-position: 0% 50%; }
|
|
838
|
+
50% { background-position: 100% 50%; }
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
@keyframes level-holographic {
|
|
842
|
+
0%, 100% { background-position: 0% 50%; }
|
|
843
|
+
50% { background-position: 100% 50%; }
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* level-up-pop — the composite pop + glow burst fired when a user levels up.
|
|
847
|
+
Apply to the badge (or its wrapper); one-shot on add. */
|
|
848
|
+
.level-up-pop {
|
|
849
|
+
animation: levelPop 1.1s cubic-bezier(0.34, 1.56, 0.64, 1), levelGlow 1.4s ease-out;
|
|
850
|
+
z-index: 10;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/* nav-level-pop — the smaller nav level-up bounce (shared by the nav seeds bar). */
|
|
854
|
+
.nav-level-pop {
|
|
855
|
+
animation: navLevelPop 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
@keyframes levelPop {
|
|
859
|
+
0% { transform: scale(1) rotate(0deg); }
|
|
860
|
+
12% { transform: scale(3.2) rotate(-4deg); }
|
|
861
|
+
28% { transform: scale(2.4) rotate(3deg); }
|
|
862
|
+
42% { transform: scale(2.8) rotate(-1.5deg); }
|
|
863
|
+
60% { transform: scale(2.2) rotate(1deg); }
|
|
864
|
+
78% { transform: scale(1.3) rotate(0deg); }
|
|
865
|
+
100% { transform: scale(1) rotate(0deg); }
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
@keyframes levelGlow {
|
|
869
|
+
/* Modern slash form — a space-separated --color-*-rgb var is invalid inside
|
|
870
|
+
legacy rgba(var(...), A) and silently drops (TM's original bug). */
|
|
871
|
+
0% { box-shadow: 0 0 0 0 rgb(var(--color-primary-500-rgb) / 0.9); }
|
|
872
|
+
25% { box-shadow: 0 0 40px 16px rgb(var(--color-primary-500-rgb) / 0.7); }
|
|
873
|
+
50% { box-shadow: 0 0 25px 10px rgb(var(--color-primary-500-rgb) / 0.4); }
|
|
874
|
+
100% { box-shadow: 0 0 0 0 rgb(var(--color-primary-500-rgb) / 0); }
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
@keyframes navLevelPop {
|
|
878
|
+
0% { transform: scale(1); }
|
|
879
|
+
15% { transform: scale(1.5); }
|
|
880
|
+
35% { transform: scale(1.2); }
|
|
881
|
+
50% { transform: scale(1.4); }
|
|
882
|
+
70% { transform: scale(1.1); }
|
|
883
|
+
100% { transform: scale(1); }
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/* badge-with-sheen — wrap any pill to fire a one-shot diagonal white sheen on
|
|
887
|
+
render. Ported from TM's Tailwind utility as a plain class (this layer's
|
|
888
|
+
convention). Tune the fire time with --sheen-delay to sync to nearby motion. */
|
|
889
|
+
.badge-with-sheen {
|
|
890
|
+
position: relative;
|
|
891
|
+
display: inline-block;
|
|
892
|
+
border-radius: 9999px;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.badge-with-sheen::after {
|
|
896
|
+
content: "";
|
|
897
|
+
position: absolute;
|
|
898
|
+
inset: 0;
|
|
899
|
+
background: linear-gradient(
|
|
900
|
+
90deg,
|
|
901
|
+
transparent 35%,
|
|
902
|
+
rgba(255, 255, 255, 0.45) 50%,
|
|
903
|
+
transparent 65%
|
|
904
|
+
);
|
|
905
|
+
background-size: 250% 100%;
|
|
906
|
+
background-repeat: no-repeat;
|
|
907
|
+
background-position: 200% 0;
|
|
908
|
+
border-radius: inherit;
|
|
909
|
+
pointer-events: none;
|
|
910
|
+
animation: badge-sheen-sweep 2.5s ease-in-out var(--sheen-delay, 0s) 1 both;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
@keyframes badge-sheen-sweep {
|
|
914
|
+
from { background-position: 200% 0; }
|
|
915
|
+
to { background-position: -100% 0; }
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* Leveling honors reduced-motion: the animated tiers settle to a static gradient
|
|
919
|
+
and the bursts/sheen do not fire. */
|
|
920
|
+
@media (prefers-reduced-motion: reduce) {
|
|
921
|
+
.level-badge-9,
|
|
922
|
+
.level-badge-10,
|
|
923
|
+
.level-up-pop,
|
|
924
|
+
.nav-level-pop,
|
|
925
|
+
.badge-with-sheen::after {
|
|
926
|
+
animation: none;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
422
930
|
/* =============================================================================
|
|
423
931
|
Reduced-motion: honor the user preference. Animated primitives settle into a
|
|
424
|
-
sensible static resting state; the static primitives (fade-edge
|
|
932
|
+
sensible static resting state; the static primitives (fade-edge, text-gradient,
|
|
933
|
+
surface-glass) are unaffected.
|
|
425
934
|
========================================================================== */
|
|
426
935
|
@media (prefers-reduced-motion: reduce) {
|
|
427
936
|
.studio-border-glow::before,
|
|
@@ -431,10 +940,116 @@
|
|
|
431
940
|
.sheen::after,
|
|
432
941
|
.ping,
|
|
433
942
|
.ping.ping--dot::after,
|
|
434
|
-
.progress-meter--indeterminate .progress-meter-fill
|
|
943
|
+
.progress-meter--indeterminate .progress-meter-fill,
|
|
944
|
+
.studio-glow--pulse,
|
|
945
|
+
.conic-surface::before {
|
|
435
946
|
animation: none;
|
|
436
947
|
}
|
|
437
948
|
|
|
438
949
|
.loading-dots span { opacity: 0.85; }
|
|
439
950
|
.sheen::after { display: none; }
|
|
440
951
|
}
|
|
952
|
+
|
|
953
|
+
/* =============================================================================
|
|
954
|
+
8. Modal host motion — the studio modal card + backdrop animation layer.
|
|
955
|
+
Ported verbatim from turf-monster app/assets/tailwind/application.css (the
|
|
956
|
+
modal system migrated into the engine). Drives the shared modal host and the
|
|
957
|
+
living style guide's page-scoped host (dsModals): the store binds the
|
|
958
|
+
.modal-card-{mount,unmount,shake-in,swap-*} + .modal-backdrop-{mount,unmount}
|
|
959
|
+
classes; the card open/close use spring curves so the modal POPS in with a
|
|
960
|
+
small overshoot and POPS out with a tiny anticipation windup. Backdrop stays
|
|
961
|
+
a clean fade. Durations are synchronized with the store's close/swap timeouts
|
|
962
|
+
(CLOSE_ANIM_MS 220, SWAP_IN_MS 220) so a keyframe plays fully before the DOM
|
|
963
|
+
unmounts. Bump both together if you retune.
|
|
964
|
+
========================================================================== */
|
|
965
|
+
|
|
966
|
+
/* Scroll lock applied by the store's _sync() when the stack is non-empty. */
|
|
967
|
+
body.modal-open { overflow: hidden; }
|
|
968
|
+
|
|
969
|
+
/* Drain-bar keyframe — a translucent overlay sweeping a CTA button over its
|
|
970
|
+
countdown (studio/modals/blocks/_cta_redirect + _success_card cta_drain). */
|
|
971
|
+
@keyframes studio-modal-drain {
|
|
972
|
+
from { transform: scaleX(1); }
|
|
973
|
+
to { transform: scaleX(0); }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/* Card mount / unmount + backdrop fade. */
|
|
977
|
+
@keyframes modal-card-in {
|
|
978
|
+
0% { opacity: 0; transform: scale(0.86) translateY(12px); }
|
|
979
|
+
55% { opacity: 1; transform: scale(1.04) translateY(-3px); }
|
|
980
|
+
80% { transform: scale(0.99) translateY(1px); }
|
|
981
|
+
100% { opacity: 1; transform: scale(1) translateY(0); }
|
|
982
|
+
}
|
|
983
|
+
@keyframes modal-card-out {
|
|
984
|
+
0% { opacity: 1; transform: scale(1) translateY(0); }
|
|
985
|
+
25% { opacity: 1; transform: scale(1.03) translateY(-2px); }
|
|
986
|
+
100% { opacity: 0; transform: scale(0.92) translateY(4px); }
|
|
987
|
+
}
|
|
988
|
+
@keyframes modal-backdrop-in { from { opacity: 0; } to { opacity: 1; } }
|
|
989
|
+
@keyframes modal-backdrop-out { from { opacity: 1; } to { opacity: 0; } }
|
|
990
|
+
|
|
991
|
+
.modal-card-mount { animation: modal-card-in 320ms cubic-bezier(0.34, 1.4, 0.5, 1) both; }
|
|
992
|
+
.modal-card-unmount { animation: modal-card-out 220ms cubic-bezier(0.6, -0.2, 0.7, 0.4) forwards; }
|
|
993
|
+
.modal-backdrop-mount { animation: modal-backdrop-in 180ms ease-out both; }
|
|
994
|
+
.modal-backdrop-unmount { animation: modal-backdrop-out 200ms ease-in forwards; }
|
|
995
|
+
|
|
996
|
+
/* Named entrance "shake" — Discord-style "not quite yet" nope. Registered as
|
|
997
|
+
enterAnim: 'shake'; duration MUST match the store registry ms (600). */
|
|
998
|
+
@keyframes modal-card-shake-in {
|
|
999
|
+
0% { opacity: 0; transform: scale(0.92) translateX(0); }
|
|
1000
|
+
22% { opacity: 1; transform: scale(1) translateX(-11px); }
|
|
1001
|
+
37% { transform: scale(1) translateX(9px); }
|
|
1002
|
+
52% { transform: scale(1) translateX(-7px); }
|
|
1003
|
+
67% { transform: scale(1) translateX(5px); }
|
|
1004
|
+
82% { transform: scale(1) translateX(-3px); }
|
|
1005
|
+
100% { opacity: 1; transform: scale(1) translateX(0); }
|
|
1006
|
+
}
|
|
1007
|
+
.modal-card-shake-in { animation: modal-card-shake-in 600ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
|
|
1008
|
+
|
|
1009
|
+
/* Inter-modal swap — directional slide for wizard forward-flow feel. Forward:
|
|
1010
|
+
leaving card exits stage-right, new one enters from the left. */
|
|
1011
|
+
@keyframes modal-card-slide-out-right {
|
|
1012
|
+
from { opacity: 1; transform: translateX(0) scale(1); }
|
|
1013
|
+
to { opacity: 0; transform: translateX(48px) scale(0.98); }
|
|
1014
|
+
}
|
|
1015
|
+
@keyframes modal-card-slide-in-left {
|
|
1016
|
+
from { opacity: 0; transform: translateX(-48px) scale(0.98); }
|
|
1017
|
+
to { opacity: 1; transform: translateX(0) scale(1); }
|
|
1018
|
+
}
|
|
1019
|
+
.modal-card-swap-out { animation: modal-card-slide-out-right 220ms cubic-bezier(0.4, 0, 1, 1) forwards; }
|
|
1020
|
+
.modal-card-swap-in { animation: modal-card-slide-in-left 220ms cubic-bezier(0, 0, 0.2, 1) both; }
|
|
1021
|
+
|
|
1022
|
+
/* Back-direction pair — exits stage-left, enters from the right. */
|
|
1023
|
+
@keyframes modal-card-slide-out-left {
|
|
1024
|
+
from { opacity: 1; transform: translateX(0) scale(1); }
|
|
1025
|
+
to { opacity: 0; transform: translateX(-48px) scale(0.98); }
|
|
1026
|
+
}
|
|
1027
|
+
@keyframes modal-card-slide-in-right {
|
|
1028
|
+
from { opacity: 0; transform: translateX(48px) scale(0.98); }
|
|
1029
|
+
to { opacity: 1; transform: translateX(0) scale(1); }
|
|
1030
|
+
}
|
|
1031
|
+
.modal-card-swap-out-back { animation: modal-card-slide-out-left 220ms cubic-bezier(0.4, 0, 1, 1) forwards; }
|
|
1032
|
+
.modal-card-swap-in-back { animation: modal-card-slide-in-right 220ms cubic-bezier(0, 0, 0.2, 1) both; }
|
|
1033
|
+
|
|
1034
|
+
/* Vestibular safety — drop the spring/slide, just fade fast. */
|
|
1035
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1036
|
+
.modal-card-mount,
|
|
1037
|
+
.modal-card-unmount,
|
|
1038
|
+
.modal-card-shake-in,
|
|
1039
|
+
.modal-card-swap-out,
|
|
1040
|
+
.modal-card-swap-in,
|
|
1041
|
+
.modal-card-swap-out-back,
|
|
1042
|
+
.modal-card-swap-in-back,
|
|
1043
|
+
.modal-backdrop-mount,
|
|
1044
|
+
.modal-backdrop-unmount {
|
|
1045
|
+
animation-duration: 80ms;
|
|
1046
|
+
animation-timing-function: linear;
|
|
1047
|
+
}
|
|
1048
|
+
@keyframes modal-card-shake-in { from { opacity: 0; } to { opacity: 1; } }
|
|
1049
|
+
@keyframes modal-card-in { from { opacity: 0; } to { opacity: 1; } }
|
|
1050
|
+
@keyframes modal-card-out { from { opacity: 1; } to { opacity: 0; } }
|
|
1051
|
+
@keyframes modal-card-slide-out-right { from { opacity: 1; } to { opacity: 0; } }
|
|
1052
|
+
@keyframes modal-card-slide-in-left { from { opacity: 0; } to { opacity: 1; } }
|
|
1053
|
+
@keyframes modal-card-slide-out-left { from { opacity: 1; } to { opacity: 0; } }
|
|
1054
|
+
@keyframes modal-card-slide-in-right { from { opacity: 0; } to { opacity: 1; } }
|
|
1055
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# The living style guide — a single admin-gated page (/admin/style) that renders
|
|
2
|
+
# the engine's shared UI primitives (buttons, surfaces, the seven motion
|
|
3
|
+
# primitives, the leveling badges, and the theme role tokens) live in the host
|
|
4
|
+
# app's theme. Mirrors ThemeSettingsController: a plain host-inherited controller,
|
|
5
|
+
# gated by `require_admin` from the already-included Studio::ErrorHandling concern.
|
|
6
|
+
# The view is a bare content wrapper that renders inside each host's
|
|
7
|
+
# application.html.erb, so it inherits that app's navbar + theme automatically.
|
|
8
|
+
#
|
|
9
|
+
# Reached at /admin/style (helper admin_style_path). The legacy /admin/design_system
|
|
10
|
+
# route redirects here and keeps its admin_design_system_path helper resolving, so a
|
|
11
|
+
# shipped host sidebar link that still points at the old helper keeps working.
|
|
12
|
+
class StyleController < ApplicationController
|
|
13
|
+
before_action :require_admin
|
|
14
|
+
|
|
15
|
+
# Load the live theme so the Theme section (the landing section, folded in from
|
|
16
|
+
# /admin/theme) shows this app's saved colors + the CSRF-protected editor.
|
|
17
|
+
# Best-effort: the section renders from Studio.theme_config defaults when no
|
|
18
|
+
# ThemeSetting row exists, so a load hiccup must not take the whole page down.
|
|
19
|
+
def index
|
|
20
|
+
@theme_setting = ThemeSetting.current if defined?(ThemeSetting)
|
|
21
|
+
@theme_defaults = Studio.theme_config if Studio.respond_to?(:theme_config)
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
Rails.logger.warn("[style] theme preload skipped: #{e.message}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -21,8 +21,12 @@
|
|
|
21
21
|
|
|
22
22
|
Single root: lives inside the host's <template x-if>, so only the outer
|
|
23
23
|
div is the top-level element here.
|
|
24
|
+
|
|
25
|
+
Local: store — Alpine store name this modal is mounted in. Default "modals".
|
|
26
|
+
The living style guide passes store: "dsModals" for its page-scoped host.
|
|
24
27
|
%>
|
|
25
|
-
|
|
28
|
+
<% crop_store = local_assigns.fetch(:store, "modals") %>
|
|
29
|
+
<div x-data="cropPhotoModal({ store: '<%= crop_store %>' })">
|
|
26
30
|
<h3 class="text-heading font-bold text-lg text-center mb-4">Crop Photo</h3>
|
|
27
31
|
|
|
28
32
|
<template x-if="error">
|