d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
367 lines
9.1 KiB
CSS
367 lines
9.1 KiB
CSS
/**
|
|
* ThinkingLoader gooey animation
|
|
*
|
|
* Metaball technique on the SVG alpha channel: ink shapes are blurred with
|
|
* feGaussianBlur, then feColorMatrix crushes the soft alpha back to hard
|
|
* edges. Unlike the CSS blur+contrast trick this needs no solid backdrop and
|
|
* no blend mode, so the loader composites cleanly on any background.
|
|
*
|
|
* Every variant is authored in the shared 100x100 viewBox. All stages stack
|
|
* inside one filtered group; because the alpha crush happens after the
|
|
* stages composite, crossfading two stages reads as one pattern melting
|
|
* into the next, which is what drives the cycling morph.
|
|
*
|
|
* Ink color rides on currentColor: near-black on light surfaces, off-white
|
|
* in dark mode.
|
|
*/
|
|
|
|
.frame {
|
|
/* currentColor paints the squeeze ring's stroked arcs (the only non-filled
|
|
geometry). It tracks the loader polarity — dark on light, light on dark —
|
|
matching the gradient stops below. */
|
|
color: #2c2c2c;
|
|
flex: none;
|
|
/* Loader fill is a radial gradient (center → edge) plus a soft white inner
|
|
glow, per the Figma loader spec. Stops and glow opacity are theme vars,
|
|
inherited by the gradient stops and feFlood inside the SVG, so one set of
|
|
defs serves both themes. Light mode = the DARK-grey loader (reads on white):
|
|
#2C2C2C → #5F5F5F, glow white 0.6. */
|
|
--tl-grad-inner: #2c2c2c;
|
|
--tl-grad-outer: #5f5f5f;
|
|
--tl-glow: rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
/* Gradient stops + glow flood read the theme vars via CSS (a raw `var()` in an
|
|
SVG presentation attribute would not resolve — it must come through CSS).
|
|
`stop-color`/`flood-color` are transitionable even when fed by a var, so a
|
|
caller that re-points the ink vars (e.g. settling the loader's material into
|
|
a surface it hands off to) gets a smooth color tween, not a hard flip. */
|
|
.gradInner {
|
|
stop-color: var(--tl-grad-inner);
|
|
transition: stop-color 450ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
}
|
|
.gradOuter {
|
|
stop-color: var(--tl-grad-outer);
|
|
transition: stop-color 450ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
}
|
|
.glow {
|
|
flood-color: var(--tl-glow);
|
|
transition: flood-color 450ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
}
|
|
|
|
/* Wall-clock phase lock: the component sets --tl-sync to a shared negative
|
|
delay (now mod the common animation period), so every instance's keyframes
|
|
line up no matter when it mounted. The doubled class out-specifies the
|
|
shape classes' animation shorthand (implicit 0s delay) regardless of
|
|
source order. */
|
|
.frame.frame * {
|
|
animation-delay: var(--tl-sync, 0s);
|
|
}
|
|
|
|
:global(.dark) .frame {
|
|
color: #d6d6d6;
|
|
/* Dark mode = the LIGHT-grey loader (reads on dark): #A7A7A7 → #D6D6D6,
|
|
glow white 0.9. */
|
|
--tl-grad-inner: #a7a7a7;
|
|
--tl-grad-outer: #d6d6d6;
|
|
--tl-glow: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
/* An explicit `.light` wrapper (e.g. the always-light landing) re-asserts the
|
|
dark-grey loader even when an outer `.dark` theme is on the document — the
|
|
nearer theme wins. Ordered after the `.dark` rule so on equal specificity
|
|
(both ancestors present) this takes precedence. */
|
|
:global(.light) .frame {
|
|
color: #2c2c2c;
|
|
--tl-grad-inner: #2c2c2c;
|
|
--tl-grad-outer: #5f5f5f;
|
|
--tl-glow: rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
.labelRow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
/* gap + label size scale with the loader (vars set per-instance); defaults
|
|
match the chat's size-20 loader (8px gap, 14px text). */
|
|
gap: var(--tl-label-gap, 8px);
|
|
/* The status phrase must never break onto a second line — the glyph + phrase
|
|
stay on one row regardless of phrase length or how narrow the available
|
|
width is (e.g. the hero loader docked near the card's right edge).
|
|
white-space inherits, so this one declaration reaches both the shimmer
|
|
(.label) and static (.labelStatic) text spans. */
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The sweeping-band treatment itself (gradient, timing, dark mode, reduced
|
|
motion) is owned by the shared shimmer-text module; this class only adds the
|
|
loader-scaled font sizing. Canonical normal weight per emcn rules: body text
|
|
is 400, never medium. */
|
|
.label {
|
|
composes: shimmer from "./shimmer-text.module.css";
|
|
font-size: var(--tl-label-size, 14px);
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* Static label (shimmer off): the phrase in solid body ink, no gradient sweep. */
|
|
.labelStatic {
|
|
font-size: var(--tl-label-size, 14px);
|
|
font-weight: 400;
|
|
color: var(--text-body);
|
|
}
|
|
|
|
/* Phrase crossfade — the incoming phrase rises and fades in while the outgoing
|
|
one rises and fades out (stacked over it), so phrases rotate smoothly. */
|
|
.labelStack {
|
|
position: relative;
|
|
display: inline-flex;
|
|
}
|
|
.labelLayer {
|
|
display: inline-flex;
|
|
}
|
|
/* Incoming phrase wipes in left → right through an animated gradient mask
|
|
(soft trailing edge), so it's revealed horizontally rather than fading up. */
|
|
.labelIn {
|
|
-webkit-mask-image: linear-gradient(90deg, #000 0, #000 70%, transparent 100%);
|
|
mask-image: linear-gradient(90deg, #000 0, #000 70%, transparent 100%);
|
|
-webkit-mask-size: 300% 100%;
|
|
mask-size: 300% 100%;
|
|
-webkit-mask-repeat: no-repeat;
|
|
mask-repeat: no-repeat;
|
|
animation: label-reveal 460ms ease both;
|
|
}
|
|
.labelOut {
|
|
position: absolute;
|
|
inset: 0;
|
|
animation: label-out 300ms ease both;
|
|
}
|
|
@keyframes label-reveal {
|
|
from {
|
|
opacity: 0;
|
|
-webkit-mask-position: 100% 0;
|
|
mask-position: 100% 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
-webkit-mask-position: 0% 0;
|
|
mask-position: 0% 0;
|
|
}
|
|
}
|
|
@keyframes label-out {
|
|
to {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.stage {
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
|
|
.stageActive {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* metaballs — two circles drift apart and melt back together */
|
|
.metaballsA {
|
|
animation: metaballs-a 1s infinite alternate;
|
|
}
|
|
.metaballsB {
|
|
animation: metaballs-b 1s infinite alternate;
|
|
}
|
|
@keyframes metaballs-a {
|
|
90%,
|
|
100% {
|
|
transform: translateX(28px);
|
|
}
|
|
}
|
|
@keyframes metaballs-b {
|
|
90%,
|
|
100% {
|
|
transform: translateX(-28px);
|
|
}
|
|
}
|
|
|
|
/* relay (Return) — always left → right: the ball emerges from the left post,
|
|
travels across, and dissolves into the right post (the goo morphs it into
|
|
that shape). It fades out at the right and in at the left, so the reset back
|
|
to the start happens while invisible — no bounce, no hard snap. */
|
|
.relayBall {
|
|
animation: relay 1.3s linear infinite;
|
|
}
|
|
@keyframes relay {
|
|
0% {
|
|
transform: translateX(0);
|
|
opacity: 0;
|
|
}
|
|
20% {
|
|
opacity: 1;
|
|
}
|
|
78% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
transform: translateX(58px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* corners — four dots rotate around a center square */
|
|
.cornersA {
|
|
animation: corners-a 0.8s infinite;
|
|
}
|
|
.cornersB {
|
|
animation: corners-b 0.8s infinite;
|
|
}
|
|
.cornersC {
|
|
animation: corners-c 0.8s infinite;
|
|
}
|
|
.cornersD {
|
|
animation: corners-d 0.8s infinite;
|
|
}
|
|
@keyframes corners-a {
|
|
100% {
|
|
transform: translate(46px, 0);
|
|
}
|
|
}
|
|
@keyframes corners-b {
|
|
100% {
|
|
transform: translate(0, 46px);
|
|
}
|
|
}
|
|
@keyframes corners-c {
|
|
100% {
|
|
transform: translate(-46px, 0);
|
|
}
|
|
}
|
|
@keyframes corners-d {
|
|
100% {
|
|
transform: translate(0, -46px);
|
|
}
|
|
}
|
|
|
|
/* burst — four dots repeatedly fling out of a center cross */
|
|
.burstUp {
|
|
animation: burst-up 0.8s infinite;
|
|
}
|
|
.burstDown {
|
|
animation: burst-down 0.8s infinite;
|
|
}
|
|
.burstLeft {
|
|
animation: burst-left 0.8s infinite;
|
|
}
|
|
.burstRight {
|
|
animation: burst-right 0.8s infinite;
|
|
}
|
|
@keyframes burst-up {
|
|
100% {
|
|
transform: translateY(-50px);
|
|
}
|
|
}
|
|
@keyframes burst-down {
|
|
100% {
|
|
transform: translateY(50px);
|
|
}
|
|
}
|
|
@keyframes burst-left {
|
|
100% {
|
|
transform: translateX(-50px);
|
|
}
|
|
}
|
|
@keyframes burst-right {
|
|
100% {
|
|
transform: translateX(50px);
|
|
}
|
|
}
|
|
|
|
/* compass — one dot tours the four cardinal points */
|
|
.compassMover {
|
|
animation: compass 2s infinite;
|
|
}
|
|
@keyframes compass {
|
|
0% {
|
|
transform: translate(0, 0);
|
|
}
|
|
25% {
|
|
transform: translate(27px, 27px);
|
|
}
|
|
50% {
|
|
transform: translate(0, 54px);
|
|
}
|
|
75% {
|
|
transform: translate(-27px, 27px);
|
|
}
|
|
100% {
|
|
transform: translate(0, 0);
|
|
}
|
|
}
|
|
|
|
/* squeeze — two bars pinch together inside the ring arcs */
|
|
.squeezeBarL {
|
|
animation: squeeze-l 0.6s infinite alternate;
|
|
}
|
|
.squeezeBarR {
|
|
animation: squeeze-r 0.6s infinite alternate;
|
|
}
|
|
@keyframes squeeze-l {
|
|
0%,
|
|
30% {
|
|
transform: translateX(0);
|
|
}
|
|
100% {
|
|
transform: translateX(10px);
|
|
}
|
|
}
|
|
@keyframes squeeze-r {
|
|
0%,
|
|
30% {
|
|
transform: translateX(0);
|
|
}
|
|
100% {
|
|
transform: translateX(-10px);
|
|
}
|
|
}
|
|
|
|
/* thinking — three small blobs drift out from the core and back on offset
|
|
periods (1.3/1.6/1.9s), so the merged cloud churns and never repeats:
|
|
the Core weighing options. */
|
|
.thinkA {
|
|
animation: think-a 1.6s infinite alternate;
|
|
}
|
|
.thinkB {
|
|
animation: think-b 1.9s infinite alternate;
|
|
}
|
|
.thinkC {
|
|
animation: think-c 1.3s infinite alternate;
|
|
}
|
|
@keyframes think-a {
|
|
100% {
|
|
transform: translate(-20px, -14px);
|
|
}
|
|
}
|
|
@keyframes think-b {
|
|
100% {
|
|
transform: translate(22px, -10px);
|
|
}
|
|
}
|
|
@keyframes think-c {
|
|
100% {
|
|
transform: translate(2px, 22px);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.frame *,
|
|
.labelIn {
|
|
animation: none;
|
|
}
|
|
.gradInner,
|
|
.gradOuter,
|
|
.glow {
|
|
transition: none;
|
|
}
|
|
/* No crossfade — the incoming phrase just replaces the outgoing one. */
|
|
.labelOut {
|
|
display: none;
|
|
}
|
|
}
|