diff --git a/webapp/src/styles.css b/webapp/src/styles.css index 7c4a830..2eab3c7 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -10,7 +10,9 @@ @import './styles/responsive.css'; @import './styles/dark.css'; -/* Unified product polish: clean, flat, quiet surfaces across desktop, mobile, and dark mode. */ +/* Unified product polish: refined, smooth, comfortable surfaces across desktop, mobile, and dark mode. */ + +/* ── surface consistency ── */ .app-shell, .auth-card, .dialog-card, @@ -48,69 +50,7 @@ border-color: var(--line-soft); } -.brand-logo, -.brand-wordmark, -.standalone-brand-logo, -.standalone-brand-wordmark { - filter: none; -} - -.btn, -.input, -.search-input, -.side-link, -.mobile-tab, -.tree-btn, -.list-item, -.dialog-card, -.mobile-sidebar-sheet, -.mobile-detail-sheet, -.create-menu, -.sort-menu, -.toast-item { - transition-duration: 150ms; -} - -.btn:hover:not(:disabled), -.side-link:hover, -.mobile-tab:hover, -.tree-btn:hover, -.list-item:hover, -.create-menu-item:hover, -.sort-menu-item:hover, -.folder-delete-btn:hover, -.eye-btn:hover, -.password-toggle:hover { - transform: none; -} - -.btn-primary { - background: var(--primary); - border-color: transparent; - color: #ffffff; - box-shadow: none; -} - -.btn-primary:hover { - background: var(--primary-hover); - box-shadow: none; -} - -.btn-secondary, -.btn-danger { - box-shadow: none; -} - -.btn-secondary:hover, -.side-link:hover, -.mobile-tab:hover, -.tree-btn:hover, -.list-item:hover, -.backup-destination-item:hover, -.mobile-settings-link:hover { - background: var(--panel-subtle); -} - +/* ── active states ── */ .side-link.active, .mobile-tab.active, .tree-btn.active, @@ -122,32 +62,9 @@ background: color-mix(in srgb, var(--primary) 12%, var(--panel)); border-color: color-mix(in srgb, var(--primary) 32%, var(--line)); color: var(--primary-strong); - box-shadow: none; -} - -.list-item::before, -.topbar-actions .btn::before, -.user-chip::before, -.side-link::before, -.mobile-tab::before { - display: none; -} - -.stagger-item { - opacity: 1; - animation: none; -} - -.dialog-mask { - background: rgba(15, 23, 42, 0.42); - backdrop-filter: none; - -webkit-backdrop-filter: none; -} - -.dialog-mask.warning { - background: rgba(15, 23, 42, 0.56); } +/* ── danger / warning panels (cosmetic resets only) ── */ .dialog-card.warning, :root[data-theme='dark'] .dialog-card.warning { background: var(--panel); @@ -159,7 +76,6 @@ border-radius: var(--radius-lg); background: color-mix(in srgb, var(--danger) 12%, var(--panel)); color: var(--danger); - box-shadow: none; } .dialog-warning-kicker { @@ -171,25 +87,10 @@ :root[data-theme='dark'] .dialog-message.warning { background: color-mix(in srgb, var(--danger) 8%, var(--panel)); border-color: color-mix(in srgb, var(--danger) 20%, var(--line)); - box-shadow: none; color: var(--text); } -.mobile-sidebar-sheet, -.mobile-detail-sheet { - transform: none; - box-shadow: var(--shadow-md); -} - -.mobile-sidebar-sheet.open, -.mobile-detail-sheet.open { - transform: none; -} - -.mobile-fab-trigger { - box-shadow: var(--shadow-md); -} - +/* ── theme switch ── */ .theme-switch-slider, .theme-switch-input:checked + .theme-switch-slider, :root[data-theme='dark'] .theme-switch-slider { @@ -203,6 +104,7 @@ box-shadow: var(--shadow-sm); } +/* ── dark mode surface resets ── */ :root[data-theme='dark'] .app-shell, :root[data-theme='dark'] .auth-card, :root[data-theme='dark'] .dialog-card, @@ -244,21 +146,18 @@ background: var(--panel); border-color: var(--line); color: var(--primary); - box-shadow: none; } :root[data-theme='dark'] .btn-primary { background: var(--primary); border-color: transparent; color: #08111f; - box-shadow: none; } :root[data-theme='dark'] .btn-danger { background: var(--panel); border-color: color-mix(in srgb, var(--danger) 36%, var(--line)); color: var(--danger); - box-shadow: none; } :root[data-theme='dark'] .list-item:hover, diff --git a/webapp/src/styles/base.css b/webapp/src/styles/base.css index 3d1bcd7..7a655c0 100644 --- a/webapp/src/styles/base.css +++ b/webapp/src/styles/base.css @@ -11,6 +11,10 @@ body, font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', sans-serif; } +html { + scroll-behavior: smooth; +} + body { @apply relative antialiased; transition: background-color var(--dur-medium) var(--ease-smooth), color var(--dur-medium) var(--ease-smooth); @@ -20,3 +24,36 @@ body.dialog-open { @apply overflow-hidden; overscroll-behavior: contain; } + +::selection { + background: color-mix(in srgb, var(--primary) 20%, transparent); + color: var(--text); +} + +:focus-visible { + outline: none; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent); +} + +/* --- custom scrollbar --- */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--muted) 30%, transparent); + border-radius: 999px; +} + +::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--muted) 50%, transparent); +} + +::-webkit-scrollbar-corner { + background: transparent; +} diff --git a/webapp/src/styles/dark.css b/webapp/src/styles/dark.css index a8e0d8f..eca0fb7 100644 --- a/webapp/src/styles/dark.css +++ b/webapp/src/styles/dark.css @@ -143,3 +143,43 @@ background: #ffffff; border-color: rgba(15, 23, 42, 0.12); } + +/* ── dark mode scrollbar ── */ +:root[data-theme='dark'] ::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--muted) 24%, transparent); +} + +:root[data-theme='dark'] ::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--muted) 44%, transparent); +} + +/* ── dark mode backdrop-filter ── */ +:root[data-theme='dark'] .dialog-mask { + backdrop-filter: blur(8px) brightness(0.7); + -webkit-backdrop-filter: blur(8px) brightness(0.7); +} + +:root[data-theme='dark'] .topbar { + background: rgba(17, 24, 39, 0.78); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +/* ── dark mode depth ── */ +:root[data-theme='dark'] .card, +:root[data-theme='dark'] .list-panel, +:root[data-theme='dark'] .sidebar-block { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.20), 0 8px 24px rgba(0, 0, 0, 0.16); +} + +:root[data-theme='dark'] .app-shell { + box-shadow: 0 4px 40px rgba(0, 0, 0, 0.30); +} + +:root[data-theme='dark'] .list-item:hover { + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(139, 184, 255, 0.12); +} + +:root[data-theme='dark'] .list-item.active { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 4px 0 0 rgba(139, 184, 255, 0.70); +} diff --git a/webapp/src/styles/forms.css b/webapp/src/styles/forms.css index a93af92..6783409 100644 --- a/webapp/src/styles/forms.css +++ b/webapp/src/styles/forms.css @@ -123,10 +123,12 @@ input[type='file'].input::file-selector-button:hover { .btn:hover:not(:disabled) { transform: translateY(-1px); + transition-duration: var(--dur-quick); } .btn:active:not(:disabled) { - transform: translateY(0) scale(0.985); + transform: translateY(0) scale(0.97); + transition-duration: var(--dur-instant); } .btn-icon { diff --git a/webapp/src/styles/motion.css b/webapp/src/styles/motion.css index 073672a..2d15735 100644 --- a/webapp/src/styles/motion.css +++ b/webapp/src/styles/motion.css @@ -10,102 +10,58 @@ } @keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; } + to { opacity: 1; } } @keyframes fade-in-up { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } } @keyframes shell-enter { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; transform: scale(0.98); } + to { opacity: 1; transform: scale(1); } } @keyframes surface-enter { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } } @keyframes menu-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; transform: translateY(-6px) scale(0.97); } + to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes dialog-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; transform: translateY(12px) scale(0.97); } + to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes toast-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; transform: translateX(16px) scale(0.96); } + to { opacity: 1; transform: translateX(0) scale(1); } } @keyframes stagger-rise { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } } @keyframes fade-out { - from { - opacity: 1; - } - to { - opacity: 0; - } + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(-6px) scale(0.98); } } @keyframes dialog-out { - from { - opacity: 1; - } - to { - opacity: 0; - } + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(8px) scale(0.97); } } @keyframes route-stage-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { diff --git a/webapp/src/styles/responsive.css b/webapp/src/styles/responsive.css index 0167fd0..e082445 100644 --- a/webapp/src/styles/responsive.css +++ b/webapp/src/styles/responsive.css @@ -173,13 +173,18 @@ @apply grid justify-items-center gap-1 rounded-xl px-1 py-1.5 text-[11px] font-bold no-underline; color: #64748b; transition: - transform 220ms var(--ease-out-soft), + transform 180ms var(--ease-spring), background-color var(--dur-fast) var(--ease-smooth), color var(--dur-fast) var(--ease-smooth); } .mobile-tab:hover { - transform: translateY(-1px); + transform: translateY(-2px); + } + + .mobile-tab:active { + transform: translateY(0) scale(0.95); + transition-duration: 80ms; } .mobile-tab.active { @@ -206,11 +211,11 @@ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.16); visibility: hidden; pointer-events: none; - transform: translate3d(0, 10px, 0) scale(0.98); + transform: translate3d(0, 12px, 0) scale(0.97); transition: - opacity 220ms var(--ease-smooth), - transform 240ms var(--ease-out-soft), - visibility 220ms var(--ease-smooth); + opacity 200ms var(--ease-out-expo), + transform 260ms var(--ease-spring), + visibility 200ms var(--ease-out-expo); } .mobile-sidebar-sheet.open { @@ -328,6 +333,15 @@ .mobile-fab-trigger { @apply h-14 w-9 gap-0 rounded-full p-0 text-[0]; box-shadow: 0 14px 30px rgba(37, 99, 235, 0.28); + transition: transform 180ms var(--ease-spring), box-shadow var(--dur-fast) var(--ease-out-soft); + } + + .mobile-fab-trigger:hover { + transform: scale(1.06); + } + + .mobile-fab-trigger:active { + transform: scale(0.94); } .mobile-fab-trigger .btn-icon { @@ -369,11 +383,11 @@ padding: 0 0 18px; visibility: hidden; pointer-events: none; - transform: translate3d(0, 18px, 0); + transform: translate3d(0, 20px, 0); transition: - opacity 220ms var(--ease-smooth), - transform 260ms var(--ease-out-soft), - visibility 220ms var(--ease-smooth); + opacity 200ms var(--ease-out-expo), + transform 280ms var(--ease-spring), + visibility 200ms var(--ease-out-expo); } .mobile-detail-sheet.open { diff --git a/webapp/src/styles/shell.css b/webapp/src/styles/shell.css index d5f2579..6537920 100644 --- a/webapp/src/styles/shell.css +++ b/webapp/src/styles/shell.css @@ -12,7 +12,9 @@ .topbar { @apply flex h-[58px] items-center justify-between border-b px-[18px] text-slate-900 transition; border-color: var(--line-soft); - background: rgba(244, 248, 255, 0.82); + background: rgba(244, 248, 255, 0.78); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); } .brand { @@ -182,6 +184,7 @@ .route-stage { @apply h-full min-h-0 overflow-auto; + animation: route-stage-in 220ms var(--ease-out-expo) both; } .mobile-sidebar-mask { diff --git a/webapp/src/styles/tokens.css b/webapp/src/styles/tokens.css index 3be4098..b126901 100644 --- a/webapp/src/styles/tokens.css +++ b/webapp/src/styles/tokens.css @@ -26,6 +26,10 @@ --ease-out-strong: cubic-bezier(0.22, 1, 0.36, 1); --ease-out-soft: cubic-bezier(0.24, 0.8, 0.32, 1); --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + --dur-instant: 80ms; + --dur-quick: 120ms; --dur-fast: 180ms; --dur-medium: 240ms; --dur-panel: 280ms; diff --git a/webapp/src/styles/vault.css b/webapp/src/styles/vault.css index c668253..17ccc04 100644 --- a/webapp/src/styles/vault.css +++ b/webapp/src/styles/vault.css @@ -313,8 +313,8 @@ .list-item:hover { background: #fcfdff; - border-color: rgba(148, 163, 184, 0.26); - box-shadow: 0 10px 22px rgba(15, 23, 42, 0.05); + border-color: rgba(37, 99, 235, 0.18); + box-shadow: 0 10px 22px rgba(15, 23, 42, 0.06), 0 0 0 1px rgba(37, 99, 235, 0.04); } .list-item:hover::before { @@ -470,23 +470,21 @@ } .detail-col > .card { - @apply opacity-100; - animation: none; + animation: surface-enter 280ms var(--ease-out-soft) both; } .detail-col > .card:nth-of-type(1) { animation-delay: 0ms; } -.detail-col > .card:nth-of-type(2) { animation-delay: 0ms; } -.detail-col > .card:nth-of-type(3) { animation-delay: 0ms; } -.detail-col > .card:nth-of-type(4) { animation-delay: 0ms; } -.detail-col > .card:nth-of-type(5) { animation-delay: 0ms; } +.detail-col > .card:nth-of-type(2) { animation-delay: 40ms; } +.detail-col > .card:nth-of-type(3) { animation-delay: 80ms; } +.detail-col > .card:nth-of-type(4) { animation-delay: 120ms; } +.detail-col > .card:nth-of-type(5) { animation-delay: 160ms; } .detail-switch-stage { - animation: none; + animation: fade-in 160ms var(--ease-out-soft) both; } .detail-switch-stage > .card { - @apply opacity-100; - animation: none; + animation: surface-enter 280ms var(--ease-out-soft) both; } .card h4 {