diff --git a/app/frontend/m/css/shell.css b/app/frontend/m/css/shell.css index 531a95e..7798e3f 100644 --- a/app/frontend/m/css/shell.css +++ b/app/frontend/m/css/shell.css @@ -23,6 +23,26 @@ body { color: var(--text-primary); overflow-x: hidden; -webkit-tap-highlight-color: transparent; + transition: background-color var(--motion-pop) var(--ease-press), + border-color var(--motion-pop) var(--ease-press); +} + +.m-motion-fade-in { + animation: m-fade-in var(--motion-fade) var(--ease-press) both; +} + +@keyframes m-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.m-motion-rise-in { + animation: m-rise-in var(--motion-enter) var(--ease-enter) both; +} + +@keyframes m-rise-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: none; } } .m-header { @@ -35,6 +55,8 @@ body { padding: 0 4px; background: var(--surface); border-bottom: 1px solid var(--border); + transition: background-color var(--motion-pop) var(--ease-press), + border-color var(--motion-pop) var(--ease-press); } .m-header-btn { @@ -48,10 +70,14 @@ body { background: transparent; color: var(--text-primary); cursor: pointer; + transition: transform var(--motion-press-release) var(--ease-press), + background-color var(--motion-press-release) var(--ease-press); } .m-header-btn:active { background: var(--surface-hover); + transform: scale(0.94); + transition-duration: var(--motion-press); } .m-title { @@ -63,6 +89,7 @@ body { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + transition: color var(--motion-pop) var(--ease-press); } .m-actions { @@ -135,11 +162,14 @@ body { color: var(--text-primary); cursor: pointer; -webkit-tap-highlight-color: transparent; + transition: transform var(--motion-press-release) var(--ease-press), + background-color var(--motion-press-release) var(--ease-press); } .m-grid-item:active { - background: var(--surface); - transform: scale(0.96); + background: var(--surface-hover); + transform: scale(0.94); + transition-duration: var(--motion-press); } .m-grid-icon { @@ -152,6 +182,8 @@ body { background: var(--action-soft); color: var(--action); flex: 0 0 auto; + transition: background-color var(--motion-pop) var(--ease-press), + border-color var(--motion-pop) var(--ease-press); } .m-grid-icon svg { @@ -168,6 +200,7 @@ body { white-space: nowrap; text-overflow: ellipsis; max-width: 100%; + transition: color var(--motion-pop) var(--ease-press); } .m-auth { @@ -275,6 +308,14 @@ body { font-size: var(--font-size-body); font-weight: 600; cursor: pointer; + transition: transform var(--motion-press-release) var(--ease-press), + background-color var(--motion-press-release) var(--ease-press); +} + +.m-btn-primary:active { + background: var(--action-hover); + transform: scale(0.98); + transition-duration: var(--motion-press); } .m-btn-primary:disabled { @@ -294,6 +335,8 @@ body { padding-bottom: var(--mobile-safe-bottom); background: var(--surface); border-top: 1px solid var(--border); + transition: background-color var(--motion-pop) var(--ease-press), + border-color var(--motion-pop) var(--ease-press); } .m-tabbar-item { @@ -310,10 +353,15 @@ body { color: var(--text-tertiary); cursor: pointer; -webkit-tap-highlight-color: transparent; + transition: transform var(--motion-press-release) var(--ease-press), + background-color var(--motion-press-release) var(--ease-press), + color var(--motion-pop) var(--ease-press); } .m-tabbar-item:active { background: var(--surface-hover); + transform: scale(0.94); + transition-duration: var(--motion-press), var(--motion-press), var(--motion-pop); } .m-tabbar-icon { @@ -323,6 +371,12 @@ body { width: 24px; height: 24px; flex: 0 0 auto; + transition: transform var(--motion-press-release) var(--ease-press); +} + +.m-tabbar-item:active .m-tabbar-icon { + transform: scale(0.88); + transition-duration: var(--motion-press); } .m-tabbar-label { @@ -351,6 +405,8 @@ body { border-radius: 12px; background: var(--surface); box-shadow: var(--elevation-card); + transition: background-color var(--motion-pop) var(--ease-press), + border-color var(--motion-pop) var(--ease-press); } .m-theme-row-icon { @@ -363,6 +419,8 @@ body { background: var(--action-soft); color: var(--action); flex: 0 0 auto; + transition: background-color var(--motion-pop) var(--ease-press), + border-color var(--motion-pop) var(--ease-press); } .m-theme-row-body { @@ -416,3 +474,25 @@ body { .m-theme-switch[aria-checked="true"] .m-theme-switch-thumb { transform: translateX(20px); } + +@media (prefers-reduced-motion: reduce) { + .m-motion-rise-in { + animation-name: m-fade-in; + animation-duration: 100ms; + } + + .m-motion-fade-in { + animation-duration: 100ms; + } + + .m-grid-item:active, + .m-header-btn:active, + .m-btn-primary:active, + .m-tabbar-item:active { + transform: none; + } + + .m-tabbar-item:active .m-tabbar-icon { + transform: none; + } +} diff --git a/app/frontend/m/css/tokens.css b/app/frontend/m/css/tokens.css index 9a07e5e..f710d13 100644 --- a/app/frontend/m/css/tokens.css +++ b/app/frontend/m/css/tokens.css @@ -30,6 +30,16 @@ --elevation-float: 0 12px 32px rgba(0, 0, 0, .14); --backdrop: rgba(17, 24, 39, .48); + --motion-press: 100ms; + --motion-press-release: 160ms; + --motion-fade: 120ms; + --motion-enter: 260ms; + --motion-exit: 200ms; + --motion-pop: 240ms; + --ease-enter: cubic-bezier(0.22, 1, 0.36, 1); + --ease-exit: cubic-bezier(0.4, 0, 1, 1); + --ease-press: ease-out; + --mobile-header-height: 52px; --mobile-touch-size: 44px; --mobile-safe-bottom: env(safe-area-inset-bottom, 0px); diff --git a/app/frontend/m/js/router.js b/app/frontend/m/js/router.js index 224a6ad..b9a453a 100644 --- a/app/frontend/m/js/router.js +++ b/app/frontend/m/js/router.js @@ -144,7 +144,7 @@ } function placeholderHtml(title, subtitle) { - return '
" + escapeHtml(subtitle || "该页面将在后续批次实现,返回即可继续浏览。") + "
" + @@ -172,6 +172,16 @@ "