From 12ff78a693556e8a9bbe42c06f592ffb3370c10d Mon Sep 17 00:00:00 2001 From: bilalbayram Date: Fri, 13 Mar 2026 04:57:50 +0300 Subject: [PATCH 1/4] fix(styles): polish micro-interactions and add motion design tokens Add centralized motion tokens (durations, easings, spinner speed) to ds-tokens.css and apply them across 14 CSS files: button :active press feedback, modal/popover entrance animations, settings toggle spring animation, segmented control transitions, input focus transitions, consistent 0.7s spinner speed, pulse easing fix, transition:all cleanup, and a global prefers-reduced-motion rule. --- src/styles/base.css | 11 +++++++++++ src/styles/buttons.css | 20 +++++++++++++++++++- src/styles/compact-tablet.css | 2 +- src/styles/composer.css | 2 +- src/styles/diff.css | 6 +++--- src/styles/ds-modal.css | 20 ++++++++++++++++++++ src/styles/ds-popover.css | 14 ++++++++++++++ src/styles/ds-tokens.css | 14 ++++++++++++++ src/styles/home.css | 2 +- src/styles/main.css | 2 +- src/styles/messages.css | 2 +- src/styles/settings.css | 18 +++++++++++++++--- src/styles/sidebar.css | 20 ++++++++++---------- src/styles/tabbar.css | 2 +- 14 files changed, 112 insertions(+), 23 deletions(-) diff --git a/src/styles/base.css b/src/styles/base.css index e3d73c340..5e3719c02 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -229,3 +229,14 @@ button.window-caption-control:focus-visible, display: none; } } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/src/styles/buttons.css b/src/styles/buttons.css index ad99bc6f5..24bc4cec7 100644 --- a/src/styles/buttons.css +++ b/src/styles/buttons.css @@ -10,7 +10,7 @@ button { font-size: 13px; font-weight: 600; cursor: pointer; - transition: transform 0.15s ease, box-shadow 0.15s ease; + transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease, filter 0.15s ease; -webkit-app-region: no-drag; } @@ -52,3 +52,21 @@ button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 12px 18px rgba(0, 0, 0, 0.2); } + +button:active:not(:disabled) { + transform: scale(var(--ds-active-scale)); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + transition-duration: 50ms; +} + +.icon-button:active:not(:disabled) { + transform: scale(var(--ds-active-scale-sm)); +} + +.primary:active:not(:disabled) { + filter: brightness(0.92); +} + +.ghost:active:not(:disabled) { + background: rgba(255, 255, 255, 0.06); +} diff --git a/src/styles/compact-tablet.css b/src/styles/compact-tablet.css index 1d9b95d01..6bff16f3e 100644 --- a/src/styles/compact-tablet.css +++ b/src/styles/compact-tablet.css @@ -38,7 +38,7 @@ flex-direction: column; align-items: center; gap: 6px; - transition: all 0.15s ease; + transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; } .tablet-nav-item:hover { diff --git a/src/styles/composer.css b/src/styles/composer.css index e076d920a..338a2a3b4 100644 --- a/src/styles/composer.css +++ b/src/styles/composer.css @@ -486,7 +486,7 @@ border-radius: 999px; border: 2px solid rgba(255, 120, 120, 0.35); border-top-color: rgba(255, 120, 120, 0.9); - animation: composer-action-spin 0.8s linear infinite; + animation: composer-action-spin var(--ds-spinner-dur) linear infinite; } diff --git a/src/styles/diff.css b/src/styles/diff.css index 19f8bee3a..a9627b1d2 100644 --- a/src/styles/diff.css +++ b/src/styles/diff.css @@ -997,7 +997,7 @@ border-radius: 50%; border: 2px solid var(--border-subtle); border-top-color: var(--text-strong); - animation: spin 0.9s linear infinite; + animation: spin var(--ds-spinner-dur) linear infinite; } @keyframes spin { @@ -1102,7 +1102,7 @@ .commit-message-loader { width: 14px; height: 14px; - animation: spin 1s linear infinite; + animation: spin var(--ds-spinner-dur) linear infinite; } .commit-message-error { @@ -1150,7 +1150,7 @@ border-radius: 50%; border: 2px solid var(--border-subtle); border-top-color: var(--text-emphasis); - animation: spin 0.9s linear infinite; + animation: spin var(--ds-spinner-dur) linear infinite; } /* Push section */ diff --git a/src/styles/ds-modal.css b/src/styles/ds-modal.css index 98ef07dfd..11e230516 100644 --- a/src/styles/ds-modal.css +++ b/src/styles/ds-modal.css @@ -10,6 +10,7 @@ background: var(--ds-modal-backdrop); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + animation: ds-modal-backdrop-in var(--ds-dur-entrance) var(--ds-ease-out) both; } .app.reduced-transparency .ds-modal-backdrop { @@ -30,6 +31,7 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); + animation: ds-modal-card-in var(--ds-dur-slow) var(--ds-ease-out) both; } .ds-modal :where(input, textarea, select):focus-visible { @@ -62,6 +64,8 @@ padding: 10px 12px; font-size: 13px; width: 100%; + transition: border-color var(--ds-dur-normal) var(--ds-ease-out-soft), + box-shadow var(--ds-dur-normal) var(--ds-ease-out-soft); } .ds-modal-textarea { @@ -97,3 +101,19 @@ padding: 6px 12px; border-radius: 10px; } + +@keyframes ds-modal-backdrop-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes ds-modal-card-in { + from { + opacity: 0; + transform: translate(-50%, -48%) scale(0.98); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} diff --git a/src/styles/ds-popover.css b/src/styles/ds-popover.css index a16d888b8..a8cbf0094 100644 --- a/src/styles/ds-popover.css +++ b/src/styles/ds-popover.css @@ -3,6 +3,7 @@ border: 1px solid var(--ds-popover-border, var(--border-muted)); box-shadow: var(--ds-popover-shadow, 0 14px 34px rgba(0, 0, 0, 0.3)); border-radius: 10px; + animation: ds-popover-in var(--ds-dur-fast) var(--ds-ease-out) both; } .ds-popover-item { @@ -19,6 +20,8 @@ font-size: 12px; text-align: left; cursor: pointer; + transition: background-color var(--ds-dur-fast) var(--ds-ease-out-soft), + color var(--ds-dur-fast) var(--ds-ease-out-soft); } .ds-popover-item:disabled { @@ -55,3 +58,14 @@ min-width: 0; flex: 1; } + +@keyframes ds-popover-in { + from { + opacity: 0; + transform: translateY(-4px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} diff --git a/src/styles/ds-tokens.css b/src/styles/ds-tokens.css index b7965aa6f..bb982b6fa 100644 --- a/src/styles/ds-tokens.css +++ b/src/styles/ds-tokens.css @@ -41,4 +41,18 @@ /* Global layer scale (keep numeric so it works with z-index + calc()). */ --ds-layer-modal: 10000; --ds-layer-toast: 11000; + + /* ── Motion ── */ + --ds-dur-fast: 120ms; + --ds-dur-normal: 160ms; + --ds-dur-slow: 220ms; + --ds-dur-entrance: 200ms; + + --ds-ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ds-ease-out-soft: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --ds-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + + --ds-spinner-dur: 0.7s; + --ds-active-scale: 0.97; + --ds-active-scale-sm: 0.92; } diff --git a/src/styles/home.css b/src/styles/home.css index 531752887..c26768fe1 100644 --- a/src/styles/home.css +++ b/src/styles/home.css @@ -213,7 +213,7 @@ } .home-usage-refresh-icon.spinning { - animation: home-spin 0.8s linear infinite; + animation: home-spin var(--ds-spinner-dur) linear infinite; } .home-usage-grid { diff --git a/src/styles/main.css b/src/styles/main.css index e2695faf1..e859e59d8 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -883,7 +883,7 @@ } .compact-codex-refresh-icon.spinning { - animation: compact-codex-refresh-spin 0.8s linear infinite; + animation: compact-codex-refresh-spin var(--ds-spinner-dur) linear infinite; } @keyframes compact-codex-refresh-spin { diff --git a/src/styles/messages.css b/src/styles/messages.css index 29f5614bd..2149f5581 100644 --- a/src/styles/messages.css +++ b/src/styles/messages.css @@ -58,7 +58,7 @@ border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.2); border-top-color: var(--text-stronger); - animation: working-spin 1s linear infinite; + animation: working-spin var(--ds-spinner-dur) linear infinite; } .working-timer { diff --git a/src/styles/settings.css b/src/styles/settings.css index cefef11b4..2198d24cd 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -451,7 +451,7 @@ .settings-agents-generate-loader { width: 14px; height: 14px; - animation: settings-spin 1s linear infinite; + animation: settings-spin var(--ds-spinner-dur) linear infinite; } .settings-agents-textarea { @@ -641,6 +641,8 @@ background: var(--surface-control); color: var(--text-strong); font-size: 12px; + transition: border-color var(--ds-dur-normal) var(--ds-ease-out-soft), + box-shadow var(--ds-dur-normal) var(--ds-ease-out-soft); } .settings-input--compact { @@ -685,6 +687,8 @@ background: var(--surface-control); color: var(--text-strong); font-size: 12px; + transition: border-color var(--ds-dur-normal) var(--ds-ease-out-soft), + box-shadow var(--ds-dur-normal) var(--ds-ease-out-soft); } .settings-select option { @@ -720,6 +724,9 @@ padding: 0; min-width: 72px; overflow: hidden; + transition: color var(--ds-dur-fast) var(--ds-ease-out-soft), + background-color var(--ds-dur-fast) var(--ds-ease-out-soft), + box-shadow var(--ds-dur-fast) var(--ds-ease-out-soft); } .settings-segmented-input { @@ -1198,13 +1205,13 @@ display: inline-flex; align-items: center; justify-content: flex-start; - transition: all 0.2s ease; + transition: background var(--ds-dur-normal) var(--ds-ease-out-soft), + border-color var(--ds-dur-normal) var(--ds-ease-out-soft); } .settings-toggle.on { background: linear-gradient(135deg, rgba(100, 200, 255, 0.6), rgba(120, 235, 190, 0.6)); border-color: var(--border-accent); - justify-content: flex-end; } .app.reduced-transparency .settings-toggle:not(.on) { @@ -1218,6 +1225,11 @@ border-radius: 999px; background: var(--surface-card-strong); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); + transition: transform var(--ds-dur-normal) var(--ds-ease-spring); +} + +.settings-toggle.on .settings-toggle-knob { + transform: translateX(20px); } @media (max-width: 720px) { diff --git a/src/styles/sidebar.css b/src/styles/sidebar.css index 007b971e0..c9df0b492 100644 --- a/src/styles/sidebar.css +++ b/src/styles/sidebar.css @@ -291,7 +291,7 @@ } .sidebar-refresh-icon.spinning { - animation: sidebar-refresh-spin 0.8s linear infinite; + animation: sidebar-refresh-spin var(--ds-spinner-dur) linear infinite; } .sidebar-refresh-toggle:hover, @@ -705,7 +705,7 @@ color: inherit; text-align: left; cursor: pointer; - transition: all 0.2s ease; + transition: background-color 0.2s ease; -webkit-app-region: no-drag; overflow-x: hidden; position: relative; @@ -980,13 +980,13 @@ .thread-status.processing { background: #ff9f43; box-shadow: 0 0 8px rgba(255, 159, 67, 0.8); - animation: pulse 1.2s ease-in-out infinite; + animation: pulse 1.2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } .thread-status.reviewing { background: #2fd1c4; box-shadow: 0 0 8px rgba(47, 209, 196, 0.75); - animation: pulse 1.4s ease-in-out infinite; + animation: pulse 1.4s cubic-bezier(0.4, 0, 0.6, 1) infinite; } .thread-status.unread { @@ -1253,7 +1253,7 @@ color: inherit; text-align: left; cursor: pointer; - transition: all 0.2s ease; + transition: background-color 0.2s ease; -webkit-app-region: no-drag; overflow-x: hidden; } @@ -1308,7 +1308,7 @@ border-radius: 50%; border: 2px solid var(--border-subtle); border-top-color: var(--text-strong); - animation: spin 0.9s linear infinite; + animation: spin var(--ds-spinner-dur) linear infinite; } .worktree-deleting-label { @@ -1483,7 +1483,7 @@ border-radius: 999px; border: 2px solid rgba(11, 15, 26, 0.22); border-top-color: rgba(11, 15, 26, 0.8); - animation: sidebar-account-spin 0.8s linear infinite; + animation: sidebar-account-spin var(--ds-spinner-dur) linear infinite; } @keyframes sidebar-account-spin { @@ -1525,15 +1525,15 @@ @keyframes pulse { 0% { - transform: scale(0.9); + transform: scale(0.92); opacity: 0.6; } 50% { - transform: scale(1.2); + transform: scale(1.1); opacity: 1; } 100% { - transform: scale(0.9); + transform: scale(0.92); opacity: 0.6; } } diff --git a/src/styles/tabbar.css b/src/styles/tabbar.css index 472d13dbd..2a41a5039 100644 --- a/src/styles/tabbar.css +++ b/src/styles/tabbar.css @@ -47,7 +47,7 @@ html[data-mobile-composer-focus="true"] .app.layout-phone .tabbar { justify-content: center; gap: 4px; cursor: pointer; - transition: all 0.15s ease; + transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; } .tabbar-item:hover { From 1282b03f784c19a1ff41f2d4b30bdf96b1c65893 Mon Sep 17 00:00:00 2001 From: bilalbayram Date: Fri, 13 Mar 2026 16:40:31 +0300 Subject: [PATCH 2/4] fix(styles): use individual transform properties in popover animation The ds-popover-in keyframe was using the `transform` shorthand which overrides inline style transforms used for boundary-shift correction, causing popovers (e.g. sidebar sort menu) to clip off-screen. Switch to individual `translate` and `scale` properties which compose with `transform` instead of overriding it. --- src/styles/ds-popover.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/styles/ds-popover.css b/src/styles/ds-popover.css index a8cbf0094..0182ed0d9 100644 --- a/src/styles/ds-popover.css +++ b/src/styles/ds-popover.css @@ -62,10 +62,12 @@ @keyframes ds-popover-in { from { opacity: 0; - transform: translateY(-4px) scale(0.98); + translate: 0 -4px; + scale: 0.98; } to { opacity: 1; - transform: translateY(0) scale(1); + translate: 0 0; + scale: 1; } } From 409fa45a597300ac735cee438d166bfc18f74f04 Mon Sep 17 00:00:00 2001 From: bilalbayram Date: Fri, 13 Mar 2026 16:51:13 +0300 Subject: [PATCH 3/4] fix(a11y): preserve functional spinners under prefers-reduced-motion The blanket reduced-motion rule was killing spinner animations too, making loading states look frozen. Whitelist all spinner selectors so they keep rotating while decorative animations (entrances, pulses, shimmers, transitions) remain suppressed. --- src/styles/base.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/styles/base.css b/src/styles/base.css index 5e3719c02..893406938 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -239,4 +239,20 @@ button.window-caption-control:focus-visible, transition-duration: 0.01ms !important; scroll-behavior: auto !important; } + + /* Restore functional spinners — these communicate loading state */ + .working-spinner, + .git-panel-spinner, + .commit-message-loader, + .commit-button-spinner, + .sidebar-refresh-icon.spinning, + .worktree-deleting-spinner, + .sidebar-account-spinner, + .settings-agents-generate-loader, + .composer-action-spinner, + .compact-codex-refresh-icon.spinning, + .home-usage-refresh-icon.spinning { + animation-duration: var(--ds-spinner-dur) !important; + animation-iteration-count: infinite !important; + } } From 23feae339eb4a89d9e6a28729af5a4acb13ecfc2 Mon Sep 17 00:00:00 2001 From: bilalbayram Date: Fri, 13 Mar 2026 17:17:07 +0300 Subject: [PATCH 4/4] fix(styles): add sliding pill indicator to segmented control Replace per-option background swap with a ::before pseudo-element that physically slides between segments via translateX, giving spatial movement instead of a flat fade. --- .../sections/SettingsComposerSection.tsx | 2 +- src/styles/settings.css | 27 +++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/features/settings/components/sections/SettingsComposerSection.tsx b/src/features/settings/components/sections/SettingsComposerSection.tsx index ae3014bbc..1b5bef63f 100644 --- a/src/features/settings/components/sections/SettingsComposerSection.tsx +++ b/src/features/settings/components/sections/SettingsComposerSection.tsx @@ -32,7 +32,7 @@ export function SettingsComposerSection({ >
Follow-up behavior
-
+