From b0bee1e8180486aa1617d543bbb3abefc205153a Mon Sep 17 00:00:00 2001 From: RedStar Date: Sun, 31 May 2026 17:21:24 +0200 Subject: [PATCH 1/2] feat(ui): apply icon-swap transition to clipboard/check icons - Add .t-icon-swap CSS block to utilities.css (transitions.dev #09) - Both icons stacked in the same grid cell; state toggles via data-state - Apply to copy-user-id button in profile.vue - Apply to copy-server-id button in guild settings General.vue - prefers-reduced-motion honored via existing global kill-switch in utilities.css --- app/assets/css/utilities.css | 40 +++++++++++++++++++++++ app/components/guild/settings/General.vue | 13 ++++---- app/pages/profile.vue | 17 +++++++--- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/app/assets/css/utilities.css b/app/assets/css/utilities.css index 76cdee6d4..89dd0cb3a 100644 --- a/app/assets/css/utilities.css +++ b/app/assets/css/utilities.css @@ -364,6 +364,46 @@ } } +/* ============================================================================= + Icon Swap (transitions.dev #09) + Cross-fade two icons in the same grid slot with blur and scale. + ============================================================================= */ + +:root { + --icon-swap-dur: 200ms; + --icon-swap-blur: 2px; + --icon-swap-start-scale: 0.25; + --icon-swap-ease: ease-in-out; +} + +.t-icon-swap { + position: relative; + display: inline-grid; +} + +.t-icon-swap .t-icon { + grid-area: 1 / 1; + transition: + opacity var(--icon-swap-dur) var(--icon-swap-ease), + filter var(--icon-swap-dur) var(--icon-swap-ease), + transform var(--icon-swap-dur) var(--icon-swap-ease); + will-change: opacity, filter, transform; +} + +.t-icon-swap[data-state="a"] .t-icon[data-icon="a"], +.t-icon-swap[data-state="b"] .t-icon[data-icon="b"] { + opacity: 1; + filter: blur(0); + transform: scale(1); +} + +.t-icon-swap[data-state="a"] .t-icon[data-icon="b"], +.t-icon-swap[data-state="b"] .t-icon[data-icon="a"] { + opacity: 0; + filter: blur(var(--icon-swap-blur)); + transform: scale(var(--icon-swap-start-scale)); +} + /* ----------------------------------------------------------------------------- Page Transitions ----------------------------------------------------------------------------- */ diff --git a/app/components/guild/settings/General.vue b/app/components/guild/settings/General.vue index fd0ac5fce..a9e329051 100644 --- a/app/components/guild/settings/General.vue +++ b/app/components/guild/settings/General.vue @@ -24,12 +24,13 @@
- + + {{ copied ? "Copied!" : "Copy Server ID" }} {{ user.id }} From c5a9add238bb35695c698a7daa2993df62cc8c49 Mon Sep 17 00:00:00 2001 From: RedStar Date: Sun, 31 May 2026 17:28:10 +0200 Subject: [PATCH 2/2] feat(skills): add avatar group hover and error state shake - Introduce avatar group hover transition with customizable lift, scale, and easing. - Implement error state shake transition for form validation feedback. - Create a comprehensive SKILL.md for transitions with usage examples and decision rules. - Add universal CSS root variables for consistent transition styling. - Update skills-lock.json to include transitions-dev skill. --- .../skills/transitions-dev/01-card-resize.md | 53 +++++ .../transitions-dev/02-number-pop-in.md | 119 ++++++++++ .../transitions-dev/03-notification-badge.md | 110 +++++++++ .../transitions-dev/04-text-states-swap.md | 97 ++++++++ .../transitions-dev/05-menu-dropdown.md | 105 +++++++++ .agents/skills/transitions-dev/06-modal.md | 94 ++++++++ .../skills/transitions-dev/07-panel-reveal.md | 81 +++++++ .../transitions-dev/08-page-side-by-side.md | 100 ++++++++ .../skills/transitions-dev/09-icon-swap.md | 78 +++++++ .../transitions-dev/10-success-check.md | 169 ++++++++++++++ .../transitions-dev/11-avatar-group-hover.md | 200 ++++++++++++++++ .../transitions-dev/12-error-state-shake.md | 202 ++++++++++++++++ .agents/skills/transitions-dev/SKILL.md | 220 ++++++++++++++++++ .agents/skills/transitions-dev/_root.css | 94 ++++++++ skills-lock.json | 6 + 15 files changed, 1728 insertions(+) create mode 100644 .agents/skills/transitions-dev/01-card-resize.md create mode 100644 .agents/skills/transitions-dev/02-number-pop-in.md create mode 100644 .agents/skills/transitions-dev/03-notification-badge.md create mode 100644 .agents/skills/transitions-dev/04-text-states-swap.md create mode 100644 .agents/skills/transitions-dev/05-menu-dropdown.md create mode 100644 .agents/skills/transitions-dev/06-modal.md create mode 100644 .agents/skills/transitions-dev/07-panel-reveal.md create mode 100644 .agents/skills/transitions-dev/08-page-side-by-side.md create mode 100644 .agents/skills/transitions-dev/09-icon-swap.md create mode 100644 .agents/skills/transitions-dev/10-success-check.md create mode 100644 .agents/skills/transitions-dev/11-avatar-group-hover.md create mode 100644 .agents/skills/transitions-dev/12-error-state-shake.md create mode 100644 .agents/skills/transitions-dev/SKILL.md create mode 100644 .agents/skills/transitions-dev/_root.css diff --git a/.agents/skills/transitions-dev/01-card-resize.md b/.agents/skills/transitions-dev/01-card-resize.md new file mode 100644 index 000000000..893c07f62 --- /dev/null +++ b/.agents/skills/transitions-dev/01-card-resize.md @@ -0,0 +1,53 @@ +# Card resize + +## When to use + +Tweening a container's width or height when its layout state changes (compact ↔ expanded card, collapsing panel, list row toggling extra detail). Pure CSS — no JS required beyond the class toggle that drives the size change. + +## HTML usage + +```html +
+``` + +Put `.t-resize` on any element and change its width/height +(directly, or via a state class such as `.is-small`). The +transition will tween the two sizes. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--resize-dur` | `300ms` | sourced from `--p4-dur` | +| `--resize-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p4-ease` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --resize-dur: 300ms; + --resize-ease: cubic-bezier(0.22, 1, 0.36, 1); +} +``` + +## CSS + +```css +.t-resize { + transition: + width var(--resize-dur) var(--resize-ease), + height var(--resize-dur) var(--resize-ease); + will-change: width, height; +} + +@media (prefers-reduced-motion: reduce) { + .t-resize { transition: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app. + diff --git a/.agents/skills/transitions-dev/02-number-pop-in.md b/.agents/skills/transitions-dev/02-number-pop-in.md new file mode 100644 index 000000000..196ced01d --- /dev/null +++ b/.agents/skills/transitions-dev/02-number-pop-in.md @@ -0,0 +1,119 @@ +# Number pop-in + +## When to use + +Counters, prices, balances, or any number that updates and should re-enter from a direction with blur. Each character animates independently and the last two digits stagger so decimals feel alive without looking chaotic. + +## HTML usage + +```html + + 1 + 2 + . + 3 + +``` + +Replay: + - Remove `.is-animating`, re-render digits (or swap text), + force a reflow, then re-add `.is-animating`. + - Use data-stagger="1", "2", … to delay individual + digits by `n * var(--digit-stagger)`. + +Direction: + --digit-dir-x / --digit-dir-y are unit-less multipliers + (e.g. 1, -1, 0) applied to --digit-distance. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--digit-dur` | `500ms` | sourced from `--p9-dur` | +| `--digit-distance` | `8px` | sourced from `--p9-distance` | +| `--digit-stagger` | `70ms` | sourced from `--p9-stagger` | +| `--digit-blur` | `2px` | sourced from `--p9-blur` | +| `--digit-ease` | `cubic-bezier(0.34, 1.45, 0.64, 1)` | sourced from `--p9-ease` | +| `--digit-dir-x` | `0` | sourced from `--p9-dir-x` | +| `--digit-dir-y` | `1` | sourced from `--p9-dir-y` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --digit-dur: 500ms; + --digit-distance: 8px; + --digit-stagger: 70ms; + --digit-blur: 2px; + --digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1); + --digit-dir-x: 0; + --digit-dir-y: 1; +} +``` + +## CSS + +```css +@keyframes t-digit-pop-in { + 0% { + transform: translate( + calc(var(--digit-distance) * var(--digit-dir-x)), + calc(var(--digit-distance) * var(--digit-dir-y)) + ); + opacity: 0; + filter: blur(var(--digit-blur)); + } + 100% { transform: translate(0, 0); opacity: 1; filter: blur(0); } +} + +.t-digit-group { + display: inline-flex; + align-items: baseline; +} +.t-digit { + display: inline-block; + will-change: transform, opacity, filter; +} +.t-digit-group.is-animating .t-digit { + animation: t-digit-pop-in var(--digit-dur) var(--digit-ease) both; +} +.t-digit-group.is-animating .t-digit[data-stagger="1"] { + animation-delay: var(--digit-stagger); +} +.t-digit-group.is-animating .t-digit[data-stagger="2"] { + animation-delay: calc(var(--digit-stagger) * 2); +} + +@media (prefers-reduced-motion: reduce) { + .t-digit-group .t-digit { animation: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +```js +// Replay the digit pop-in: remove .is-animating, swap the digit spans, +// force a reflow, then re-add .is-animating. Mark the last two digits +// with data-stagger="1" / "2" so they ride in 1× / 2× --digit-stagger +// behind the leading digits. +const group = document.querySelector(".t-digit-group"); + +function setDigits(str) { + group.classList.remove("is-animating"); + group.replaceChildren(); + const chars = str.split(""); + chars.forEach((ch, i) => { + const span = document.createElement("span"); + span.className = "t-digit"; + span.textContent = ch; + if (i === chars.length - 2) span.dataset.stagger = "1"; + else if (i === chars.length - 1) span.dataset.stagger = "2"; + group.appendChild(span); + }); + void group.offsetHeight; // force reflow + group.classList.add("is-animating"); +} +``` + diff --git a/.agents/skills/transitions-dev/03-notification-badge.md b/.agents/skills/transitions-dev/03-notification-badge.md new file mode 100644 index 000000000..f00b034f2 --- /dev/null +++ b/.agents/skills/transitions-dev/03-notification-badge.md @@ -0,0 +1,110 @@ +# Notification badge + +## When to use + +A small badge appearing on top of a trigger (bell, inbox, button). Slides in diagonally and pops the dot independently of the trigger so the trigger itself never moves. + +## HTML usage + +```html + + + +``` + +State: toggle data-open="true" / "false" on .t-badge. +Only the badge slides + pops — the trigger itself stays put. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--badge-slide-dur` | `260ms` | sourced from `--p1-pos-open-dur` | +| `--badge-pop-dur` | `500ms` | sourced from `--p1-scale-open-dur` | +| `--badge-pop-close-dur` | `180ms` | sourced from `--p1-scale-close-dur` | +| `--badge-fade-dur` | `400ms` | sourced from `--p1-opacity-open-dur` | +| `--badge-fade-close-dur` | `180ms` | sourced from `--p1-opacity-close-dur` | +| `--badge-blur` | `2px` | sourced from `--p1-blur` | +| `--badge-offset-x` | `-8.2px` | sourced from `--p1-distance-x` | +| `--badge-offset-y` | `12.4px` | sourced from `--p1-distance-y` | +| `--badge-slide-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p1-ease-pos-open` | +| `--badge-pop-ease` | `cubic-bezier(0.34, 1.36, 0.64, 1)` | sourced from `--p1-ease-scale-open` | +| `--badge-close-ease` | `cubic-bezier(0.4, 0, 0.2, 1)` | sourced from `--p1-ease-close` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --badge-slide-dur: 260ms; + --badge-pop-dur: 500ms; + --badge-pop-close-dur: 180ms; + --badge-fade-dur: 400ms; + --badge-fade-close-dur: 180ms; + --badge-blur: 2px; + --badge-offset-x: -8.2px; + --badge-offset-y: 12.4px; + --badge-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); + --badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1); + --badge-close-ease: cubic-bezier(0.4, 0, 0.2, 1); +} +``` + +## CSS + +```css +@keyframes t-badge-slide-in { + from { transform: translate(var(--badge-offset-x), var(--badge-offset-y)); } + to { transform: translate(0, 0); } +} + +/* .t-badge is the absolutely-positioned wrapper for the dot. + Adjust top/right (or left/bottom) to anchor it on your trigger. */ +.t-badge { + position: absolute; + top: -6px; + right: -8px; + pointer-events: none; + will-change: transform; +} +.t-badge[data-open="true"] { + animation: t-badge-slide-in var(--badge-slide-dur) var(--badge-slide-ease); +} + +.t-badge-dot { + display: block; + transform-origin: center; + transform: scale(1); + opacity: 1; + filter: blur(0); + transition: + transform var(--badge-pop-dur) var(--badge-pop-ease), + opacity var(--badge-fade-dur) var(--badge-pop-ease), + filter var(--badge-pop-dur) var(--badge-pop-ease); + will-change: transform, opacity, filter; +} +.t-badge[data-open="false"] .t-badge-dot { + transform: scale(0); + opacity: 0; + filter: blur(var(--badge-blur)); + transition: + transform var(--badge-pop-close-dur) var(--badge-close-ease), + opacity var(--badge-fade-close-dur) var(--badge-close-ease), + filter var(--badge-pop-close-dur) var(--badge-close-ease); +} + +@media (prefers-reduced-motion: reduce) { + .t-badge, .t-badge-dot { animation: none !important; transition: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app. + diff --git a/.agents/skills/transitions-dev/04-text-states-swap.md b/.agents/skills/transitions-dev/04-text-states-swap.md new file mode 100644 index 000000000..39bbafd03 --- /dev/null +++ b/.agents/skills/transitions-dev/04-text-states-swap.md @@ -0,0 +1,97 @@ +# Text states swap + +## When to use + +Swapping the text of a status indicator in place — "Processing…" → "Done", "Save" → "Saved". The old text exits up with blur, the new text enters from below. + +## HTML usage + +```html +Processing… +``` + +Driven by JS (three-phase sequence): + 1. Add `.is-exit` -> old text slides up + blurs + fades. + 2. After --text-swap-dur: change textContent, then add + `.is-enter-start` (jumps to below, no transition). + 3. Force reflow, remove `.is-enter-start` so the new text + animates back to 0 with the default transition. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--text-swap-dur` | `150ms` | sourced from `--p6-dur` | +| `--text-swap-translate-y` | `4px` | sourced from `--p6-translate-y` | +| `--text-swap-blur` | `2px` | sourced from `--p6-blur` | +| `--text-swap-ease` | `ease-in-out` | sourced from `--p6-ease` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --text-swap-dur: 150ms; + --text-swap-translate-y: 4px; + --text-swap-blur: 2px; + --text-swap-ease: ease-in-out; +} +``` + +## CSS + +```css +.t-text-swap { + display: inline-block; + transform: translateY(0); + filter: blur(0); + opacity: 1; + transition: + transform var(--text-swap-dur) var(--text-swap-ease), + filter var(--text-swap-dur) var(--text-swap-ease), + opacity var(--text-swap-dur) var(--text-swap-ease); + will-change: transform, filter, opacity; +} +.t-text-swap.is-exit { + transform: translateY(calc(var(--text-swap-translate-y) * -1)); + filter: blur(var(--text-swap-blur)); + opacity: 0; +} +.t-text-swap.is-enter-start { + transform: translateY(var(--text-swap-translate-y)); + filter: blur(var(--text-swap-blur)); + opacity: 0; + transition: none; +} + +@media (prefers-reduced-motion: reduce) { + .t-text-swap { transition: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +```js +// Three-phase text swap: +// 1. Add .is-exit — old text exits up with blur. +// 2. After --text-swap-dur, swap textContent and add .is-enter-start +// (jumps to "below, no transition"), force a reflow. +// 3. Remove .is-enter-start — new text animates back to rest. +const el = document.querySelector(".t-text-swap"); +const dur = parseFloat( + getComputedStyle(document.documentElement).getPropertyValue("--text-swap-dur") +) || 200; + +function swapText(next) { + el.classList.add("is-exit"); + setTimeout(() => { + el.textContent = next; + el.classList.remove("is-exit"); + el.classList.add("is-enter-start"); + void el.offsetHeight; // force reflow so the next change transitions + el.classList.remove("is-enter-start"); + }, dur); +} +``` + diff --git a/.agents/skills/transitions-dev/05-menu-dropdown.md b/.agents/skills/transitions-dev/05-menu-dropdown.md new file mode 100644 index 000000000..e2ef3f92f --- /dev/null +++ b/.agents/skills/transitions-dev/05-menu-dropdown.md @@ -0,0 +1,105 @@ +# Menu dropdown + +## When to use + +Contextual menus, dropdowns, popovers — anything that opens from a trigger and should visually grow from that trigger's position. Origin-aware via `data-origin` (top-left, top-center, top-right, bottom-*). + +## HTML usage + +```html +
+ +
+``` + +State: + - Add `.is-open` to show. + - On close, swap `.is-open` for `.is-closing`, then remove + `.is-closing` after --dropdown-close-dur. + +data-origin values: top-left | top-center | top-right | + bottom-left | bottom-center | bottom-right. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--dropdown-open-dur` | `250ms` | sourced from `--p2-open-dur` | +| `--dropdown-close-dur` | `150ms` | sourced from `--p2-close-dur` | +| `--dropdown-pre-scale` | `0.97` | sourced from `--p2-pre-scale` | +| `--dropdown-closing-scale` | `0.99` | sourced from `--p2-closing-scale` | +| `--dropdown-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p2-ease` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --dropdown-open-dur: 250ms; + --dropdown-close-dur: 150ms; + --dropdown-pre-scale: 0.97; + --dropdown-closing-scale: 0.99; + --dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1); +} +``` + +## CSS + +```css +.t-dropdown { + transform-origin: top left; + transform: scale(var(--dropdown-pre-scale)); + opacity: 0; + pointer-events: none; + transition: + transform var(--dropdown-open-dur) var(--dropdown-ease), + opacity var(--dropdown-open-dur) var(--dropdown-ease); + will-change: transform, opacity; +} +.t-dropdown[data-origin="top-right"] { transform-origin: top right; } +.t-dropdown[data-origin="top-center"] { transform-origin: top center; } +.t-dropdown[data-origin="bottom-left"] { transform-origin: bottom left; } +.t-dropdown[data-origin="bottom-center"] { transform-origin: bottom center; } +.t-dropdown[data-origin="bottom-right"] { transform-origin: bottom right; } + +.t-dropdown.is-open { + transform: scale(1); + opacity: 1; + pointer-events: auto; +} +.t-dropdown.is-closing { + transform: scale(var(--dropdown-closing-scale)); + opacity: 0; + pointer-events: none; + transition: + transform var(--dropdown-close-dur) var(--dropdown-ease), + opacity var(--dropdown-close-dur) var(--dropdown-ease); +} + +@media (prefers-reduced-motion: reduce) { + .t-dropdown { transition: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +```js +// Toggle .is-open / .is-closing with a setTimeout cleanup so the closing +// scale animates before the element resets to its pre-open rest state. +const dropdown = document.querySelector(".t-dropdown"); +const closeMs = parseFloat( + getComputedStyle(document.documentElement).getPropertyValue("--dropdown-close-dur") +) || 150; + +function openDropdown() { + dropdown.classList.remove("is-closing"); + dropdown.classList.add("is-open"); +} +function closeDropdown() { + dropdown.classList.remove("is-open"); + dropdown.classList.add("is-closing"); + setTimeout(() => dropdown.classList.remove("is-closing"), closeMs); +} +``` + diff --git a/.agents/skills/transitions-dev/06-modal.md b/.agents/skills/transitions-dev/06-modal.md new file mode 100644 index 000000000..87b2a5407 --- /dev/null +++ b/.agents/skills/transitions-dev/06-modal.md @@ -0,0 +1,94 @@ +# Modal open / close + +## When to use + +Modal dialogs and full-overlay surfaces that scale up from center. Use when the surface is conceptually "on top of" the page rather than anchored to a trigger. + +## HTML usage + +```html + +``` + +State: + - Add `.is-open` to open (scales up from --modal-scale). + - On close, swap `.is-open` for `.is-closing`, then remove + `.is-closing` after --modal-close-dur. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--modal-open-dur` | `250ms` | sourced from `--p7-open-dur` | +| `--modal-close-dur` | `150ms` | sourced from `--p7-close-dur` | +| `--modal-scale` | `0.96` | sourced from `--p7-scale` | +| `--modal-scale-close` | `0.96` | sourced from `--p7-scale-close` | +| `--modal-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p7-ease` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --modal-open-dur: 250ms; + --modal-close-dur: 150ms; + --modal-scale: 0.96; + --modal-scale-close: 0.96; + --modal-ease: cubic-bezier(0.22, 1, 0.36, 1); +} +``` + +## CSS + +```css +.t-modal { + transform-origin: center; + transform: scale(var(--modal-scale)); + opacity: 0; + pointer-events: none; + transition: + transform var(--modal-open-dur) var(--modal-ease), + opacity var(--modal-open-dur) var(--modal-ease); + will-change: transform, opacity; +} +.t-modal.is-open { + transform: scale(1); + opacity: 1; + pointer-events: auto; +} +.t-modal.is-closing { + transform: scale(var(--modal-scale-close)); + opacity: 0; + pointer-events: none; + transition: + transform var(--modal-close-dur) var(--modal-ease), + opacity var(--modal-close-dur) var(--modal-ease); +} + +@media (prefers-reduced-motion: reduce) { + .t-modal { transition: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +```js +// Same close-then-cleanup pattern as the dropdown — modals scale from +// --modal-scale up to 1, then on close dip to --modal-scale-close. +const modal = document.querySelector(".t-modal"); +const closeMs = parseFloat( + getComputedStyle(document.documentElement).getPropertyValue("--modal-close-dur") +) || 150; + +function openModal() { + modal.classList.remove("is-closing"); + modal.classList.add("is-open"); +} +function closeModal() { + modal.classList.remove("is-open"); + modal.classList.add("is-closing"); + setTimeout(() => modal.classList.remove("is-closing"), closeMs); +} +``` + diff --git a/.agents/skills/transitions-dev/07-panel-reveal.md b/.agents/skills/transitions-dev/07-panel-reveal.md new file mode 100644 index 000000000..5c7a32a04 --- /dev/null +++ b/.agents/skills/transitions-dev/07-panel-reveal.md @@ -0,0 +1,81 @@ +# Panel reveal + +## When to use + +A panel that slides into view inside an existing container — e.g. detail panel inside a card, expanding section. Combines a short translate, opacity, and a 2px cross-blur so a half-height travel still reads as a full open. + +## HTML usage + +```html +
+ +
+``` + +The panel slides on the Y axis, fades opacity 0 ↔ 1, +and cross-blurs --panel-blur ↔ 0, all on the same +duration / ease so a shorter travel (e.g. 50% of the +panel height) still reads as a full open / close. +Wrap it in your own container with `overflow: hidden` +if you want the closed state fully clipped. Set +--panel-translate-y to the travel distance (e.g. half +the panel's own height). + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--panel-open-dur` | `400ms` | sourced from `--p3-open-dur` | +| `--panel-close-dur` | `350ms` | sourced from `--p3-close-dur` | +| `--panel-translate-y` | `100px` | sourced from `--p3-translate-y` | +| `--panel-blur` | `2px` | sourced from `--p3-blur` | +| `--panel-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p3-ease` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --panel-open-dur: 400ms; + --panel-close-dur: 350ms; + --panel-translate-y: 100px; + --panel-blur: 2px; + --panel-ease: cubic-bezier(0.22, 1, 0.36, 1); +} +``` + +## CSS + +```css +.t-panel-slide { + transform: translateY(var(--panel-translate-y)); + opacity: 0; + filter: blur(var(--panel-blur)); + pointer-events: none; + transition: + transform var(--panel-close-dur) var(--panel-ease), + opacity var(--panel-close-dur) var(--panel-ease), + filter var(--panel-close-dur) var(--panel-ease); + will-change: transform, opacity, filter; +} +.t-panel-slide[data-open="true"] { + transform: translateY(0); + opacity: 1; + filter: blur(0); + pointer-events: auto; + transition: + transform var(--panel-open-dur) var(--panel-ease), + opacity var(--panel-open-dur) var(--panel-ease), + filter var(--panel-open-dur) var(--panel-ease); +} + +@media (prefers-reduced-motion: reduce) { + .t-panel-slide { transition: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app. + diff --git a/.agents/skills/transitions-dev/08-page-side-by-side.md b/.agents/skills/transitions-dev/08-page-side-by-side.md new file mode 100644 index 000000000..1f9a608c0 --- /dev/null +++ b/.agents/skills/transitions-dev/08-page-side-by-side.md @@ -0,0 +1,100 @@ +# Page side-by-side + +## When to use + +Sliding between two full pages or screens that live side-by-side: list ↔ detail, step 1 ↔ step 2 in a wizard. Page 1 exits left, page 2 exits right. + +## HTML usage + +```html +
+
+
+
+``` + +State: set data-page="1" or "2" on .t-page-slide. +Page 1 exits to the left, page 2 exits to the right. +--page-exit-enabled (0/1) disables the outgoing slide. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--page-slide-dur` | `200ms` | sourced from `--p8-slide-dur` | +| `--page-fade-dur` | `200ms` | sourced from `--p8-fade-dur` | +| `--page-slide-distance` | `8px` | sourced from `--p8-distance` | +| `--page-blur` | `3px` | sourced from `--p8-blur` | +| `--page-stagger` | `0ms` | sourced from `--p8-stagger` | +| `--page-exit-enabled` | `1` | sourced from `--p8-exit-enabled` | +| `--page-slide-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p8-slide-ease` | +| `--page-fade-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p8-fade-ease` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --page-slide-dur: 200ms; + --page-fade-dur: 200ms; + --page-slide-distance: 8px; + --page-blur: 3px; + --page-stagger: 0ms; + --page-exit-enabled: 1; + --page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); + --page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1); +} +``` + +## CSS + +```css +.t-page-slide { + position: relative; +} +.t-page-slide .t-page[data-page-id="1"] { + --t-page-from-x: calc(var(--page-slide-distance) * -1); +} +.t-page-slide .t-page[data-page-id="2"] { + --t-page-from-x: var(--page-slide-distance); +} +.t-page-slide .t-page { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; + transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled))); + filter: blur(calc(var(--page-blur) * var(--page-exit-enabled))); + transition: + opacity var(--page-fade-dur) var(--page-fade-ease), + transform var(--page-slide-dur) var(--page-slide-ease), + filter var(--page-slide-dur) var(--page-slide-ease); + will-change: opacity, transform, filter; +} +.t-page-slide[data-page="1"] .t-page[data-page-id="1"], +.t-page-slide[data-page="2"] .t-page[data-page-id="2"] { + opacity: 1; + pointer-events: auto; + transform: translateX(0); + filter: blur(0); + transition-delay: var(--page-stagger); +} + +@media (prefers-reduced-motion: reduce) { + .t-page-slide .t-page { transition: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +```js +// Flip data-page on the container — the CSS handles the rest. +// Set --page-exit-enabled: 0 on the container if you want pages to +// fade without sliding (useful on first paint). +const slider = document.querySelector(".t-page-slide"); +function showPage(n) { + slider.setAttribute("data-page", String(n)); +} +``` + diff --git a/.agents/skills/transitions-dev/09-icon-swap.md b/.agents/skills/transitions-dev/09-icon-swap.md new file mode 100644 index 000000000..2843f9fff --- /dev/null +++ b/.agents/skills/transitions-dev/09-icon-swap.md @@ -0,0 +1,78 @@ +# Icon swap + +## When to use + +Cross-fading two icons in the same slot — hamburger ↔ close, sun ↔ moon, play ↔ pause, expand ↔ collapse. Both icons stay in the DOM stacked in the same grid cell. + +## HTML usage + +```html +
+ + +
+``` + +State: set data-state="a" or "b" on .t-icon-swap. +The matching .t-icon fades in; the other fades out with blur +and scale. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--icon-swap-dur` | `200ms` | sourced from `--p5-dur` | +| `--icon-swap-blur` | `2px` | sourced from `--p5-blur` | +| `--icon-swap-start-scale` | `0.25` | sourced from `--p5-start-scale` | +| `--icon-swap-ease` | `ease-in-out` | sourced from `--p5-ease` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --icon-swap-dur: 200ms; + --icon-swap-blur: 2px; + --icon-swap-start-scale: 0.25; + --icon-swap-ease: ease-in-out; +} +``` + +## CSS + +```css +.t-icon-swap { + position: relative; + display: inline-grid; +} +.t-icon-swap .t-icon { + grid-area: 1 / 1; + transition: + opacity var(--icon-swap-dur) var(--icon-swap-ease), + filter var(--icon-swap-dur) var(--icon-swap-ease), + transform var(--icon-swap-dur) var(--icon-swap-ease); + will-change: opacity, filter, transform; +} +.t-icon-swap[data-state="a"] .t-icon[data-icon="a"], +.t-icon-swap[data-state="b"] .t-icon[data-icon="b"] { + opacity: 1; + filter: blur(0); + transform: scale(1); +} +.t-icon-swap[data-state="a"] .t-icon[data-icon="b"], +.t-icon-swap[data-state="b"] .t-icon[data-icon="a"] { + opacity: 0; + filter: blur(var(--icon-swap-blur)); + transform: scale(var(--icon-swap-start-scale)); +} + +@media (prefers-reduced-motion: reduce) { + .t-icon-swap .t-icon { transition: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app. + diff --git a/.agents/skills/transitions-dev/10-success-check.md b/.agents/skills/transitions-dev/10-success-check.md new file mode 100644 index 000000000..f91370dda --- /dev/null +++ b/.agents/skills/transitions-dev/10-success-check.md @@ -0,0 +1,169 @@ +# Success check + +## When to use + +Confirming a completed action — payment processed, file uploaded, message sent, form saved. The icon fades in, rotates upright, settles with a Y-bob, and (for SVG icons) draws its path stroke. Use whenever a status changes from "pending / unknown" to "success" and you want the moment to feel earned rather than instantaneous. + +The snippet covers the **appear transition only** — bring your own hide behavior (e.g. unmount, opacity:0, or a custom exit). This is intentional: success states are usually persistent, and a soft fade-out is rarely worth the extra DOM/JS surface. + +## HTML usage + +```html + + +``` + +Trigger: + - Cold load is data-state="out" (opacity 0; no animation). + - Show: set data-state="in" (fade + rotate + blur + Y-bob + + path draw run in parallel). + +Snippet covers the appear transition only — bring your own +hide behavior (e.g. unmount, opacity:0, or a custom exit). + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--check-opacity-dur` | `550ms` | sourced from `--p10-opacity-dur` | +| `--check-rotate-dur` | `550ms` | sourced from `--p10-rotate-dur` | +| `--check-rotate-from` | `80deg` | sourced from `--p10-rotate-from` | +| `--check-bob-dur` | `450ms` | sourced from `--p10-bob-dur` | +| `--check-y-amount` | `40px` | sourced from `--p10-y-amount` | +| `--check-blur-dur` | `500ms` | sourced from `--p10-blur-dur` | +| `--check-blur-from` | `10px` | sourced from `--p10-blur-from` | +| `--check-path-dur` | `550ms` | sourced from `--p10-path-dur` | +| `--check-path-delay` | `80ms` | sourced from `--p10-path-delay` | +| `--check-ease-out` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p10-ease-out` | +| `--check-ease-opacity` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p10-ease-opacity` | +| `--check-ease-rotate` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p10-ease-rotate` | +| `--check-ease-bob` | `cubic-bezier(0.34, 1.35, 0.64, 1)` | sourced from `--p10-ease-bob` | +| `--check-ease-path` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p10-ease-path` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --check-opacity-dur: 550ms; + --check-rotate-dur: 550ms; + --check-rotate-from: 80deg; + --check-bob-dur: 450ms; + --check-y-amount: 40px; + --check-blur-dur: 500ms; + --check-blur-from: 10px; + --check-path-dur: 550ms; + --check-path-delay: 80ms; + --check-ease-out: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-opacity: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-rotate: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1); + --check-ease-path: cubic-bezier(0.22, 1, 0.36, 1); +} +``` + +## CSS + +```css +/* Wrapper drives the appear animation; it doesn't own any + sizing or color so you can drop in any icon. */ +.t-success-check { + display: inline-block; + transform-origin: center; + opacity: 0; + will-change: transform, opacity, filter; +} +/* overflow: visible keeps the stroke from clipping while it + draws; display: block kills the inline whitespace under SVGs. */ +.t-success-check svg { display: block; overflow: visible; } +/* Stroke-draw setup. Replace 20 with the result of + path.getTotalLength() for your path; round caps mean any + sub-pixel overshoot is invisible. */ +.t-success-check svg path { + stroke-dasharray: 20; + stroke-dashoffset: 20; +} + +.t-success-check[data-state="in"] { + animation: + t-check-fade var(--check-opacity-dur) var(--check-ease-opacity) forwards, + t-check-rotate var(--check-rotate-dur) var(--check-ease-rotate) forwards, + t-check-blur var(--check-blur-dur) var(--check-ease-out) forwards, + t-check-bob var(--check-bob-dur) var(--check-ease-bob) forwards; +} +.t-success-check[data-state="in"] svg path { + animation: t-check-draw var(--check-path-dur) var(--check-ease-path) var(--check-path-delay, 0ms) forwards; +} + +@keyframes t-check-fade { from { opacity: 0; } to { opacity: 1; } } +@keyframes t-check-rotate { + from { transform: rotate(var(--check-rotate-from)); } + to { transform: rotate(0deg); } +} +@keyframes t-check-blur { + from { filter: blur(var(--check-blur-from)); } + to { filter: blur(0); } +} +@keyframes t-check-bob { + from { translate: 0 var(--check-y-amount); } + to { translate: 0 0; } +} +@keyframes t-check-draw { to { stroke-dashoffset: 0; } } + +@media (prefers-reduced-motion: reduce) { + .t-success-check { animation: none !important; opacity: 1; } + .t-success-check svg path { animation: none !important; stroke-dashoffset: 0 !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +```js +// Cold-load → "out" (no animation). On show, flip to "in". +// Replay-on-retrigger: reset to "out", force a reflow, then flip +// back to "in" so the keyframes restart from offset 0. +const check = document.querySelector(".t-success-check"); + +function showCheck() { + check.setAttribute("data-state", "out"); + void check.offsetWidth; // force reflow so keyframes restart + check.setAttribute("data-state", "in"); +} + +// If the icon is mounted unconditionally and only shown after some +// event (e.g. await save()), the simpler form is enough: +// check.setAttribute("data-state", "in"); +// The reflow trick only matters when you replay the appear from +// an already-visible state. +``` + +### Calibrating `stroke-dasharray` for your path + +The CSS hardcodes `stroke-dasharray: 20` as a placeholder. For a clean draw, replace 20 with the actual length of **your** path (in user units), measured once with `path.getTotalLength()`. Two ways to do it: + +1. **Static (recommended)** — measure the path in the browser console once, then paste the rounded-up integer into the CSS: + + ```js + document.querySelector(".t-success-check svg path").getTotalLength() + // → 19.42 → use stroke-dasharray: 20 (round up by 1px for safety) + ``` + +2. **Dynamic** — measure on mount and set both properties inline. Use this when paths vary per-render: + + ```js + const path = wrapper.querySelector("svg path"); + const len = Math.ceil(path.getTotalLength()); + path.style.strokeDasharray = String(len); + path.style.strokeDashoffset = String(len); + ``` + +If the dasharray is too short the stroke pre-reveals before the animation starts; too long and the path appears to draw past its end before fading in. Round up by 1px to absorb sub-pixel float jitter. + diff --git a/.agents/skills/transitions-dev/11-avatar-group-hover.md b/.agents/skills/transitions-dev/11-avatar-group-hover.md new file mode 100644 index 000000000..02564672d --- /dev/null +++ b/.agents/skills/transitions-dev/11-avatar-group-hover.md @@ -0,0 +1,200 @@ +# Avatar group hover + +## When to use + +Hovering an item in a horizontal stack (avatar row, chip group, badge cluster, segmented button) should lift the hovered item, gently lift its neighbors with a power-falloff, then snap everything back with an overshoot spring on `mouseleave`. Direction-aware easing (clean ease-in on hover, bouncy ease-out on return) is what gives the group its springy, physical feel. + +Equally good for: pill stacks in a tag editor, chips in a filter bar, reaction-emoji rows, anywhere a horizontal row benefits from a "comb" interaction signal. + +## HTML usage + +```html + +
+
+
+ +
+``` + +Wire-up (vanilla JS): + On `mouseenter` of any .t-avatar, walk every sibling and + set inline: + el.style.setProperty('--shift', + (lift * Math.pow(falloff, distance)).toFixed(3) + 'px'); + el.style.setProperty('--scale-active', + i === activeIdx ? scale : 1); + Set transition-timing-function inline BEFORE the + variable writes — use --avatar-ease-in on hover-in and + --avatar-ease-out on the root's `mouseleave` (resets + --shift to 0 and --scale-active to 1). + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--avatar-lift` | `-4px` | sourced from `--p11-lift` | +| `--avatar-dur` | `320ms` | sourced from `--p11-dur` | +| `--avatar-scale` | `1.05` | sourced from `--p11-scale` | +| `--avatar-falloff` | `0.45` | sourced from `--p11-falloff` | +| `--avatar-ease-in` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p11-ease-in` | +| `--avatar-ease-out` | `cubic-bezier(0.34, 3.85, 0.64, 1)` | sourced from `--p11-ease-out` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --avatar-lift: -4px; + --avatar-dur: 320ms; + --avatar-scale: 1.05; + --avatar-falloff: 0.45; + --avatar-ease-in: cubic-bezier(0.22, 1, 0.36, 1); + --avatar-ease-out: cubic-bezier(0.34, 3.85, 0.64, 1); +} +``` + +## CSS + +```css +/* Hover-spring transition only — bring your own avatar/chip + styling (size, shape, border, stacking, background). */ +.t-avatar { + transform-origin: center; + /* translateY before scale so scale doesn't amplify the lift offset. */ + transform: + translateY(var(--shift, 0px)) + scale(var(--scale-active, 1)); + transition: transform var(--avatar-dur) var(--avatar-ease-in); + will-change: transform; +} + +@media (prefers-reduced-motion: reduce) { + .t-avatar { transition: none !important; transform: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +```js +// Distance-falloff lift with direction-aware easing. The trick +// is setting transition-timing-function inline BEFORE writing the +// CSS variables — the browser uses whatever timing-function is +// current at the moment a transitionable property changes, so this +// gives us ease-in on the way up and a bouncy spring on the return +// without two separate transition declarations. +const root = document.querySelector(".t-avatar-group"); +const avatars = Array.from(root.querySelectorAll(".t-avatar")); +const cs = getComputedStyle(document.documentElement); +const num = (name, fb) => { + const v = parseFloat(cs.getPropertyValue(name)); + return Number.isFinite(v) ? v : fb; +}; +const ease = (name, fb) => + cs.getPropertyValue(name).trim() || fb; + +function setShifts(activeIdx, phase) { + const lift = num("--avatar-lift", -4); + const falloff = num("--avatar-falloff", 0.45); + const scale = num("--avatar-scale", 1.05); + const tf = phase === "out" + ? ease("--avatar-ease-out", "cubic-bezier(0.34, 3.85, 0.64, 1)") + : ease("--avatar-ease-in", "cubic-bezier(0.22, 1, 0.36, 1)"); + + avatars.forEach((el, i) => { + el.style.transitionTimingFunction = tf; + if (activeIdx == null) { + el.style.setProperty("--shift", "0px"); + el.style.setProperty("--scale-active", "1"); + return; + } + const d = Math.abs(i - activeIdx); + el.style.setProperty( + "--shift", + (lift * Math.pow(falloff, d)).toFixed(3) + "px" + ); + el.style.setProperty( + "--scale-active", + i === activeIdx ? String(scale) : "1" + ); + }); +} + +avatars.forEach((el, i) => { + el.addEventListener("mouseenter", () => setShifts(i, "in")); +}); +root.addEventListener("mouseleave", () => setShifts(null, "out")); +``` + +### React form + +```jsx +import { useRef } from "react"; + +// `items` is any list of React nodes (avatars, chips, badges, …) +// — this hook only owns the hover-spring transition. Each item is +// wrapped in a .t-avatar so it picks up the transform/transition +// rules from CSS. +export function AvatarGroup({ items }) { + const rootRef = useRef(null); + + const setShifts = (activeIdx, phase) => { + if (!rootRef.current) return; + const cs = getComputedStyle(document.documentElement); + const num = (name, fb) => { + const v = parseFloat(cs.getPropertyValue(name)); + return Number.isFinite(v) ? v : fb; + }; + const ease = (name, fb) => + cs.getPropertyValue(name).trim() || fb; + + const lift = num("--avatar-lift", -4); + const falloff = num("--avatar-falloff", 0.45); + const scale = num("--avatar-scale", 1.05); + const tf = phase === "out" + ? ease("--avatar-ease-out", "cubic-bezier(0.34, 3.85, 0.64, 1)") + : ease("--avatar-ease-in", "cubic-bezier(0.22, 1, 0.36, 1)"); + + rootRef.current.querySelectorAll(".t-avatar").forEach((el, i) => { + el.style.transitionTimingFunction = tf; + if (activeIdx == null) { + el.style.setProperty("--shift", "0px"); + el.style.setProperty("--scale-active", "1"); + return; + } + const d = Math.abs(i - activeIdx); + el.style.setProperty( + "--shift", + (lift * Math.pow(falloff, d)).toFixed(3) + "px" + ); + el.style.setProperty( + "--scale-active", + i === activeIdx ? String(scale) : "1" + ); + }); + }; + + return ( +
setShifts(null, "out")}> + {items.map((node, i) => ( +
setShifts(i, "in")} + > + {node} +
+ ))} +
+ ); +} +``` + +### Why the timing-function is set inline before the variable writes + +Both the lift (hover-in) and the return (mouseleave) animate the same property — `transform`. If we declared one fixed `transition-timing-function` in CSS, both directions would share it. Setting it inline immediately before mutating `--shift` / `--scale-active` means each new transition picks up the timing-function that was current at the moment the property changed, giving us a clean curve on the way up and a bouncy overshoot on the way back without a second `.is-leaving` class. + diff --git a/.agents/skills/transitions-dev/12-error-state-shake.md b/.agents/skills/transitions-dev/12-error-state-shake.md new file mode 100644 index 000000000..20f7199d9 --- /dev/null +++ b/.agents/skills/transitions-dev/12-error-state-shake.md @@ -0,0 +1,202 @@ +# Error state shake + +## When to use + +Form validation feedback — invalid email, wrong password, missing required field, mismatched confirmation. The input shakes left/right with overshoot, the border switches to error color, and a message reveals beneath. After a hold timer (long enough to read the message), border + message fade back to neutral. Optional: typing into the input cancels the auto-revert immediately. + +The `t-` snippet is also a fit for any "this is wrong, try again" moment that needs a percussive hint without an OS-level alert — a wrong-PIN field on a lock screen, a duplicate-tag warning in a tag editor, a "name already taken" username field. + +## HTML usage + +```html + +
+
+ +
+

Please enter a valid email.

+
+``` + +Trigger: + - Add `.is-error` to .t-input-wrap and .t-input. Your + own border-color rules drive the visible color; this + stylesheet only owns the tween. + - Restart the shake by removing `.is-shaking` from + .t-input, forcing a reflow, then re-adding it. + - Optional: after --revert-hold ms, drop both + `.is-error` classes so border + message fade back + to neutral over --revert-dur. + +Per-segment ease: each keyframe stop carries its own +animation-timing-function so each leg follows the Figma +cubic-bezier curve independently. + +## Tunable variables + +| Variable | Default | Notes | +| --- | --- | --- | +| `--shake-distance` | `6px` | sourced from `--p12-shake-distance` | +| `--shake-overshoot` | `4px` | sourced from `--p12-shake-overshoot` | +| `--shake-dur-a` | `80ms` | sourced from `--p12-shake-dur-a` | +| `--shake-dur-b` | `60ms` | sourced from `--p12-shake-dur-b` | +| `--shake-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p12-shake-ease` | +| `--revert-hold` | `3000ms` | sourced from `--p12-revert-hold` | +| `--revert-dur` | `280ms` | sourced from `--p12-revert-dur` | + +The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block. + +```css +:root { + --shake-distance: 6px; + --shake-overshoot: 4px; + --shake-dur-a: 80ms; + --shake-dur-b: 60ms; + --shake-ease: cubic-bezier(0.22, 1, 0.36, 1); + --revert-hold: 3000ms; + --revert-dur: 280ms; +} +``` + +## CSS + +```css +/* Border-color tween. Define your input's default / focused + / error border-color in your own component CSS — this rule + only owns the interpolation. Use a constant border-width + across states so the tween never shifts inner content. */ +.t-input { + transition: border-color 150ms ease-out; + will-change: transform; +} +.t-input.is-error { + /* Error border auto-reverts on the hold timer, so the + fade-out uses the slower revert duration (matches the + message fade). */ + transition: border-color var(--revert-dur, 280ms) ease-out; +} + +/* Error message reveal. Visibility is delayed by --revert-dur + on hide so the message stays painted for the full opacity + fade-out. Entering .is-error drops the delay to 0 so the + message becomes visible immediately. */ +.t-error-msg { + opacity: 0; + visibility: hidden; + transition: + opacity var(--revert-dur, 280ms) ease-out, + visibility 0s linear var(--revert-dur, 280ms); +} +.t-input-wrap.is-error .t-error-msg { + opacity: 1; + visibility: visible; + transition: + opacity var(--revert-dur, 280ms) ease-out, + visibility 0s linear 0s; +} + +/* Multi-segment keyframe with per-stop easing so each leg + of the shake follows its own cubic-bezier independently. + %-stops are cumulative durations as a fraction of the + total (80, 60, 80, 60 = 280ms): 28.57%, 57.14%, 78.57%, + 100%. Recompute if any segment duration changes. */ +.t-input.is-shaking { + animation: t-input-shake calc( + var(--shake-dur-a) * 2 + var(--shake-dur-b) * 2 + ) linear; +} +@keyframes t-input-shake { + 0% { transform: translateX(0); animation-timing-function: var(--shake-ease); } + 28.57% { transform: translateX(var(--shake-distance)); animation-timing-function: var(--shake-ease); } + 57.14% { transform: translateX(calc(var(--shake-distance) * -1)); animation-timing-function: var(--shake-ease); } + 78.57% { transform: translateX(var(--shake-overshoot)); animation-timing-function: var(--shake-ease); } + 100% { transform: translateX(0); } +} + +@media (prefers-reduced-motion: reduce) { + .t-input { animation: none !important; transform: none !important; } +} +``` + +The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level. + +## JavaScript orchestration + +```js +// Trigger the error state, replay the shake, and schedule the +// auto-revert. Cancel any in-flight revert so the timer always +// tracks the latest call. +const wrap = document.querySelector(".t-input-wrap"); +const input = wrap.querySelector(".t-input"); + +const cs = getComputedStyle(document.documentElement); +const ms = (name, fb) => { + const v = parseFloat(cs.getPropertyValue(name)); + return Number.isFinite(v) ? v : fb; +}; + +function showError() { + wrap.classList.add("is-error"); + input.classList.add("is-error"); + + // Replay the shake from a clean baseline. + input.classList.remove("is-shaking"); + void input.offsetWidth; // force reflow + input.classList.add("is-shaking"); + + const shakeMs = + ms("--shake-dur-a", 80) * 2 + + ms("--shake-dur-b", 60) * 2; + setTimeout(() => input.classList.remove("is-shaking"), shakeMs + 20); + + // Auto-revert: hold long enough to read the message, then fade + // border + message back to neutral via the CSS transitions. + if (wrap._revertTimer) clearTimeout(wrap._revertTimer); + const hold = ms("--revert-hold", 3000); + wrap._revertTimer = setTimeout(() => { + wrap._revertTimer = null; + wrap.classList.remove("is-error"); + input.classList.remove("is-error"); + }, shakeMs + hold); +} + +// Optional but recommended: typing cancels the auto-revert and +// clears the error so the user isn't shaking at a value they're +// already correcting. +const inputEl = wrap.querySelector("input, textarea"); +inputEl?.addEventListener("input", () => { + if (wrap._revertTimer) { + clearTimeout(wrap._revertTimer); + wrap._revertTimer = null; + } + wrap.classList.remove("is-error"); + input.classList.remove("is-error"); +}); +``` + +### Recomputing the keyframe stops + +The `%`-stops in `@keyframes t-input-shake` are cumulative leg durations as a fraction of the total. The default leg pattern is **A, A, B, B** — the two big-swing legs (right peak → left peak) take `--shake-dur-a` each, the two recovery legs (left peak → overshoot → rest) take `--shake-dur-b` each: + +``` +total = 2·A + 2·B = 2·80 + 2·60 = 280ms +stop 1 (start) = 0 / 280 = 0% (rest) +stop 2 (after A) = 80 / 280 = 28.57% (peak right, +distance) +stop 3 (after 2·A) = 160 / 280 = 57.14% (peak left, -distance) +stop 4 (after 2·A+B) = 220 / 280 = 78.57% (overshoot, +overshoot) +stop 5 (end) = 280 / 280 = 100% (rest) +``` + +The total in the CSS uses `calc(var(--shake-dur-a) * 2 + var(--shake-dur-b) * 2)` — so the math stays consistent with the variables, but the **percentages** are baked literals. If you tune `--shake-dur-a` and `--shake-dur-b` to a different ratio, recompute the percentages by hand or the legs will drift out of sync with the duration calc. + +### Why three classes (`.is-error` on wrap + input, `.is-shaking` on input) + +- `.is-error` on `.t-input-wrap` controls the **message** visibility — the message lives in the wrap, not the input. +- `.is-error` on `.t-input` controls the **border color** — the input owns the border. +- `.is-shaking` on `.t-input` is **separate** from `.is-error` so you can replay the shake (remove → reflow → add) without flickering the error state on/off in the same tick. Keeping the shake state orthogonal also lets you trigger the shake on its own (e.g. for a "hint" jiggle) without the full error treatment. + diff --git a/.agents/skills/transitions-dev/SKILL.md b/.agents/skills/transitions-dev/SKILL.md new file mode 100644 index 000000000..4d2d96fc8 --- /dev/null +++ b/.agents/skills/transitions-dev/SKILL.md @@ -0,0 +1,220 @@ +--- +name: transitions-dev +description: Production-ready CSS transitions for web apps. Use when implementing notification badges, dropdowns, modals, panel reveals, page transitions, card resizes, number pop-ins, text swaps, icon swaps, success checks, avatar group hovers, or error state shakes. Triggers on "add a transition", "animate the dropdown", "make the modal open smoothly", "swap icon", "page slide", "stagger animation", "open / close transition", "make it animate", "tween the size", "fade between", "smooth open", "smooth close", "success animation", "checkmark animation", "form error", "shake on invalid", "hover lift", "avatar stack hover", "chip group hover". Also transitions reveal, transitions review, transitions apply. +--- + +# Transitions.dev + +Twelve portable CSS transitions, each namespaced under `t-*` selectors with semantic CSS custom properties. Drop-in: paste the snippet, wire the documented HTML hooks, done. No framework dependencies, no demo-specific markup, and every snippet ships a `prefers-reduced-motion` guard. + +## Quick reference + +| Transition | When to use | Reference | +| --- | --- | --- | +| **Card resize** | Tween a container's width or height when its layout state changes. | [01-card-resize.md](./01-card-resize.md) | +| **Number pop-in** | Re-enter each digit with a blurred slide when a number updates. | [02-number-pop-in.md](./02-number-pop-in.md) | +| **Notification badge** | Slide a small badge onto a trigger and pop the dot. | [03-notification-badge.md](./03-notification-badge.md) | +| **Text states swap** | Swap text in place with a blurred up-and-down transition. | [04-text-states-swap.md](./04-text-states-swap.md) | +| **Menu dropdown** | Open an origin-aware dropdown that grows from its trigger. | [05-menu-dropdown.md](./05-menu-dropdown.md) | +| **Modal open / close** | Scale-up modal dialog with a softer scale-down on close. | [06-modal.md](./06-modal.md) | +| **Panel reveal** | Slide a panel into a region with a cross-blur. | [07-panel-reveal.md](./07-panel-reveal.md) | +| **Page side-by-side** | Slide between two side-by-side pages (list ↔ detail, step 1 ↔ step 2). | [08-page-side-by-side.md](./08-page-side-by-side.md) | +| **Icon swap** | Cross-fade two icons in the same slot with blur and scale. | [09-icon-swap.md](./09-icon-swap.md) | +| **Success check** | Compose fade + rotate + Y-bob + path stroke-draw to celebrate a completed action. | [10-success-check.md](./10-success-check.md) | +| **Avatar group hover** | Distance-falloff lift on a row of items with a bouncy spring on return. | [11-avatar-group-hover.md](./11-avatar-group-hover.md) | +| **Error state shake** | Per-segment cubic-bezier shake with auto-reverting border + message. | [12-error-state-shake.md](./12-error-state-shake.md) | + +## Decision rules + +When the user asks for a transition, match against the visible UI element first, then the verb: + +- **Trigger + small dot floating on top** → notification badge. +- **Trigger + surface that grows from it** → dropdown (anchored, origin-aware) or modal (centered, no anchor). +- **Surface that slides into a region of the page** → panel reveal. +- **Two screens, list ↔ detail or step 1 ↔ step 2** → page side-by-side. +- **Element changes width or height** → card resize. +- **Element's text content changes in place** → text states swap. +- **Two icons in the same slot** → icon swap. +- **A number updates** → number pop-in. +- **Confirmation / success / "done" moment** (checkmark, payment processed, file uploaded) → success check. +- **Hovering an item in a horizontal stack** (avatars, chips, segmented buttons, tag pills) → avatar group hover. +- **Form validation error / "this is wrong" feedback** (invalid field, wrong PIN, duplicate name) → error state shake. +- **No clear match** → fall back to `transitions reveal` and let the user pick. Don't guess. + +If two transitions could fit, prefer the lower-overhead one (card resize over panel reveal, dropdown over modal, success check over a full modal celebration) unless the design clearly calls for the heavier surface. The success check is animation-only — if you also need to swap from a spinner to the check, pair it with **icon swap**. + +## Commands + +The skill exposes three namespaced verbs the agent should recognise in addition to direct transition requests. Every command starts with `transitions` so the invocation never collides with verbs from other skills installed in the same project. + +### transitions reveal — list every transition + +**Trigger phrases:** `transitions reveal`, "reveal the transitions", "list all transitions", "what transitions are in this skill", "show the transitions catalog". + +**Behaviour:** print the twelve transitions as a numbered plain-text list — name, one-line summary, and the matching reference filename. Reuse the rows in `## Quick reference` above; do not invent new copy. No project access. + +### transitions review — audit the project for fit + +**Trigger phrases:** `transitions review`, "review my project", "audit my animations", "where would transitions.dev help", "find places to use this skill". + +**Behaviour:** + +1. Search the workspace for indicators: `transition:` declarations, `@keyframes`, hardcoded `ms` / `s` durations in style files, components matching the eleven decision-rule patterns (modals, dropdowns, badges, …). +2. For each hit, match against the decision rules and pick the single best-fit transition. +3. Output a numbered list grouped by file: + - `path/to/Component.tsx:L42` — looks like a dropdown opening, suggest **menu-dropdown** (`05-menu-dropdown.md`). + - Skip ad-hoc transitions that already use a `t-*` class. +4. Do not edit anything. End with: "Run `transitions apply` on any line to install the suggested transition." + +### transitions apply — install the best-fit transition + +**Trigger phrases:** `transitions apply`, "apply a transition here", "add the right transition", "install transitions-dev here", "fix the animation on this element". + +**Behaviour:** + +1. Read context: the currently-open file, the element nearest the cursor, surrounding CSS / JSX. If the user named a transition explicitly (e.g. `transitions apply menu-dropdown`), use it. +2. Run the decision rules from `## Decision rules` on that context and pick **one** transition. If two could fit, prefer the lower-overhead one (same tie-breaker the existing rules use). +3. Surface a one-line proposal: "I'd apply **menu-dropdown** here because the element opens from a trigger and is anchored. Confirm to install?". +4. On confirmation, follow the existing five-step procedure in `## Output format` verbatim (root block, snippet, hooks, reduced-motion guard, JS orchestration if needed). +5. If the agent can't pick a single transition with confidence, fall back to `transitions reveal` and ask the user to choose. + +## Universal install + +Drop this `:root` block into your project **once**. Every transition snippet reads from these semantic names — there are no per-component values to chase down later. + +```css +/* transitions-dev — copy this :root block into your project once. + Every transition snippet reads from these semantic names. */ +:root { + /* Card resize */ + --resize-dur: 300ms; + --resize-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Number pop-in */ + --digit-dur: 500ms; + --digit-distance: 8px; + --digit-stagger: 70ms; + --digit-blur: 2px; + --digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1); + --digit-dir-x: 0; + --digit-dir-y: 1; + /* Notification badge */ + --badge-slide-dur: 260ms; + --badge-pop-dur: 500ms; + --badge-pop-close-dur: 180ms; + --badge-fade-dur: 400ms; + --badge-fade-close-dur: 180ms; + --badge-blur: 2px; + --badge-offset-x: -8.2px; + --badge-offset-y: 12.4px; + --badge-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); + --badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1); + --badge-close-ease: cubic-bezier(0.4, 0, 0.2, 1); + /* Text states swap */ + --text-swap-dur: 150ms; + --text-swap-translate-y: 4px; + --text-swap-blur: 2px; + --text-swap-ease: ease-in-out; + /* Menu dropdown */ + --dropdown-open-dur: 250ms; + --dropdown-close-dur: 150ms; + --dropdown-pre-scale: 0.97; + --dropdown-closing-scale: 0.99; + --dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Modal open / close */ + --modal-open-dur: 250ms; + --modal-close-dur: 150ms; + --modal-scale: 0.96; + --modal-scale-close: 0.96; + --modal-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Panel reveal */ + --panel-open-dur: 400ms; + --panel-close-dur: 350ms; + --panel-translate-y: 100px; + --panel-blur: 2px; + --panel-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Page side-by-side */ + --page-slide-dur: 200ms; + --page-fade-dur: 200ms; + --page-slide-distance: 8px; + --page-blur: 3px; + --page-stagger: 0ms; + --page-exit-enabled: 1; + --page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); + --page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Icon swap */ + --icon-swap-dur: 200ms; + --icon-swap-blur: 2px; + --icon-swap-start-scale: 0.25; + --icon-swap-ease: ease-in-out; + /* Success check */ + --check-opacity-dur: 550ms; + --check-rotate-dur: 550ms; + --check-rotate-from: 80deg; + --check-bob-dur: 450ms; + --check-y-amount: 40px; + --check-blur-dur: 500ms; + --check-blur-from: 10px; + --check-path-dur: 550ms; + --check-path-delay: 80ms; + --check-ease-out: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-opacity: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-rotate: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1); + --check-ease-path: cubic-bezier(0.22, 1, 0.36, 1); + /* Avatar group hover */ + --avatar-lift: -4px; + --avatar-dur: 320ms; + --avatar-scale: 1.05; + --avatar-falloff: 0.45; + --avatar-ease-in: cubic-bezier(0.22, 1, 0.36, 1); + --avatar-ease-out: cubic-bezier(0.34, 3.85, 0.64, 1); + /* Error state shake */ + --shake-distance: 6px; + --shake-overshoot: 4px; + --shake-dur-a: 80ms; + --shake-dur-b: 60ms; + --shake-ease: cubic-bezier(0.22, 1, 0.36, 1); + --revert-hold: 3000ms; + --revert-dur: 280ms; +} +``` + +The `--pX-*` source tokens used by the live demo at [transitions.dev](https://transitions.dev) are intentionally **not** exported here. Tunable values are renamed to semantic names (`--badge-*`, `--dropdown-*`, `--modal-*`, …) so the user owns the design vocabulary. + +## Output format + +When inserting a transition into the user's project: + +1. **Add the `:root` block above** to the user's global stylesheet, but only if it isn't already there. If the user already imported the universal install block once, do **not** duplicate it. +2. **Paste the chosen transition's CSS verbatim** from the relevant reference file. Do not rewrite selectors, do not collapse the transition into shorthand, do not strip `will-change`. The snippets are tuned and tested. +3. **Wire the documented HTML hooks** — class names (`.t-dropdown`, `.t-modal`, `.t-success-check`, `.t-avatar`, `.t-input`, …) and state attributes (`data-open`, `data-state`, `data-page`, `.is-open`, `.is-closing`, `.is-exit`, `.is-enter-start`, `.is-animating`, `.is-error`, `.is-shaking`). +4. **Preserve the `@media (prefers-reduced-motion: reduce)` block.** Every snippet ships one. Removing it makes the component fail accessibility audits. +5. **For transitions that need JS** (dropdown, modal, text swap, number pop-in, page slide, success check, avatar group hover, error state shake), copy the small orchestration snippet from the reference file and adapt the selectors to the user's DOM. Keep the timing reads (`getComputedStyle(...)getPropertyValue("--…")`) so durations stay in sync with the `:root` values. + +Keep the diff small: only edit the files needed to introduce the transition. Don't rename the user's existing variables, don't reformat unrelated CSS, don't pull in a motion library. + +## Common mistakes to avoid + +- **Stripping the close-state class cleanup** on dropdown/modal — without the `setTimeout` that removes `.is-closing`, the next open jumps from the closing scale instead of the resting pre-open scale. +- **Forgetting the reflow** in the text swap, number pop-in, success check replay, and error state shake — `void el.offsetWidth` (or `offsetHeight`) between class/attribute removal and re-addition is what guarantees the animation replays. +- **Animating a single container** instead of the inner pieces — for the badge, animate the dot, not the trigger; for page slide, animate the page sections, not the container. +- **Replacing `transition: …` with `transition: all`** — every snippet enumerates exact properties on purpose so unrelated style changes don't ride in for free. +- **Hardcoding the success check's `stroke-dasharray`** — the snippet ships `20` as a placeholder. Replace it with `path.getTotalLength()` rounded up by 1 for *your* path, otherwise the stroke pre-reveals or over-draws. +- **Setting `transition-timing-function` in CSS** for the avatar group hover — it has to be set inline in JS *before* the `--shift` / `--scale-active` writes so the bouncy ease-out only applies on `mouseleave`. +- **Mixing `.is-error` and `.is-shaking` into one class** for the error state shake — keeping them orthogonal is what allows the shake to replay (remove → reflow → re-add) without flickering the whole error treatment. + +## Reference files + +- [01-card-resize.md](./01-card-resize.md) — Card resize +- [02-number-pop-in.md](./02-number-pop-in.md) — Number pop-in +- [03-notification-badge.md](./03-notification-badge.md) — Notification badge +- [04-text-states-swap.md](./04-text-states-swap.md) — Text states swap +- [05-menu-dropdown.md](./05-menu-dropdown.md) — Menu dropdown +- [06-modal.md](./06-modal.md) — Modal open / close +- [07-panel-reveal.md](./07-panel-reveal.md) — Panel reveal +- [08-page-side-by-side.md](./08-page-side-by-side.md) — Page side-by-side +- [09-icon-swap.md](./09-icon-swap.md) — Icon swap +- [10-success-check.md](./10-success-check.md) — Success check +- [11-avatar-group-hover.md](./11-avatar-group-hover.md) — Avatar group hover +- [12-error-state-shake.md](./12-error-state-shake.md) — Error state shake +- [_root.css](./_root.css) — the universal install block on its own, ready to import directly. diff --git a/.agents/skills/transitions-dev/_root.css b/.agents/skills/transitions-dev/_root.css new file mode 100644 index 000000000..486bd50a5 --- /dev/null +++ b/.agents/skills/transitions-dev/_root.css @@ -0,0 +1,94 @@ +/* transitions-dev — copy this :root block into your project once. + Every transition snippet reads from these semantic names. */ +:root { + /* Card resize */ + --resize-dur: 300ms; + --resize-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Number pop-in */ + --digit-dur: 500ms; + --digit-distance: 8px; + --digit-stagger: 70ms; + --digit-blur: 2px; + --digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1); + --digit-dir-x: 0; + --digit-dir-y: 1; + /* Notification badge */ + --badge-slide-dur: 260ms; + --badge-pop-dur: 500ms; + --badge-pop-close-dur: 180ms; + --badge-fade-dur: 400ms; + --badge-fade-close-dur: 180ms; + --badge-blur: 2px; + --badge-offset-x: -8.2px; + --badge-offset-y: 12.4px; + --badge-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); + --badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1); + --badge-close-ease: cubic-bezier(0.4, 0, 0.2, 1); + /* Text states swap */ + --text-swap-dur: 150ms; + --text-swap-translate-y: 4px; + --text-swap-blur: 2px; + --text-swap-ease: ease-in-out; + /* Menu dropdown */ + --dropdown-open-dur: 250ms; + --dropdown-close-dur: 150ms; + --dropdown-pre-scale: 0.97; + --dropdown-closing-scale: 0.99; + --dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Modal open / close */ + --modal-open-dur: 250ms; + --modal-close-dur: 150ms; + --modal-scale: 0.96; + --modal-scale-close: 0.96; + --modal-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Panel reveal */ + --panel-open-dur: 400ms; + --panel-close-dur: 350ms; + --panel-translate-y: 100px; + --panel-blur: 2px; + --panel-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Page side-by-side */ + --page-slide-dur: 200ms; + --page-fade-dur: 200ms; + --page-slide-distance: 8px; + --page-blur: 3px; + --page-stagger: 0ms; + --page-exit-enabled: 1; + --page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1); + --page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1); + /* Icon swap */ + --icon-swap-dur: 200ms; + --icon-swap-blur: 2px; + --icon-swap-start-scale: 0.25; + --icon-swap-ease: ease-in-out; + /* Success check */ + --check-opacity-dur: 550ms; + --check-rotate-dur: 550ms; + --check-rotate-from: 80deg; + --check-bob-dur: 450ms; + --check-y-amount: 40px; + --check-blur-dur: 500ms; + --check-blur-from: 10px; + --check-path-dur: 550ms; + --check-path-delay: 80ms; + --check-ease-out: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-opacity: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-rotate: cubic-bezier(0.22, 1, 0.36, 1); + --check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1); + --check-ease-path: cubic-bezier(0.22, 1, 0.36, 1); + /* Avatar group hover */ + --avatar-lift: -4px; + --avatar-dur: 320ms; + --avatar-scale: 1.05; + --avatar-falloff: 0.45; + --avatar-ease-in: cubic-bezier(0.22, 1, 0.36, 1); + --avatar-ease-out: cubic-bezier(0.34, 3.85, 0.64, 1); + /* Error state shake */ + --shake-distance: 6px; + --shake-overshoot: 4px; + --shake-dur-a: 80ms; + --shake-dur-b: 60ms; + --shake-ease: cubic-bezier(0.22, 1, 0.36, 1); + --revert-hold: 3000ms; + --revert-dur: 280ms; +} diff --git a/skills-lock.json b/skills-lock.json index 62af2bd10..55f994918 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -12,6 +12,12 @@ "sourceType": "github", "skillPath": "skills/git-commit/SKILL.md", "computedHash": "2607fc60629b82b257136dd2a7a373f0a4466c0b49df7746d845d59313c99b21" + }, + "transitions-dev": { + "source": "Jakubantalik/transitions.dev", + "sourceType": "github", + "skillPath": "skills/transitions-dev/SKILL.md", + "computedHash": "2bd1d5142edb75c988faf5562e01c659623f713a11c3a5f94f5744b9801ccd2a" } } }