From 354a229727d980c7808cc5034916ce8958731679 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Wed, 29 Jul 2026 05:47:27 -0700 Subject: [PATCH 1/2] feat(web): redesign desktop UI and interactions --- .../components/AboutDialog/AboutDialog.scss | 32 +- .../GalleryLayout/GalleryLayout.scss | 64 +-- .../src/app/components/NavBar/NavBar.scss | 78 ++-- .../src/app/components/NavBar/NavBar.tsx | 4 +- .../src/app/components/NavPanel/NavPanel.scss | 403 +++++++++++++++++ .../components/NavPanel/NavSearchDialog.scss | 312 +++++-------- .../components/NavPanel/NavSearchDialog.tsx | 8 +- .../components/BranchQuickSwitch.scss | 56 ++- .../sections/sessions/SessionsSection.scss | 113 +++++ .../workspaces/WorkspaceListSection.scss | 156 +++++++ .../NewProjectDialog/NewProjectDialog.scss | 20 +- .../RemoteConnectDialog.scss | 12 +- .../RemoteConnectDisclaimer.scss | 6 +- .../src/app/components/SceneBar/SceneBar.scss | 260 +++++------ .../src/app/components/SceneBar/SceneBar.tsx | 2 +- .../TitleBar/NotificationButton.scss | 12 +- .../TitleBar/NotificationButton.tsx | 1 + .../components/panels/BranchSelectModal.scss | 12 +- .../panels/DiffFullscreenViewer.css | 6 +- .../src/app/components/panels/FilesPanel.scss | 23 +- .../components/panels/TerminalEditModal.scss | 13 +- .../components/panels/base/FlexiblePanel.scss | 12 +- .../content-canvas/editor-area/DropZone.scss | 19 +- .../mission-control/MissionControl.scss | 18 +- .../mission-control/ThumbnailCard.scss | 12 +- .../tab-bar/TabOverflowMenu.scss | 12 +- src/web-ui/src/app/layout/AppLayout.scss | 101 ++--- .../src/app/layout/FloatingMiniChat.scss | 113 +++-- .../src/app/layout/FloatingMiniChat.tsx | 2 +- src/web-ui/src/app/layout/WorkspaceBody.scss | 121 ++--- src/web-ui/src/app/layout/WorkspaceBody.tsx | 4 +- .../src/app/scenes/agents/AgentsView.scss | 17 +- .../app/scenes/profile/views/NurseryView.scss | 12 +- .../src/app/scenes/settings/SettingsNav.scss | 10 +- .../app/scenes/settings/SettingsScene.scss | 56 +-- .../scenes/settings/SettingsScene.test.tsx | 15 +- .../src/app/scenes/settings/SettingsScene.tsx | 71 +-- .../src/app/scenes/welcome/WelcomeScene.scss | 311 +++++++------ .../components/Alert/Alert.scss | 30 +- .../components/Alert/Alert.tsx | 3 +- .../components/Badge/Badge.scss | 4 +- .../components/Button/Button.scss | 128 +++--- .../components/Button/Button.tsx | 5 +- .../components/Card/Card.scss | 70 ++- .../components/Checkbox/Checkbox.scss | 63 ++- .../components/Checkbox/Checkbox.tsx | 4 +- .../ConfirmDialog/ConfirmDialog.scss | 14 +- .../ConfirmDialog/ConfirmDialog.tsx | 9 +- .../components/Empty/Empty.scss | 31 +- .../components/Empty/Empty.tsx | 1 + .../components/IconButton/IconButton.scss | 83 ++-- .../components/IconButton/IconButton.tsx | 1 + .../components/Input/Input.scss | 68 ++- .../components/Input/Input.tsx | 16 +- .../components/Modal/Modal.scss | 90 ++-- .../components/Search/Search.scss | 78 +++- .../components/Search/Search.tsx | 9 +- .../components/Select/Select.scss | 189 +++++--- .../components/Select/Select.tsx | 40 +- .../components/Switch/Switch.scss | 87 ++-- .../components/Switch/Switch.tsx | 7 +- .../components/Tabs/Tabs.scss | 115 +++-- .../components/Tabs/Tabs.tsx | 78 +++- .../component-library/components/Tag/Tag.scss | 75 +++- .../component-library/components/Tag/Tag.tsx | 9 +- .../components/Textarea/Textarea.scss | 41 +- .../components/Textarea/Textarea.tsx | 20 +- .../components/Tooltip/Tooltip.scss | 79 +--- .../components/Tooltip/Tooltip.tsx | 21 +- .../src/flow_chat/components/ChatInput.scss | 223 +++++++--- .../src/flow_chat/components/ChatInput.tsx | 53 ++- .../components/CoworkExampleCards.scss | 26 +- .../components/CoworkExampleCards.tsx | 10 +- .../flow_chat/components/FlowTextBlock.scss | 21 +- .../flow_chat/components/ModelSelector.scss | 89 +++- .../flow_chat/components/ModelSelector.tsx | 140 +++++- .../components/PendingQueuePanel.scss | 72 ++- .../flow_chat/components/RichTextInput.scss | 39 +- .../components/ScrollToBottomButton.scss | 40 +- .../components/ScrollToBottomButton.tsx | 2 +- .../components/ScrollToLatestBar.scss | 31 +- .../components/ScrollToLatestBar.tsx | 4 +- .../components/ScrollToTurnHeaderButton.scss | 34 +- .../components/ScrollToTurnHeaderButton.tsx | 1 + .../components/StickyTaskIndicator.scss | 29 +- .../components/StickyTaskIndicator.tsx | 1 + .../flow_chat/components/ToolApprovalBar.scss | 36 +- .../src/flow_chat/components/WelcomePanel.css | 75 +++- .../src/flow_chat/components/WelcomePanel.tsx | 24 +- .../components/modern/ModelRoundItem.scss | 73 ++- .../modern/PermissionRequestPanel.scss | 74 +++- .../modern/ProcessingIndicator.scss | 33 +- .../components/modern/UserMessageItem.scss | 121 ++++- .../config/components/AIModelConfig.scss | 70 ++- .../config/components/AppearanceConfig.scss | 38 +- .../config/components/ConfigForm.scss | 10 +- .../config/components/DebugConfig.scss | 6 +- .../config/components/DefaultModelConfig.scss | 14 +- .../config/components/MCPResourceBrowser.scss | 11 +- .../components/ModelSelectionRadio.scss | 10 +- .../components/common/ConfigPageHeader.scss | 25 +- .../components/common/ConfigPageLayout.scss | 26 +- .../common/config-page-layout.tokens.scss | 4 +- .../tools/editor/components/CodeEditor.scss | 6 +- .../tools/editor/components/DiffEditor.scss | 15 +- .../editor/components/EditorStatusBar.scss | 5 +- .../editor/components/MarkdownEditor.scss | 9 +- .../tools/editor/components/PlanViewer.scss | 35 +- .../components/FileSearchResults.scss | 21 +- .../file-system/styles/FileExplorer.scss | 22 +- .../CreateBranchDialog.scss | 11 +- .../GitBranchHistoryView.scss | 17 +- .../GitDiffEditor/GitDiffEditor.scss | 19 +- .../components/GitDiffView/GitDiffView.scss | 22 +- .../components/GitGraphView/GitGraphView.scss | 6 +- .../GitSettingsView/GitSettingsView.scss | 18 +- .../git/components/PushButton/PushButton.scss | 8 +- .../LspPluginList/LspPluginList.scss | 5 +- .../ReferencesPanel/ReferencesPanel.scss | 17 +- .../tools/terminal/components/Terminal.scss | 12 +- .../workspace/components/WorkspaceManager.css | 417 ++++++++---------- .../workspace/components/WorkspaceManager.tsx | 21 +- 122 files changed, 4055 insertions(+), 2063 deletions(-) diff --git a/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss b/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss index 6a4c6bf504..7dccae44bc 100644 --- a/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss +++ b/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss @@ -374,7 +374,12 @@ cursor: pointer; padding: 4px; border-radius: 4px; - transition: all 0.2s ease; + transition: + transform 150ms cubic-bezier(0.23, 1, 0.32, 1), + background-color 150ms ease, + border-color 150ms ease, + color 150ms ease, + box-shadow 150ms ease; display: flex; align-items: center; @@ -409,7 +414,12 @@ font-size: 12px; font-weight: 500; cursor: pointer; - transition: all 0.25s ease; + transition: + transform 180ms cubic-bezier(0.23, 1, 0.32, 1), + background-color 180ms ease, + border-color 180ms ease, + color 180ms ease, + box-shadow 180ms ease; &:hover { background: var(--element-bg-subtle); @@ -430,7 +440,11 @@ svg { color: var(--color-text-muted); - transition: all 0.2s ease; + transition: + transform 150ms cubic-bezier(0.23, 1, 0.32, 1), + background-color 150ms ease, + border-color 150ms ease, + color 150ms ease; } } @@ -482,7 +496,11 @@ background: transparent; border: 1px solid transparent; border-radius: 6px; - transition: all 0.2s ease; + transition: + transform 150ms cubic-bezier(0.23, 1, 0.32, 1), + background-color 150ms ease, + border-color 150ms ease, + color 150ms ease; min-width: 0; &:hover { @@ -508,7 +526,11 @@ justify-content: center; background: var(--element-bg-subtle); border-radius: 6px; - transition: all 0.2s ease; + transition: + transform 150ms cubic-bezier(0.23, 1, 0.32, 1), + background-color 150ms ease, + border-color 150ms ease, + color 150ms ease; svg { width: 12px; diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss index 4b4d3f7e94..39ab5959dd 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss @@ -1,9 +1,8 @@ @use '../../../component-library/styles/tokens' as *; @use '../../../component-library/styles/btn-primary-tokens.scss' as btn-primary; -@use '../../styles/surface-stagger'; -$gutter: clamp(40px, 6vw, 80px); -$content-max: 1480px; +$gutter: clamp(28px, 4vw, 64px); +$content-max: 1360px; .gallery-layout { display: flex; @@ -45,20 +44,16 @@ $content-max: 1480px; align-items: flex-start; gap: $size-gap-4; width: min(100%, $content-max); - padding: $size-gap-8 $gutter $size-gap-6; + padding: 28px $gutter $size-gap-5; margin: 0 auto; box-sizing: border-box; position: sticky; top: 0; z-index: 10; - background: linear-gradient( - to bottom, - var(--color-bg-scene) 0%, - var(--color-bg-scene) 72%, - transparent 100% - ); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + background: color-mix(in srgb, var(--color-bg-scene) 94%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--border-subtle) 76%, transparent); + backdrop-filter: blur(16px) saturate(1.08); + -webkit-backdrop-filter: blur(16px) saturate(1.08); &__identity { flex: 1; @@ -67,7 +62,7 @@ $content-max: 1480px; &__title { margin: 0 0 $size-gap-1; - font-size: clamp(18px, 2vw, 22px); + font-size: clamp(20px, 2vw, 24px); font-weight: $font-weight-semibold; color: var(--color-text-primary); letter-spacing: -0.02em; @@ -103,9 +98,9 @@ $content-max: 1480px; .gallery-zones { display: flex; flex-direction: column; - gap: $size-gap-8; + gap: 28px; width: min(100%, $content-max); - padding: $size-gap-6 $gutter $size-gap-8; + padding: $size-gap-5 $gutter $size-gap-8; margin: 0 auto; box-sizing: border-box; } @@ -113,7 +108,7 @@ $content-max: 1480px; .gallery-zone { display: flex; flex-direction: column; - gap: $size-gap-3; + gap: $size-gap-4; scroll-margin-top: 152px; &__header { @@ -175,8 +170,7 @@ $content-max: 1480px; border: 1px solid var(--border-subtle); position: relative; overflow: hidden; - animation: gallery-item-in 0.28s $easing-decelerate both; - @include surface-stagger.animation-delay(60ms); + animation: gallery-item-in 180ms cubic-bezier(0.23, 1, 0.32, 1) both; &::after { content: ''; @@ -189,18 +183,21 @@ $content-max: 1480px; var(--skeleton-shimmer-peak) 50%, var(--skeleton-shimmer-0) 100% ); - animation: gallery-shimmer 1.6s ease-in-out surface-stagger.delay(0.12s) infinite; + animation: gallery-shimmer 1.2s linear infinite; } } .gallery-empty { - min-height: 180px; + min-height: 220px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: $size-gap-3; padding: $size-gap-8 $size-gap-6; + border: 1px dashed var(--border-subtle); + border-radius: $size-radius-lg; + background: color-mix(in srgb, var(--element-bg-subtle) 72%, transparent); font-size: var(--font-size-sm); color: var(--color-text-muted); text-align: center; @@ -217,7 +214,8 @@ $content-max: 1480px; .gallery-anchor-btn, .gallery-cat-chip { transition: - background $motion-fast $easing-standard, + transform $motion-fast cubic-bezier(0.23, 1, 0.32, 1), + background-color $motion-fast $easing-standard, color $motion-fast $easing-standard, border-color $motion-fast $easing-standard; } @@ -248,6 +246,10 @@ $content-max: 1480px; cursor: not-allowed; } + &:active:not(:disabled) { + transform: scale(0.975); + } + &:focus-visible { outline: 2px solid var(--color-accent-500); outline-offset: -1px; @@ -318,7 +320,7 @@ $content-max: 1480px; gap: $size-gap-1; height: 24px; padding: 0 $size-gap-2; - border-radius: $size-radius-full; + border-radius: $size-radius-base; background: transparent; color: var(--color-text-muted); font-size: var(--font-size-xs); @@ -334,6 +336,10 @@ $content-max: 1480px; outline: 2px solid var(--color-accent-500); outline-offset: -1px; } + + &:active { + transform: scale(0.975); + } } .gallery-filter-chip, @@ -368,7 +374,7 @@ $content-max: 1480px; .gallery-filter-count { height: 16px; padding: 0 4px; - border-radius: $size-radius-full; + border-radius: $size-radius-sm; background: var(--element-bg-medium); color: var(--color-text-muted); font-size: 10px; @@ -378,7 +384,7 @@ $content-max: 1480px; min-width: 24px; height: 24px; padding: 0 8px; - border-radius: $size-radius-full; + border-radius: $size-radius-base; background: var(--element-bg-medium); border: 1px solid var(--border-subtle); font-size: var(--font-size-xs); @@ -391,7 +397,7 @@ $content-max: 1480px; min-width: 18px; height: 18px; padding: 0 5px; - border-radius: $size-radius-full; + border-radius: $size-radius-sm; background: color-mix(in srgb, var(--color-success) 12%, transparent); border: 1px solid color-mix(in srgb, var(--color-success) 28%, transparent); color: var(--color-success); @@ -492,3 +498,11 @@ $content-max: 1480px; animation: none; } } + +@media (prefers-reduced-transparency: reduce) { + .gallery-page-header { + background: var(--color-bg-scene); + backdrop-filter: none; + -webkit-backdrop-filter: none; + } +} diff --git a/src/web-ui/src/app/components/NavBar/NavBar.scss b/src/web-ui/src/app/components/NavBar/NavBar.scss index 14ac7dc861..77e7498e68 100644 --- a/src/web-ui/src/app/components/NavBar/NavBar.scss +++ b/src/web-ui/src/app/components/NavBar/NavBar.scss @@ -1,5 +1,6 @@ /** - * NavBar styles — back/forward + drag region + window controls (32px). + * Compact navigation chrome. Buttons share one geometry and only animate the + * press response; panel and history changes themselves remain immediate. */ @use '../../../component-library/styles/tokens.scss' as *; @@ -7,93 +8,71 @@ .bitfun-nav-bar { display: flex; align-items: center; - height: 40px; + height: 38px; flex-shrink: 0; - padding: 0 $size-gap-2; - gap: $size-gap-1; + padding: 0 6px; + gap: 2px; background: var(--color-bg-primary); user-select: none; &--collapsed { justify-content: flex-start; - // Add tiny horizontal breathing room so edge buttons keep full corner rendering. - padding: 0 calc(#{$size-gap-2} + 1px); + padding-inline: 7px; } &--macos { - // Reserve left native titlebar area for traffic lights in overlay mode. - padding-left: calc(#{$size-gap-2} + 72px); + padding-left: 78px; padding-right: 2px; - } &--macos#{&}--collapsed { - padding-left: calc(#{$size-gap-2} + 1px); + padding-left: 7px; padding-right: 2px; } - // ── Back / Forward buttons ────────────────────────────── - - &__btn { + &__btn, + &__panel-toggle { display: flex; align-items: center; justify-content: center; - width: 26px; - height: 26px; + width: 28px; + height: 28px; + flex-shrink: 0; padding: 0; border: none; - border-radius: 4px; + border-radius: $size-radius-sm; background: transparent; color: var(--color-text-muted); cursor: pointer; - flex-shrink: 0; - transition: color $motion-fast $easing-standard, - background $motion-fast $easing-standard; - - &:hover:not(.is-inactive) { - color: var(--color-text-primary); - background: var(--element-bg-soft); - } + transition: + transform 120ms cubic-bezier(0.23, 1, 0.32, 1), + color 120ms ease, + background 120ms ease; &.is-inactive { - opacity: 0.3; + opacity: 0.32; cursor: default; } + &:active:not(.is-inactive) { + transform: scale(0.96); + background: var(--element-bg-medium); + } + &:focus-visible { outline: 2px solid var(--color-accent-500); - outline-offset: -1px; + outline-offset: -2px; } } - &__panel-toggle { - display: inline-flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - padding: 0; - border: none; - border-radius: 4px; - background: transparent; - color: var(--color-text-muted); - cursor: pointer; - transition: color $motion-fast $easing-standard, - background $motion-fast $easing-standard; - - &:hover { + @media (hover: hover) and (pointer: fine) { + &__btn:hover:not(.is-inactive), + &__panel-toggle:hover { color: var(--color-text-primary); background: var(--element-bg-soft); } - - &:focus-visible { - outline: 2px solid var(--color-accent-500); - outline-offset: -1px; - } } - // ── WindowControls container ──────────────────────────── - // WindowControls renders its own wrapper; just align it here. .window-controls { flex-shrink: 0; } @@ -103,5 +82,6 @@ .bitfun-nav-bar__btn, .bitfun-nav-bar__panel-toggle { transition: none; + transform: none; } } diff --git a/src/web-ui/src/app/components/NavBar/NavBar.tsx b/src/web-ui/src/app/components/NavBar/NavBar.tsx index cbebee436d..8a167e1202 100644 --- a/src/web-ui/src/app/components/NavBar/NavBar.tsx +++ b/src/web-ui/src/app/components/NavBar/NavBar.tsx @@ -1,7 +1,7 @@ /** * NavBar — navigation history controls + window chrome. * - * Sits at the top of the left column, same height as SceneBar (32px). + * Sits at the top of the left column, same height as SceneBar (38px). * Layout: [←][→] [_][□][×] * * - Back/Forward buttons mirror IDE navigation history. @@ -115,6 +115,7 @@ const NavBar: React.FC = ({ {/* Back / Forward */} diff --git a/src/web-ui/src/component-library/components/Input/Input.scss b/src/web-ui/src/component-library/components/Input/Input.scss index 63c5a8f9de..e1891cc5b8 100644 --- a/src/web-ui/src/component-library/components/Input/Input.scss +++ b/src/web-ui/src/component-library/components/Input/Input.scss @@ -47,19 +47,11 @@ background: var(--element-bg-subtle); border: 1px solid transparent; - &:hover:not(.bitfun-input-wrapper--disabled &) { - background: var(--element-bg-soft); - border-color: var(--border-base); - } - &:focus-within { background: var(--element-bg-soft); border-color: transparent; } - &:hover:focus-within { - border-color: var(--color-accent-500); - } } } @@ -74,10 +66,6 @@ .bitfun-input-container { border-color: var(--color-error-border) !important; - &:hover:not(.bitfun-input-wrapper--disabled &) { - border-color: var(--color-error) !important; - } - &:focus-within { border-color: var(--color-error) !important; } @@ -106,21 +94,20 @@ align-items: center; gap: var(--size-gap-2); padding: 0 var(--size-gap-3); - border-radius: 4px; - transition: all var(--motion-base) var(--easing-standard); + border-radius: var(--size-radius-sm); + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + box-shadow var(--motion-fast) ease; position: relative; min-width: 0; overflow: hidden; - &:hover:not(.bitfun-input-wrapper--disabled &) { - background: var(--element-bg-subtle); - border-color: var(--border-strong); - } - &:focus-within { background: transparent; border-color: var(--color-accent-500); outline: none; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 14%, transparent); } } @@ -185,15 +172,52 @@ align-items: center; justify-content: center; border-radius: 4px; - transition: color 0.15s; + transition: + color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); - &:hover { - color: var(--color-text-secondary); + &:active { + transform: scale(0.97); } } +.bitfun-input-hint, .bitfun-input-error-message { font-size: var(--font-size-xs); color: var(--color-error); margin-top: -2px; } + +.bitfun-input-hint { + color: var(--color-text-muted); +} + +@media (hover: hover) and (pointer: fine) { + .bitfun-input-wrapper:not(.bitfun-input-wrapper--disabled) .bitfun-input-container:hover { + background: var(--element-bg-subtle); + border-color: var(--border-strong); + } + + .bitfun-input-wrapper--filled:not(.bitfun-input-wrapper--disabled) .bitfun-input-container:hover { + background: var(--element-bg-soft); + border-color: var(--border-base); + } + + .bitfun-input-wrapper--filled .bitfun-input-container:hover:focus-within { + border-color: var(--color-accent-500); + } + + .bitfun-input-wrapper--error:not(.bitfun-input-wrapper--disabled) .bitfun-input-container:hover { + border-color: var(--color-error) !important; + } + + .bitfun-input-toggle:hover { + color: var(--color-text-secondary); + } +} + +@media (prefers-reduced-motion: reduce) { + .bitfun-input-toggle:active { + transform: none; + } +} diff --git a/src/web-ui/src/component-library/components/Input/Input.tsx b/src/web-ui/src/component-library/components/Input/Input.tsx index ddb9ba8bd0..64ca1c95d3 100644 --- a/src/web-ui/src/component-library/components/Input/Input.tsx +++ b/src/web-ui/src/component-library/components/Input/Input.tsx @@ -2,7 +2,7 @@ * Input component */ -import React, { forwardRef } from 'react'; +import React, { forwardRef, useId } from 'react'; import './Input.scss'; export interface InputProps extends Omit, 'size' | 'prefix'> { @@ -31,6 +31,9 @@ export const Input = forwardRef(({ disabled, ...props }, ref) => { + const generatedId = useId(); + const inputId = props.id ?? `${generatedId}-input`; + const supportId = `${generatedId}-support`; const resolvedInputSize = size ?? inputSize; const classNames = [ 'bitfun-input-wrapper', @@ -45,22 +48,25 @@ export const Input = forwardRef(({ return (
- {label && } + {label && }
{prefix && {prefix}} {suffix && {suffix}}
{!error && hint && ( - {hint} + {hint} )} {error && errorMessage && ( - {errorMessage} + {errorMessage} )}
); diff --git a/src/web-ui/src/component-library/components/Modal/Modal.scss b/src/web-ui/src/component-library/components/Modal/Modal.scss index 824bf8d862..5cc9bac016 100644 --- a/src/web-ui/src/component-library/components/Modal/Modal.scss +++ b/src/web-ui/src/component-library/components/Modal/Modal.scss @@ -20,7 +20,7 @@ $modal-edge-gutter-sm: 10px; justify-content: center; padding: 24px; z-index: tokens.$z-modal; - animation: modal-overlay-fade 0.2s ease-out both; + animation: modal-overlay-fade calc(var(--motion-fast) + 70ms) var(--easing-standard) both; &--bottom-left { align-items: flex-end; @@ -28,7 +28,8 @@ $modal-edge-gutter-sm: 10px; padding: 0 0 20px 20px; .modal { - animation: modal-dialog-enter-bottom 0.24s cubic-bezier(0.22, 1, 0.36, 1) both; + transform-origin: left bottom; + animation: modal-dialog-enter-bottom calc(var(--motion-fast) + 70ms) var(--easing-standard) both; } } @@ -38,7 +39,8 @@ $modal-edge-gutter-sm: 10px; padding: 0 20px 20px 0; .modal { - animation: modal-dialog-enter-bottom 0.24s cubic-bezier(0.22, 1, 0.36, 1) both; + transform-origin: right bottom; + animation: modal-dialog-enter-bottom calc(var(--motion-fast) + 70ms) var(--easing-standard) both; } } } @@ -60,11 +62,12 @@ $modal-edge-gutter-sm: 10px; flex-direction: column; background: var(--color-bg-primary); border: 1px solid var(--border-subtle); - border-radius: 8px; + border-radius: var(--size-radius-base); box-shadow: 0 16px 40px rgba(var(--color-static-black-rgb), 0.45); max-height: calc(100vh - 48px); overflow: hidden; - animation: modal-dialog-enter 0.24s cubic-bezier(0.22, 1, 0.36, 1) both; + transform-origin: center; + animation: modal-dialog-enter calc(var(--motion-fast) + 70ms) var(--easing-standard) both; &--small { @@ -92,11 +95,7 @@ $modal-edge-gutter-sm: 10px; &--draggable { cursor: default; - transition: box-shadow 0.2s ease; - - &:hover { - box-shadow: 0 20px 44px var(--color-overlay-black-50); - } + transition: box-shadow var(--motion-fast) ease; } &--dragging { @@ -127,11 +126,7 @@ $modal-edge-gutter-sm: 10px; &--draggable { cursor: move; user-select: none; - transition: background 0.2s ease; - - &:hover { - background: var(--element-bg-subtle); - } + transition: background-color var(--motion-fast) ease; } &--empty { @@ -185,7 +180,12 @@ $modal-edge-gutter-sm: 10px; border-radius: 6px; color: var(--color-text-muted); cursor: pointer; - transition: all 0.2s ease; + transform: scale(1); + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); outline: none; svg { @@ -194,14 +194,8 @@ $modal-edge-gutter-sm: 10px; height: 12px; } - &:hover { - background: var(--element-bg-subtle); - border-color: var(--border-subtle); - color: var(--color-text-primary); - } - &:active { - transform: scale(0.92); + transform: scale(0.97); } &:focus-visible { @@ -236,7 +230,7 @@ $modal-edge-gutter-sm: 10px; @keyframes modal-dialog-enter { from { opacity: 0; - transform: translateY(8px) scale(0.992); + transform: translateY(6px) scale(0.985); } to { opacity: 1; @@ -247,7 +241,7 @@ $modal-edge-gutter-sm: 10px; @keyframes modal-dialog-enter-bottom { from { opacity: 0; - transform: translateY(10px) scale(0.992); + transform: translateY(8px) scale(0.985); } to { opacity: 1; @@ -258,28 +252,26 @@ $modal-edge-gutter-sm: 10px; @keyframes modal-dialog-exit { from { opacity: 1; - translate: 0 0; - scale: 1; + transform: translateY(0) scale(1); } to { opacity: 0; - translate: 0 5px; - scale: 0.992; + transform: translateY(4px) scale(0.99); } } .modal-overlay--exiting { - animation: modal-overlay-exit 0.18s ease-in both; + animation: modal-overlay-exit calc(var(--motion-fast) + 30ms) var(--easing-standard) both; pointer-events: none; } .modal.modal--exiting { - animation: modal-dialog-exit 0.18s cubic-bezier(0.4, 0, 1, 1) both; + animation: modal-dialog-exit calc(var(--motion-fast) + 30ms) var(--easing-standard) both; } .modal--draggable, .modal--resizable { - animation: modal-overlay-fade 0.2s ease-out both; + animation: modal-overlay-fade calc(var(--motion-fast) + 70ms) var(--easing-standard) both; } @@ -329,9 +321,41 @@ $modal-edge-gutter-sm: 10px; &--sw { bottom: 0; left: 0; cursor: sw-resize; } &--se { bottom: 0; right: 0; cursor: se-resize; } - &:hover { +} + +@media (hover: hover) and (pointer: fine) { + .modal--draggable:hover { + box-shadow: 0 20px 44px var(--color-overlay-black-50); + } + + .modal__header--draggable:hover, + .modal__resize-handle:hover { background-color: var(--element-bg-subtle); } + + .modal__close:hover { + background: var(--element-bg-subtle); + border-color: var(--border-subtle); + color: var(--color-text-primary); + } +} + +@media (prefers-reduced-motion: reduce) { + .modal-overlay, + .modal, + .modal--draggable, + .modal--resizable { + animation: modal-overlay-fade var(--motion-fast) ease both; + } + + .modal-overlay--exiting, + .modal.modal--exiting { + animation: modal-overlay-exit var(--motion-fast) ease both; + } + + .modal__close:active { + transform: none; + } } diff --git a/src/web-ui/src/component-library/components/Search/Search.scss b/src/web-ui/src/component-library/components/Search/Search.scss index 69c981d216..34ea61c0ca 100644 --- a/src/web-ui/src/component-library/components/Search/Search.scss +++ b/src/web-ui/src/component-library/components/Search/Search.scss @@ -16,7 +16,10 @@ background: transparent; border: 1px solid var(--border-base); border-radius: $size-radius-sm; - transition: all $motion-base $easing-standard; + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + box-shadow var(--motion-fast) ease; min-height: 36px; } @@ -26,6 +29,7 @@ &--focused:not(.search--disabled) &__wrapper { border-color: var(--color-accent-500); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 14%, transparent); } &--focused:not(.search--disabled) &__prefix { @@ -36,7 +40,7 @@ &--expandable:not(.search--focused) &__wrapper { width: 200px; - transition: width $motion-base $easing-standard; + transition: width var(--motion-fast) var(--easing-standard); } &--expandable.search--focused &__wrapper { @@ -49,12 +53,12 @@ justify-content: center; flex-shrink: 0; color: var(--color-text-muted); - transition: color $motion-base $easing-standard; + transition: color var(--motion-fast) ease; } &__icon { display: block; - transition: color $motion-base $easing-standard; + transition: color var(--motion-fast) ease; } &__loading-icon { @@ -88,7 +92,7 @@ &::placeholder { color: var(--color-text-muted); - transition: color $motion-base $easing-standard; + transition: color var(--motion-fast) ease; } &:focus { @@ -130,15 +134,16 @@ background: transparent; color: var(--color-text-muted); cursor: pointer; - transition: color $motion-base $easing-standard; - animation: search-fade-in 0.2s ease forwards; - - &:hover { - color: var(--color-text-secondary); - } + transform: scale(1); + transition: + color var(--motion-fast) ease, + background-color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); + animation: search-fade-in var(--motion-fast) var(--easing-standard) forwards; &:active { color: var(--color-text-primary); + transform: scale(0.97); } } @@ -164,14 +169,14 @@ font-weight: $font-weight-medium; font-family: $font-family-sans; cursor: pointer; - transition: all $motion-base $easing-standard; + transform: scale(1); + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); white-space: nowrap; - &:hover:not(:disabled) { - background: var(--color-accent-100); - color: var(--color-accent-400); - } - &:active:not(:disabled) { transform: scale(0.98); } @@ -208,7 +213,7 @@ display: flex; align-items: center; gap: $size-gap-1; - animation: search-slide-down 0.2s ease; + animation: search-slide-down var(--motion-fast) var(--easing-standard); } &--small { @@ -292,7 +297,7 @@ @keyframes search-fade-in { from { opacity: 0; - transform: scale(0.8); + transform: scale(0.96); } to { opacity: 1; @@ -303,10 +308,43 @@ @keyframes search-slide-down { from { opacity: 0; - transform: translateY(-8px); + transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } } + +@media (hover: hover) and (pointer: fine) { + .search__clear:hover { + color: var(--color-text-primary); + background: var(--element-bg-subtle); + } + + .search__button:hover:not(:disabled) { + background: var(--color-accent-100); + color: var(--color-accent-400); + } +} + +@media (prefers-reduced-motion: reduce) { + .search__wrapper, + .search__clear, + .search__button { + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + color var(--motion-fast) ease; + } + + .search__clear, + .search__error-message { + animation: none; + } + + .search__clear:active, + .search__button:active:not(:disabled) { + transform: none; + } +} diff --git a/src/web-ui/src/component-library/components/Search/Search.tsx b/src/web-ui/src/component-library/components/Search/Search.tsx index 745f5a0d44..1b071a3206 100644 --- a/src/web-ui/src/component-library/components/Search/Search.tsx +++ b/src/web-ui/src/component-library/components/Search/Search.tsx @@ -2,7 +2,7 @@ * Search input component */ -import React, { useState, useRef, useEffect, useCallback, forwardRef } from 'react'; +import React, { useState, useRef, useEffect, useCallback, forwardRef, useId } from 'react'; import { useI18n } from '@/infrastructure/i18n'; import './Search.scss'; @@ -128,6 +128,8 @@ export const Search = forwardRef(({ ...rootProps }, ref) => { const { t } = useI18n('components'); + const generatedId = useId(); + const errorId = `${generatedId}-error`; // Resolve i18n default values const resolvedPlaceholder = placeholder ?? t('search.placeholder'); @@ -254,6 +256,9 @@ export const Search = forwardRef(({ aria-label={inputAriaLabel ?? t('search.placeholder')} aria-controls={ariaControls} aria-expanded={ariaExpanded} + aria-busy={loading || undefined} + aria-invalid={error || undefined} + aria-describedby={error && errorMessage ? errorId : undefined} /> {clearable && inputValue && !loading && !disabled && ( @@ -295,7 +300,7 @@ export const Search = forwardRef(({ {error && errorMessage && ( -
{errorMessage}
+ )} ); diff --git a/src/web-ui/src/component-library/components/Select/Select.scss b/src/web-ui/src/component-library/components/Select/Select.scss index 68315d2c38..f693e93a83 100644 --- a/src/web-ui/src/component-library/components/Select/Select.scss +++ b/src/web-ui/src/component-library/components/Select/Select.scss @@ -26,21 +26,26 @@ border: 1px solid var(--border-base); border-radius: var(--size-radius-sm); cursor: pointer; - transition: all var(--motion-base) var(--easing-standard); + transform: scale(1); + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + box-shadow var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); min-height: 36px; position: relative; min-width: 0; overflow: hidden; - &:hover:not(.select--disabled &) { - background: var(--element-bg-subtle); - border-color: var(--border-strong); - } - &:focus-visible { outline: none; background: transparent; border-color: var(--color-accent-500); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 14%, transparent); + } + + &:active:not(.select--disabled &) { + transform: scale(0.985); } } @@ -71,9 +76,6 @@ .select__trigger { border-color: var(--color-error-border); - &:hover:not(.select--disabled &) { - border-color: var(--color-error); - } } } @@ -125,12 +127,9 @@ font-size: var(--font-size-xs); color: var(--color-accent-500); max-width: 150px; - transition: all var(--motion-fast) var(--easing-standard); - - &:hover { - background: var(--color-accent-100); - border-color: var(--border-accent); - } + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease; &--more { background: transparent; @@ -138,9 +137,6 @@ color: var(--color-text-secondary); cursor: default; - &:hover { - background: transparent; - } } } @@ -166,13 +162,19 @@ cursor: pointer; font-size: 14px; line-height: 1; + padding: 0; + border: 0; + background: transparent; color: var(--color-text-muted); - transition: all var(--motion-fast) var(--easing-standard); + transform: scale(1); + transition: + background-color var(--motion-fast) ease, + color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); margin-left: 2px; - &:hover { - background: var(--color-error-bg); - color: var(--color-error); + &:active { + transform: scale(0.97); } } @@ -192,13 +194,19 @@ border-radius: 50%; color: var(--color-text-muted); cursor: pointer; - transition: all var(--motion-fast) var(--easing-standard); + transform: scale(1); + transition: + background-color var(--motion-fast) ease, + color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); font-size: 16px; line-height: 1; + padding: 0; + border: 0; + background: transparent; - &:hover { - background: var(--color-error-bg); - color: var(--color-error); + &:active { + transform: scale(0.97); } } @@ -221,7 +229,9 @@ align-items: center; justify-content: center; color: var(--color-text-muted); - transition: all var(--motion-base) var(--easing-standard); + transition: + color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); &--open { transform: rotate(180deg); @@ -230,6 +240,7 @@ } &__dropdown { + --select-entry-y: -2px; position: absolute; left: 0; right: 0; @@ -240,7 +251,8 @@ border-radius: 0 0 var(--size-radius-sm) var(--size-radius-sm); box-shadow: 0 4px 12px var(--color-overlay-black-40); z-index: tokens.$z-dropdown; - animation: select-dropdown-appear var(--motion-fast) tokens.$easing-decelerate; + transform-origin: center top; + animation: select-dropdown-appear var(--motion-fast) var(--easing-standard); overflow: hidden; display: flex; flex-direction: column; @@ -260,11 +272,13 @@ } &--top { + --select-entry-y: 2px; bottom: 100%; border-top: 1px solid var(--border-medium); border-bottom: none; border-radius: var(--size-radius-sm) var(--size-radius-sm) 0 0; box-shadow: 0 -4px 12px var(--color-overlay-black-40); + transform-origin: center bottom; &::before { top: auto; @@ -294,12 +308,18 @@ cursor: pointer; font-size: 14px; line-height: 1; - transition: all var(--motion-fast) var(--easing-standard); + transform: scale(1); + transition: + background-color var(--motion-fast) ease, + color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); flex-shrink: 0; + padding: 0; + border: 0; + background: transparent; - &:hover { - background: var(--color-error-bg); - color: var(--color-error); + &:active { + transform: scale(0.97); } } @@ -315,16 +335,12 @@ box-shadow: none !important; appearance: none; -webkit-appearance: none; - transition: all var(--motion-fast) var(--easing-standard); + transition: background-color var(--motion-fast) ease; &::placeholder { color: var(--color-text-muted); } - &:hover { - background: var(--element-bg-soft); - } - &:focus, &:focus-visible { background: var(--element-bg-soft); @@ -345,14 +361,13 @@ font-weight: var(--font-weight-medium); border-bottom: 1px solid var(--border-subtle); background: transparent; - transition: all var(--motion-fast) var(--easing-standard); + transition: + background-color var(--motion-fast) ease, + color var(--motion-fast) ease; border-radius: 3px; margin-bottom: 0; padding-bottom: 8px; - &:hover { - background: var(--element-bg-subtle); - } } &__options { @@ -367,7 +382,9 @@ gap: 8px; padding: 6px 10px; cursor: pointer; - transition: all var(--motion-fast) var(--easing-standard); + transition: + background-color var(--motion-fast) ease, + color var(--motion-fast) ease; font-size: var(--font-size-sm); color: var(--color-text-secondary); border-radius: 3px; @@ -379,11 +396,6 @@ margin-bottom: 0; } - &:hover:not(&--disabled) { - background: var(--element-bg-subtle); - color: var(--color-text-primary); - } - &--highlighted:not(&--disabled) { background: var(--element-bg-subtle); color: var(--color-text-primary); @@ -394,9 +406,6 @@ color: var(--color-accent-500); font-weight: var(--font-weight-medium); - &:hover { - background: var(--color-accent-200); - } } &--disabled { @@ -444,7 +453,10 @@ justify-content: center; flex-shrink: 0; font-size: 11px; - transition: all var(--motion-fast) var(--easing-standard); + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + color var(--motion-fast) ease; background: transparent; color: transparent; @@ -494,7 +506,9 @@ gap: 8px; padding: 8px 12px; cursor: pointer; - transition: all var(--motion-fast) var(--easing-standard); + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease; font-size: var(--font-size-sm); color: var(--color-accent-500); border-radius: 3px; @@ -502,10 +516,6 @@ background: var(--color-accent-100); border: 1px dashed var(--border-accent-soft); - &:hover { - background: var(--color-accent-200); - border-color: var(--border-accent); - } } &__custom-value-text { @@ -606,7 +616,7 @@ @keyframes select-dropdown-appear { from { opacity: 0; - transform: translateY(-8px); + transform: translateY(var(--select-entry-y)) scale(0.985); } to { opacity: 1; @@ -614,6 +624,71 @@ } } +@media (hover: hover) and (pointer: fine) { + .select:not(.select--disabled) .select__trigger:hover { + background: var(--element-bg-subtle); + border-color: var(--border-strong); + } + + .select--error:not(.select--disabled) .select__trigger:hover { + border-color: var(--color-error); + } + + .select__tag:hover { + background: var(--color-accent-100); + border-color: var(--border-accent); + } + + .select__tag--more:hover { + background: transparent; + border-color: var(--border-base); + } + + .select__tag-remove:hover, + .select__clear:hover, + .select__search-clear:hover { + background: var(--color-error-bg); + color: var(--color-error); + } + + .select__search-input:hover { + background: var(--element-bg-soft); + } + + .select__select-all:hover, + .select__option:hover:not(.select__option--disabled) { + background: var(--element-bg-subtle); + color: var(--color-text-primary); + } + + .select__option--selected:hover { + background: var(--color-accent-200); + } + + .select__custom-value-hint:hover { + background: var(--color-accent-200); + border-color: var(--border-accent); + } +} + +@media (prefers-reduced-motion: reduce) { + .select__trigger, + .select__tag-remove, + .select__clear, + .select__search-clear { + transform: none; + } + + .select__dropdown { + animation: select-dropdown-fade var(--motion-fast) ease; + } +} + +@keyframes select-dropdown-fade { + from { opacity: 0; } + to { opacity: 1; } +} + @keyframes select-spin { to { transform: rotate(360deg); diff --git a/src/web-ui/src/component-library/components/Select/Select.tsx b/src/web-ui/src/component-library/components/Select/Select.tsx index dc03c4ecae..e15fb3f299 100644 --- a/src/web-ui/src/component-library/components/Select/Select.tsx +++ b/src/web-ui/src/component-library/components/Select/Select.tsx @@ -55,6 +55,7 @@ export interface SelectProps extends Omit, dropdownTestId?: string; triggerAriaLabel?: string; triggerAriaLabelledBy?: string; + triggerAriaDescribedBy?: string; } export const Select: React.FC = ({ @@ -88,12 +89,14 @@ export const Select: React.FC = ({ dropdownTestId, triggerAriaLabel, triggerAriaLabelledBy, + triggerAriaDescribedBy, ...rootProps }) => { const { t } = useI18n('components'); const baseId = React.useId(); const labelId = `${baseId}-label`; const listboxId = `${baseId}-listbox`; + const errorId = `${baseId}-error`; // Resolve i18n default values const resolvedPlaceholder = placeholder ?? t('select.placeholder'); @@ -402,7 +405,7 @@ export const Select: React.FC = ({ useEffect(() => { if (highlightedIndex >= 0 && dropdownRef.current && isKeyboardNavigation.current) { const highlightedElement = document.getElementById(`${listboxId}-option-${highlightedIndex}`); - highlightedElement?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + highlightedElement?.scrollIntoView({ block: 'nearest' }); isKeyboardNavigation.current = false; } }, [highlightedIndex, listboxId]); @@ -443,15 +446,17 @@ export const Select: React.FC = ({ {opt.icon && {opt.icon}} {opt.label} - { e.stopPropagation(); handleSelect(opt); }} + aria-label={opt.label} > × - + ))} {remaining > 0 && ( @@ -540,8 +545,11 @@ export const Select: React.FC = ({ ? `${listboxId}-option-${highlightedIndex}` : undefined} aria-disabled={disabled} + aria-busy={loading || undefined} + aria-invalid={error || undefined} aria-label={triggerAriaLabel} aria-labelledby={triggerAriaLabelledBy ?? (label ? labelId : undefined)} + aria-describedby={error && errorMessage ? errorId : triggerAriaDescribedBy} data-testid={triggerTestId} > {renderSelectedValue()} @@ -553,7 +561,14 @@ export const Select: React.FC = ({ )} {clearable && !loading && (multiple ? (selectedValue as any[]).length > 0 : selectedValue) && ( - × + )} @@ -569,6 +584,8 @@ export const Select: React.FC = ({ className={`select__dropdown select__dropdown--${resolvedPlacement}`} ref={dropdownRef} role="listbox" + aria-multiselectable={multiple || undefined} + aria-busy={loading || undefined} data-testid={dropdownTestId} > {searchable && ( @@ -616,7 +633,8 @@ export const Select: React.FC = ({ }} /> {searchQuery && ( - { e.stopPropagation(); @@ -624,15 +642,21 @@ export const Select: React.FC = ({ setHighlightedIndex(-1); searchInputRef.current?.focus(); }} + aria-label={t('search.clear')} > × - + )} )} {multiple && showSelectAll && filteredOptions.length > 0 && ( -
+
!opt.disabled).every(opt => isSelected(opt.value))} + > !opt.disabled).every(opt => isSelected(opt.value)) ? 'select__checkbox--checked' : '' @@ -708,7 +732,7 @@ export const Select: React.FC = ({ )} {error && errorMessage && ( -
{errorMessage}
+ )}
); diff --git a/src/web-ui/src/component-library/components/Switch/Switch.scss b/src/web-ui/src/component-library/components/Switch/Switch.scss index 7a66296b25..1e3639ee6d 100644 --- a/src/web-ui/src/component-library/components/Switch/Switch.scss +++ b/src/web-ui/src/component-library/components/Switch/Switch.scss @@ -57,7 +57,11 @@ background: transparent; border: 1px solid var(--border-subtle); border-radius: 10px; - transition: all 0.2s ease; + transform: scale(1); + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + transform var(--motion-fast) var(--easing-standard); pointer-events: none; &--checked { @@ -73,7 +77,7 @@ color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.5px; - transition: opacity 0.2s ease; + transition: opacity var(--motion-fast) ease; } &__thumb { @@ -84,7 +88,11 @@ background: var(--element-bg-subtle); border: 1px solid var(--border-base); border-radius: 50%; - transition: all 0.2s ease; + transform: translateX(0); + transition: + transform var(--motion-fast) var(--easing-standard), + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease; display: flex; align-items: center; justify-content: center; @@ -99,7 +107,7 @@ } &__input:checked + &__track &__thumb { - left: calc(100% - 18px); + transform: translateX(16px); background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); } @@ -118,30 +126,6 @@ outline: none; } - &:hover:not(&--disabled):not(&--loading) &__track { - border-color: var(--border-base); - } - - &:hover:not(&--disabled):not(&--loading) &__thumb { - background: var(--element-bg-base); - border-color: var(--border-base); - } - - &:hover:not(&--disabled):not(&--loading) &__input:checked + &__track { - background: var(--color-accent-300); - border-color: var(--color-accent-500); - } - - &:hover:not(&--disabled):not(&--loading) &__input:checked + &__track &__thumb { - background: var(--btn-primary-hover-bg); - border-color: var(--btn-primary-hover-bg); - } - - :root[data-theme-type='dark'] &:hover:not(&--disabled):not(&--loading) &__input:checked + &__track &__thumb { - background: color-mix(in srgb, var(--color-accent-500) 88%, white); - border-color: var(--color-accent-500); - } - &__input:checked + &__track &__loading { color: var(--color-overlay-white-95); } @@ -186,7 +170,7 @@ } .bitfun-switch__input:checked + .bitfun-switch__track .bitfun-switch__thumb { - left: calc(100% - 16px); + transform: translateX(14px); } .bitfun-switch__loading { @@ -223,7 +207,7 @@ } .bitfun-switch__input:checked + .bitfun-switch__track .bitfun-switch__thumb { - left: calc(100% - 22px); + transform: translateX(20px); } .bitfun-switch__loading { @@ -241,6 +225,49 @@ } } +@media (hover: hover) and (pointer: fine) { + .bitfun-switch:not(.bitfun-switch--disabled):not(.bitfun-switch--loading) { + &:hover .bitfun-switch__track { + border-color: var(--border-base); + } + + &:hover .bitfun-switch__thumb { + background: var(--element-bg-base); + border-color: var(--border-base); + } + + &:hover .bitfun-switch__input:checked + .bitfun-switch__track { + background: var(--color-accent-300); + border-color: var(--color-accent-500); + } + + &:hover .bitfun-switch__input:checked + .bitfun-switch__track .bitfun-switch__thumb { + background: var(--btn-primary-hover-bg); + border-color: var(--btn-primary-hover-bg); + } + } + + :root[data-theme-type='dark'] + .bitfun-switch:not(.bitfun-switch--disabled):not(.bitfun-switch--loading):hover + .bitfun-switch__input:checked + .bitfun-switch__track + .bitfun-switch__thumb { + background: color-mix(in srgb, var(--color-accent-500) 88%, white); + border-color: var(--color-accent-500); + } +} + +@media (prefers-reduced-motion: reduce) { + .bitfun-switch__track { + transition: + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease; + } + + .bitfun-switch__input:active:not(:disabled) + .bitfun-switch__track { + transform: none; + } +} + @keyframes bitfun-switch-spin { from { transform: rotate(0deg); diff --git a/src/web-ui/src/component-library/components/Switch/Switch.tsx b/src/web-ui/src/component-library/components/Switch/Switch.tsx index 0e4358c929..03346f17aa 100644 --- a/src/web-ui/src/component-library/components/Switch/Switch.tsx +++ b/src/web-ui/src/component-library/components/Switch/Switch.tsx @@ -47,12 +47,13 @@ export const Switch = forwardRef(