feat: Implement responsive page layout components and CSS variables
- Added PageScaffold component for shared page layout with safe-area padding and scroll management. - Introduced ResponsiveMobileShell for mobile canvas layout with customizable backgrounds. - Created ScrollablePage component for scrollable content areas. - Updated CSS styles for coins rules screen, sidebar components, and subscription sections to use responsive design tokens. - Added viewport CSS variables provider to synchronize CSS variables with the visual viewport. - Refactored breakpoints and dimensions for better responsiveness across devices.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
.area {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--fixed-bottom-z-index, 5);
|
||||
flex: 0 0 auto;
|
||||
padding:
|
||||
var(--fixed-bottom-padding-top, 12px)
|
||||
calc(var(--page-padding-x, 20px) + var(--app-safe-right, 0px))
|
||||
calc(var(--fixed-bottom-padding-bottom, 12px) + var(--app-safe-bottom, 0px))
|
||||
calc(var(--page-padding-x, 20px) + var(--app-safe-left, 0px));
|
||||
background: var(--fixed-bottom-background, transparent);
|
||||
}
|
||||
|
||||
.area[data-elevated="true"] {
|
||||
box-shadow: 0 -14px 32px rgba(31, 21, 25, 0.08);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
|
||||
import styles from "./fixed-bottom-area.module.css";
|
||||
|
||||
export interface FixedBottomAreaProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
background?: string;
|
||||
elevated?: boolean;
|
||||
}
|
||||
|
||||
export function FixedBottomArea({
|
||||
children,
|
||||
className,
|
||||
background,
|
||||
elevated = false,
|
||||
}: FixedBottomAreaProps) {
|
||||
const style = background
|
||||
? ({
|
||||
"--fixed-bottom-background": background,
|
||||
} as CSSProperties)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[styles.area, className].filter(Boolean).join(" ")}
|
||||
data-elevated={elevated}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,10 @@
|
||||
export * from "./bottom-sheet";
|
||||
export * from "./checkbox";
|
||||
export * from "./dialog";
|
||||
export * from "./fixed-bottom-area";
|
||||
export * from "./loading-indicator";
|
||||
export * from "./mobile-shell";
|
||||
export * from "./page-scaffold";
|
||||
export * from "./responsive-mobile-shell";
|
||||
export * from "./scrollable-page";
|
||||
export * from "./settings-section";
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
/**
|
||||
* 500px 移动端 Shell(Server Component)
|
||||
*
|
||||
*
|
||||
* (`Breakpoints.mobileMaxWidth = 500.0`)。
|
||||
*
|
||||
* 集中封装,调用方仅需 `<MobileShell>{...}</MobileShell>` 即可获得统一的移动端宽度约束。
|
||||
*
|
||||
* 本组件无 hooks,可作为 Server Component 直接 SSR。
|
||||
*/
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import styles from "./mobile-shell.module.css";
|
||||
import { ResponsiveMobileShell } from "./responsive-mobile-shell";
|
||||
|
||||
export interface MobileShellProps {
|
||||
children: ReactNode;
|
||||
@@ -26,13 +16,8 @@ export function MobileShell({
|
||||
className,
|
||||
}: MobileShellProps) {
|
||||
return (
|
||||
<div className={styles.shell}>
|
||||
<div
|
||||
className={[styles.content, className].filter(Boolean).join(" ")}
|
||||
style={background ? { background } : undefined}
|
||||
>
|
||||
<ResponsiveMobileShell background={background} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</ResponsiveMobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: var(--app-viewport-height, 100dvh);
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-text-foreground, #171717);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.root[data-scrollable="true"] {
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding:
|
||||
calc(var(--page-padding-y, 18px) + var(--app-safe-top, 0px))
|
||||
calc(var(--page-padding-x, 20px) + var(--app-safe-right, 0px))
|
||||
calc(var(--page-padding-y, 18px) + var(--app-safe-bottom, 0px))
|
||||
calc(var(--page-padding-x, 20px) + var(--app-safe-left, 0px));
|
||||
}
|
||||
|
||||
.header,
|
||||
.footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.body[data-scrollable="true"] {
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-bottom: var(--app-safe-bottom, 0px);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import styles from "./page-scaffold.module.css";
|
||||
|
||||
export interface PageScaffoldProps {
|
||||
children: ReactNode;
|
||||
header?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
className?: string;
|
||||
bodyClassName?: string;
|
||||
padded?: boolean;
|
||||
scrollable?: boolean;
|
||||
bodyScrollable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared page layout primitive for mobile-first screens.
|
||||
*
|
||||
* Use it when migrating feature pages away from hand-written shell/padding
|
||||
* styles. It keeps safe-area padding and scroll ownership in one place.
|
||||
*/
|
||||
export function PageScaffold({
|
||||
children,
|
||||
header,
|
||||
footer,
|
||||
className,
|
||||
bodyClassName,
|
||||
padded = true,
|
||||
scrollable = false,
|
||||
bodyScrollable = false,
|
||||
}: PageScaffoldProps) {
|
||||
return (
|
||||
<main
|
||||
className={[styles.root, padded ? styles.padded : "", className]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
data-scrollable={scrollable}
|
||||
>
|
||||
{header ? <div className={styles.header}>{header}</div> : null}
|
||||
<div
|
||||
className={[styles.body, bodyClassName].filter(Boolean).join(" ")}
|
||||
data-scrollable={bodyScrollable}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{footer ? <div className={styles.footer}>{footer}</div> : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+14
-4
@@ -1,19 +1,29 @@
|
||||
.shell {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: var(--app-viewport-height, 100dvh);
|
||||
background: #000000; /* 显式黑,桌面端移动壳外圈为黑色 */
|
||||
justify-content: center;
|
||||
background: var(--mobile-shell-outer-background, #000000);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: var(--app-max-width, 540px);
|
||||
min-height: var(--app-viewport-height, 100dvh);
|
||||
flex-direction: column;
|
||||
background: var(--mobile-shell-background, transparent);
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
@supports not (overflow: clip) {
|
||||
.content {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
|
||||
import styles from "./responsive-mobile-shell.module.css";
|
||||
|
||||
export interface ResponsiveMobileShellProps {
|
||||
children: ReactNode;
|
||||
/** Background used inside the mobile canvas. */
|
||||
background?: string;
|
||||
/** Background used outside the mobile canvas on wider screens. */
|
||||
outerBackground?: string;
|
||||
/** Class applied to the mobile canvas. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ResponsiveMobileShell({
|
||||
children,
|
||||
background,
|
||||
outerBackground,
|
||||
className,
|
||||
}: ResponsiveMobileShellProps) {
|
||||
const style = {
|
||||
"--mobile-shell-background": background,
|
||||
"--mobile-shell-outer-background": outerBackground,
|
||||
} as CSSProperties;
|
||||
|
||||
return (
|
||||
<div className={styles.shell} style={style}>
|
||||
<div className={[styles.content, className].filter(Boolean).join(" ")}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior-y: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import styles from "./scrollable-page.module.css";
|
||||
|
||||
export interface ScrollablePageProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ScrollablePage({ children, className }: ScrollablePageProps) {
|
||||
return (
|
||||
<div className={[styles.scroll, className].filter(Boolean).join(" ")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
.screen {
|
||||
min-height: 100dvh;
|
||||
box-sizing: border-box;
|
||||
padding: 18px 20px 28px;
|
||||
padding:
|
||||
calc(var(--page-padding-y, 18px) + var(--app-safe-top, 0px))
|
||||
calc(var(--page-padding-x, 20px) + var(--app-safe-right, 0px))
|
||||
calc(var(--page-padding-y, 28px) + var(--app-safe-bottom, 0px))
|
||||
calc(var(--page-padding-x, 20px) + var(--app-safe-left, 0px));
|
||||
background:
|
||||
radial-gradient(circle at 16% 5%, rgba(255, 196, 143, 0.2), transparent 28%),
|
||||
radial-gradient(circle at 86% 8%, rgba(246, 87, 160, 0.2), transparent 26%),
|
||||
@@ -12,15 +16,15 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
gap: var(--page-section-gap, 18px);
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 22px;
|
||||
padding: var(--responsive-card-padding-lg, 22px);
|
||||
border: 1px solid rgba(246, 87, 160, 0.14);
|
||||
border-radius: 28px;
|
||||
border-radius: var(--responsive-card-radius, 28px);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.88) 0%, rgba(255, 237, 246, 0.94) 100%),
|
||||
#ffffff;
|
||||
@@ -55,7 +59,7 @@
|
||||
.eyebrow {
|
||||
margin: 16px 0 0;
|
||||
color: #f657a0;
|
||||
font-size: 12px;
|
||||
font-size: var(--responsive-micro, 12px);
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.12em;
|
||||
line-height: 1;
|
||||
@@ -65,7 +69,7 @@
|
||||
.title {
|
||||
margin: 8px 0 0;
|
||||
color: #171114;
|
||||
font-size: 30px;
|
||||
font-size: var(--responsive-page-title, 30px);
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.8px;
|
||||
line-height: 1.08;
|
||||
@@ -75,7 +79,7 @@
|
||||
max-width: 300px;
|
||||
margin: 10px 0 0;
|
||||
color: #6d5d64;
|
||||
font-size: 15px;
|
||||
font-size: var(--responsive-body, 15px);
|
||||
font-weight: 650;
|
||||
line-height: 1.55;
|
||||
}
|
||||
@@ -85,10 +89,10 @@
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
margin-top: var(--page-section-gap, 16px);
|
||||
padding: var(--responsive-card-padding, 16px);
|
||||
border: 1px solid rgba(248, 184, 62, 0.24);
|
||||
border-radius: 22px;
|
||||
border-radius: var(--responsive-card-radius-sm, 22px);
|
||||
background: rgba(255, 250, 235, 0.84);
|
||||
box-shadow: 0 12px 28px rgba(142, 93, 18, 0.08);
|
||||
}
|
||||
@@ -108,7 +112,7 @@
|
||||
.freeTitle {
|
||||
margin: 0;
|
||||
color: #2a1f14;
|
||||
font-size: 16px;
|
||||
font-size: var(--responsive-body, 16px);
|
||||
font-weight: 850;
|
||||
line-height: 1.1;
|
||||
}
|
||||
@@ -116,7 +120,7 @@
|
||||
.freeText {
|
||||
margin: 5px 0 0;
|
||||
color: #7a6040;
|
||||
font-size: 13px;
|
||||
font-size: var(--responsive-caption, 13px);
|
||||
font-weight: 650;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -125,7 +129,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
margin-top: var(--page-section-gap, 16px);
|
||||
}
|
||||
|
||||
.ruleCard {
|
||||
@@ -133,9 +137,9 @@
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
padding: var(--responsive-card-padding, 14px);
|
||||
border: 1px solid rgba(25, 19, 22, 0.06);
|
||||
border-radius: 20px;
|
||||
border-radius: var(--responsive-card-radius-sm, 20px);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 10px 26px rgba(55, 36, 44, 0.06);
|
||||
animation: cardIn 0.42s ease both;
|
||||
@@ -165,7 +169,7 @@
|
||||
.ruleTitle {
|
||||
margin: 0;
|
||||
color: #21191d;
|
||||
font-size: 16px;
|
||||
font-size: var(--responsive-body, 16px);
|
||||
font-weight: 850;
|
||||
line-height: 1.15;
|
||||
}
|
||||
@@ -177,7 +181,7 @@
|
||||
border-radius: 999px;
|
||||
background: #edf4ff;
|
||||
color: #2e6eea;
|
||||
font-size: 12px;
|
||||
font-size: var(--responsive-micro, 12px);
|
||||
font-weight: 780;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -185,7 +189,7 @@
|
||||
.cost {
|
||||
margin: 0;
|
||||
color: #171114;
|
||||
font-size: 15px;
|
||||
font-size: var(--responsive-body, 15px);
|
||||
font-weight: 900;
|
||||
line-height: 1.15;
|
||||
text-align: right;
|
||||
|
||||
@@ -24,4 +24,11 @@ body {
|
||||
background: var(--color-page-background);
|
||||
color: var(--color-text-foreground);
|
||||
font-family: var(--font-system);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
overflow: hidden;
|
||||
min-height: 100dvh;
|
||||
box-sizing: border-box;
|
||||
padding: 0 var(--page-padding-x, 28px) 28px;
|
||||
padding:
|
||||
var(--app-safe-top, 0px)
|
||||
calc(var(--page-padding-x, 28px) + var(--app-safe-right, 0px))
|
||||
calc(var(--page-padding-y, 20px) + var(--app-safe-bottom, 0px))
|
||||
calc(var(--page-padding-x, 28px) + var(--app-safe-left, 0px));
|
||||
background:
|
||||
radial-gradient(circle at 18% 6%, rgba(255, 206, 160, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 92% 0%, rgba(246, 87, 160, 0.22), transparent 30%),
|
||||
@@ -49,13 +53,13 @@
|
||||
}
|
||||
|
||||
.topBar {
|
||||
margin: 18px 0 -2px;
|
||||
margin: var(--page-padding-y, 18px) 0 -2px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 18px;
|
||||
padding: var(--responsive-card-padding, 18px);
|
||||
border: 1px solid rgba(246, 87, 160, 0.12);
|
||||
border-radius: 26px;
|
||||
border-radius: var(--responsive-card-radius, 26px);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.76) 0%, rgba(255, 239, 246, 0.84) 100%),
|
||||
#ffffff;
|
||||
@@ -66,7 +70,7 @@
|
||||
.kicker {
|
||||
margin: 0;
|
||||
color: #f657a0;
|
||||
font-size: 12px;
|
||||
font-size: var(--responsive-micro, 12px);
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1;
|
||||
@@ -76,7 +80,7 @@
|
||||
.title {
|
||||
margin: 8px 0 0;
|
||||
color: #171114;
|
||||
font-size: 34px;
|
||||
font-size: var(--responsive-display-title, 34px);
|
||||
font-weight: 920;
|
||||
letter-spacing: -1px;
|
||||
line-height: 1;
|
||||
@@ -86,15 +90,15 @@
|
||||
max-width: 320px;
|
||||
margin: 10px 0 0;
|
||||
color: #75636a;
|
||||
font-size: 14px;
|
||||
font-size: var(--responsive-caption, 14px);
|
||||
font-weight: 650;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.userSlot {
|
||||
padding: 18px;
|
||||
padding: var(--responsive-card-padding, 18px);
|
||||
border: 1px solid rgba(25, 19, 22, 0.06);
|
||||
border-radius: 26px;
|
||||
border-radius: var(--responsive-card-radius, 26px);
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
box-shadow: 0 16px 40px rgba(55, 36, 44, 0.08);
|
||||
backdrop-filter: blur(18px);
|
||||
@@ -113,7 +117,7 @@
|
||||
margin: 0 0 9px;
|
||||
padding: 0 4px;
|
||||
color: #817076;
|
||||
font-size: 13px;
|
||||
font-size: var(--responsive-caption, 13px);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
@@ -125,9 +129,9 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
padding: var(--responsive-card-padding, 16px);
|
||||
border: 1px solid rgba(25, 19, 22, 0.06);
|
||||
border-radius: 22px;
|
||||
border-radius: var(--responsive-card-radius-sm, 22px);
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
color: #171114;
|
||||
cursor: pointer;
|
||||
@@ -173,14 +177,14 @@
|
||||
|
||||
.logoutTitle {
|
||||
color: #ef4d64;
|
||||
font-size: 16px;
|
||||
font-size: var(--responsive-body, 16px);
|
||||
font-weight: 760;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.logoutSubtitle {
|
||||
color: #817076;
|
||||
font-size: 13px;
|
||||
font-size: var(--responsive-caption, 13px);
|
||||
font-weight: 620;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 18px;
|
||||
gap: var(--page-section-gap, 18px);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 18px;
|
||||
padding: var(--responsive-card-padding, 18px);
|
||||
border: 1px solid rgba(246, 87, 160, 0.16);
|
||||
border-radius: 24px;
|
||||
border-radius: var(--responsive-card-radius, 24px);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 239, 246, 0.94) 100%),
|
||||
#ffffff;
|
||||
@@ -48,7 +48,7 @@
|
||||
.title {
|
||||
margin: 0;
|
||||
color: #191316;
|
||||
font-size: 18px;
|
||||
font-size: var(--responsive-card-title, 18px);
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
}
|
||||
@@ -60,14 +60,14 @@
|
||||
}
|
||||
|
||||
.balanceValue {
|
||||
font-size: 22px;
|
||||
font-size: var(--responsive-page-title, 22px);
|
||||
font-weight: 850;
|
||||
letter-spacing: -0.4px;
|
||||
}
|
||||
|
||||
.balanceUnit {
|
||||
color: #4f4449;
|
||||
font-size: 16px;
|
||||
font-size: var(--responsive-body, 16px);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
color: #2e6eea;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 15px;
|
||||
font-size: var(--responsive-body, 15px);
|
||||
font-weight: 760;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
@@ -110,7 +110,7 @@
|
||||
.topUpButton {
|
||||
display: inline-flex;
|
||||
min-width: 92px;
|
||||
min-height: 44px;
|
||||
min-height: var(--responsive-control-height, 44px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 18px;
|
||||
@@ -118,7 +118,7 @@
|
||||
border-radius: 18px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 15px;
|
||||
font-size: var(--responsive-body, 15px);
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.1px;
|
||||
transition: box-shadow 0.18s ease, opacity 0.18s ease, transform 0.18s ease,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.section {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(17, 17, 17, 0.12);
|
||||
border-radius: 28px;
|
||||
border-radius: var(--responsive-card-radius, 28px);
|
||||
background: #fff8df;
|
||||
box-shadow: 0 16px 42px rgba(220, 162, 36, 0.12);
|
||||
backdrop-filter: blur(18px);
|
||||
@@ -9,7 +9,10 @@
|
||||
|
||||
.heading {
|
||||
position: relative;
|
||||
padding: 18px 18px 22px;
|
||||
padding:
|
||||
var(--responsive-card-padding, 18px)
|
||||
var(--responsive-card-padding, 18px)
|
||||
var(--responsive-card-padding-lg, 22px);
|
||||
background:
|
||||
linear-gradient(135deg, #fffdf2 0%, #fff3c8 58%, #ffe8a6 100%),
|
||||
#fff3c8;
|
||||
@@ -18,7 +21,7 @@
|
||||
.title {
|
||||
margin: 0;
|
||||
color: #181014;
|
||||
font-size: 22px;
|
||||
font-size: var(--responsive-page-title, 22px);
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -38,8 +41,11 @@
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 18px 12px 14px;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
padding:
|
||||
var(--responsive-card-padding, 18px)
|
||||
clamp(10px, 2.222vw, 12px)
|
||||
clamp(12px, 2.593vw, 14px);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -48,10 +54,10 @@
|
||||
grid-template-columns: 48px minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
min-height: var(--responsive-control-height, 48px);
|
||||
padding: 8px 12px 8px 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 18px;
|
||||
border-radius: var(--responsive-card-radius-sm, 18px);
|
||||
background: #ffffff;
|
||||
color: #53484d;
|
||||
cursor: pointer;
|
||||
@@ -97,7 +103,7 @@
|
||||
|
||||
.coins {
|
||||
min-width: 0;
|
||||
font-size: 16px;
|
||||
font-size: var(--responsive-body, 16px);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -108,7 +114,7 @@
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, #ff5fae 0%, #ff8a34 100%);
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-size: clamp(10px, 2.593vw, 14px);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
@@ -125,7 +131,7 @@
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 16px;
|
||||
font-size: var(--responsive-body, 16px);
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
@@ -134,7 +140,7 @@
|
||||
.originalPrice {
|
||||
margin-top: 3px;
|
||||
color: #9c8b91;
|
||||
font-size: 12px;
|
||||
font-size: var(--responsive-micro, 12px);
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
text-decoration: line-through;
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
background:
|
||||
radial-gradient(circle at 8% 4%, rgba(255, 255, 255, 0.95) 0 90px, transparent 160px),
|
||||
linear-gradient(180deg, #fff9fb 0%, #fcf3f4 52%, #fffefe 100%);
|
||||
padding: 18px 20px 10px;
|
||||
padding:
|
||||
calc(var(--page-padding-y, 18px) + var(--app-safe-top, 0px))
|
||||
calc(var(--page-padding-x, 20px) + var(--app-safe-right, 0px))
|
||||
calc(10px + var(--app-safe-bottom, 0px))
|
||||
calc(var(--page-padding-x, 20px) + var(--app-safe-left, 0px));
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -21,11 +25,11 @@
|
||||
.firstRechargeBanner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
gap: var(--spacing-md, 12px);
|
||||
margin-top: var(--page-section-gap, 14px);
|
||||
padding: clamp(10px, 2.222vw, 12px) clamp(12px, 2.593vw, 14px);
|
||||
border: 1px solid rgba(255, 95, 174, 0.28);
|
||||
border-radius: 22px;
|
||||
border-radius: var(--responsive-card-radius-sm, 22px);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.96) 0%, rgba(255, 239, 248, 0.94) 100%),
|
||||
#ffffff;
|
||||
@@ -38,7 +42,7 @@
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, #ff5fae 0%, #ff8a34 100%);
|
||||
color: #ffffff;
|
||||
font-size: 20;
|
||||
font-size: clamp(20px, 5.185vw, 28px);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
box-shadow: 0 10px 22px rgba(255, 95, 174, 0.25);
|
||||
@@ -51,7 +55,7 @@
|
||||
.firstRechargeTitle {
|
||||
margin: 0;
|
||||
color: #181014;
|
||||
font-size: 17px;
|
||||
font-size: var(--responsive-card-title, 17px);
|
||||
font-weight: 900;
|
||||
line-height: 1.15;
|
||||
}
|
||||
@@ -59,7 +63,7 @@
|
||||
.firstRechargeSubtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #75656d;
|
||||
font-size: 12px;
|
||||
font-size: var(--responsive-micro, 12px);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
@@ -67,8 +71,8 @@
|
||||
.offerStack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
margin-top: 14px;
|
||||
gap: var(--page-section-gap, 18px);
|
||||
margin-top: var(--page-section-gap, 14px);
|
||||
}
|
||||
|
||||
.userSlot {
|
||||
@@ -92,7 +96,7 @@
|
||||
margin: 0;
|
||||
padding: var(--spacing-md) var(--spacing-xs);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--responsive-caption, var(--font-size-sm));
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -100,7 +104,7 @@
|
||||
.autoRenewCaption {
|
||||
margin-top: var(--spacing-xxl);
|
||||
padding: 0 var(--spacing-xs);
|
||||
font-size: var(--font-size-lg);
|
||||
font-size: var(--responsive-body, var(--font-size-lg));
|
||||
color: #3c3b3b;
|
||||
}
|
||||
|
||||
@@ -113,11 +117,11 @@
|
||||
}
|
||||
|
||||
.paymentMethodSlot {
|
||||
margin-top: 22px;
|
||||
margin-top: var(--page-section-gap-lg, 22px);
|
||||
}
|
||||
|
||||
.ctaSlot {
|
||||
margin-top: 22px;
|
||||
margin-top: var(--page-section-gap-lg, 22px);
|
||||
}
|
||||
|
||||
.agreementSlot {
|
||||
@@ -126,7 +130,7 @@
|
||||
}
|
||||
|
||||
.agreementLabel {
|
||||
font-size: 12px;
|
||||
font-size: var(--responsive-micro, 12px);
|
||||
}
|
||||
|
||||
.agreementLink {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.section {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 71, 166, 0.28);
|
||||
border-radius: 28px;
|
||||
border-radius: var(--responsive-card-radius, 28px);
|
||||
background: #ffe2f0;
|
||||
box-shadow: 0 18px 45px rgba(255, 71, 166, 0.22);
|
||||
backdrop-filter: blur(18px);
|
||||
@@ -9,7 +9,10 @@
|
||||
|
||||
.summary {
|
||||
position: relative;
|
||||
padding: 18px 18px 22px;
|
||||
padding:
|
||||
var(--responsive-card-padding, 18px)
|
||||
var(--responsive-card-padding, 18px)
|
||||
var(--responsive-card-padding-lg, 22px);
|
||||
background:
|
||||
linear-gradient(135deg, #ff9bd1 0%, #ff61ad 56%, #f657a0 100%),
|
||||
#ff61ad;
|
||||
@@ -25,20 +28,20 @@
|
||||
|
||||
.titleText {
|
||||
color: #181014;
|
||||
font-size: 20px;
|
||||
font-size: var(--responsive-section-title, 20px);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.renewText {
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 13px;
|
||||
font-size: var(--responsive-caption, 13px);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 8px 0 0;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 15px;
|
||||
font-size: var(--responsive-body, 15px);
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
@@ -58,21 +61,24 @@
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 9px;
|
||||
padding: 18px 10px 12px;
|
||||
gap: clamp(7px, 1.667vw, 9px);
|
||||
padding:
|
||||
var(--responsive-card-padding, 18px)
|
||||
clamp(8px, 1.852vw, 10px)
|
||||
clamp(10px, 2.222vw, 12px);
|
||||
}
|
||||
|
||||
.planCard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 128px;
|
||||
min-height: clamp(116px, 23.704vw, 128px);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 6px 11px;
|
||||
padding: clamp(10px, 2.222vw, 12px) 6px clamp(9px, 2.037vw, 11px);
|
||||
border: 1px solid rgba(246, 87, 160, 0.08);
|
||||
border-radius: 20px;
|
||||
border-radius: var(--responsive-card-radius-sm, 20px);
|
||||
background: #ffffff;
|
||||
color: #181014;
|
||||
cursor: pointer;
|
||||
@@ -111,7 +117,7 @@
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, #ff5fae 0%, #ff8a34 100%);
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-size: clamp(12px, 2.963vw, 16px);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
@@ -121,7 +127,7 @@
|
||||
|
||||
.planTitle {
|
||||
margin-top: 20px;
|
||||
font-size: 19px;
|
||||
font-size: clamp(16px, 3.519vw, 19px);
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
white-space: nowrap;
|
||||
@@ -136,7 +142,7 @@
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 30px;
|
||||
font-size: var(--font-responsive-number-lg, 30px);
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.8px;
|
||||
}
|
||||
@@ -144,7 +150,7 @@
|
||||
.currency {
|
||||
margin-bottom: 3px;
|
||||
color: #8b717a;
|
||||
font-size: 12px;
|
||||
font-size: var(--responsive-micro, 12px);
|
||||
font-weight: 700;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
@@ -152,7 +158,7 @@
|
||||
.originalPrice {
|
||||
margin-top: 10px;
|
||||
color: #9c8b91;
|
||||
font-size: 14px;
|
||||
font-size: var(--responsive-caption, 14px);
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
text-decoration: line-through;
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
*
|
||||
*
|
||||
*
|
||||
* 与 `src/design/breakpoints.css` 中的 CSS 变量保持一致:
|
||||
* 与 `src/tokens/breakpoints.css` 中的 CSS 变量保持一致:
|
||||
* - 客户端运行时检查(matchMedia)通过 `useBreakpoint` hook
|
||||
* - 服务端 / 客户端 CSS 通过 `src/design/breakpoints.css` 的 media query
|
||||
* - 服务端 / 客户端 CSS 通过 `src/tokens/breakpoints.css` 的 media query
|
||||
*/
|
||||
export const Breakpoints = {
|
||||
/** 手机设备最大宽度 */
|
||||
mobileMaxWidth: 500,
|
||||
/** 手机设备最大宽度。 */
|
||||
mobileMaxWidth: 599.98,
|
||||
/** 移动端画布最大宽度,来自 1080px @2x 设计稿。 */
|
||||
shellMaxWidth: 540,
|
||||
/** 平板设备最小宽度 */
|
||||
tabletMinWidth: 600,
|
||||
/** 平板设备最大宽度 */
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
*/
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
import type { DeviceSize } from "@/core/breakpoints";
|
||||
import { Breakpoints, type DeviceSize } from "@/core/breakpoints";
|
||||
|
||||
const QUERIES = {
|
||||
mobile: "(max-width: 599.98px)",
|
||||
tablet: "(min-width: 600px) and (max-width: 1023.98px)",
|
||||
desktop: "(min-width: 1024px)",
|
||||
mobile: `(max-width: ${Breakpoints.mobileMaxWidth}px)`,
|
||||
tablet: `(min-width: ${Breakpoints.tabletMinWidth}px) and (max-width: ${Breakpoints.tabletMaxWidth}px)`,
|
||||
desktop: `(min-width: ${Breakpoints.desktopMinWidth}px)`,
|
||||
} as const satisfies Record<DeviceSize, string>;
|
||||
|
||||
const subscribe = (cb: () => void): (() => void) => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ViewportCssVarsProvider } from "@/providers/viewport-css-vars-provider";
|
||||
import { AuthProvider } from "@/stores/auth/auth-context";
|
||||
import { AuthStatusChecker } from "@/stores/auth/auth-status-checker";
|
||||
import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync";
|
||||
@@ -34,6 +35,7 @@ export interface RootProvidersProps {
|
||||
|
||||
export function RootProviders({ children }: RootProvidersProps) {
|
||||
return (
|
||||
<ViewportCssVarsProvider>
|
||||
<AuthProvider>
|
||||
{/* AuthStatusChecker 必须在 AuthProvider 内部(依赖 useAuthDispatch),
|
||||
* 必须在 UserProvider/ChatProvider 外部(它们可能在 init 完成前就 mount) */}
|
||||
@@ -52,5 +54,6 @@ export function RootProviders({ children }: RootProvidersProps) {
|
||||
</PaymentProvider>
|
||||
</UserProvider>
|
||||
</AuthProvider>
|
||||
</ViewportCssVarsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
|
||||
const CSS_VAR_VIEWPORT_HEIGHT = "--app-viewport-height";
|
||||
const CSS_VAR_VISIBLE_HEIGHT = "--app-visible-height";
|
||||
const CSS_VAR_VISIBLE_OFFSET_TOP = "--app-visible-offset-top";
|
||||
const CSS_VAR_SAFE_TOP = "--app-safe-top";
|
||||
const CSS_VAR_SAFE_RIGHT = "--app-safe-right";
|
||||
const CSS_VAR_SAFE_BOTTOM = "--app-safe-bottom";
|
||||
const CSS_VAR_SAFE_LEFT = "--app-safe-left";
|
||||
|
||||
export interface ViewportCssVarsProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps viewport-related CSS variables in sync with the real visual viewport.
|
||||
*
|
||||
* CSS already has safe defaults (`100dvh` and `env(safe-area-inset-*)`). This
|
||||
* client provider improves Safari/PWA/WebView behavior after hydration without
|
||||
* pushing viewport measurements into React state.
|
||||
*/
|
||||
export function ViewportCssVarsProvider({
|
||||
children,
|
||||
}: ViewportCssVarsProviderProps) {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
let frameId: number | null = null;
|
||||
|
||||
const applyVars = () => {
|
||||
frameId = null;
|
||||
const root = document.documentElement;
|
||||
const viewport = window.visualViewport;
|
||||
const layoutHeight = window.innerHeight;
|
||||
const layoutWidth = window.innerWidth;
|
||||
const visibleHeight = viewport?.height ?? layoutHeight;
|
||||
const visibleWidth = viewport?.width ?? layoutWidth;
|
||||
const visibleOffsetTop = viewport?.offsetTop ?? 0;
|
||||
|
||||
root.style.setProperty(CSS_VAR_VIEWPORT_HEIGHT, `${layoutHeight}px`);
|
||||
root.style.setProperty(CSS_VAR_VISIBLE_HEIGHT, `${visibleHeight}px`);
|
||||
root.style.setProperty(
|
||||
CSS_VAR_VISIBLE_OFFSET_TOP,
|
||||
`${visibleOffsetTop}px`,
|
||||
);
|
||||
root.style.setProperty(CSS_VAR_SAFE_TOP, "env(safe-area-inset-top, 0px)");
|
||||
root.style.setProperty(
|
||||
CSS_VAR_SAFE_RIGHT,
|
||||
"env(safe-area-inset-right, 0px)",
|
||||
);
|
||||
root.style.setProperty(
|
||||
CSS_VAR_SAFE_BOTTOM,
|
||||
"env(safe-area-inset-bottom, 0px)",
|
||||
);
|
||||
root.style.setProperty(
|
||||
CSS_VAR_SAFE_LEFT,
|
||||
"env(safe-area-inset-left, 0px)",
|
||||
);
|
||||
root.dataset.viewportWidth = String(Math.round(visibleWidth));
|
||||
root.dataset.viewportHeight = String(Math.round(visibleHeight));
|
||||
};
|
||||
|
||||
const scheduleApply = () => {
|
||||
if (frameId != null) return;
|
||||
frameId = window.requestAnimationFrame(applyVars);
|
||||
};
|
||||
|
||||
applyVars();
|
||||
|
||||
window.addEventListener("resize", scheduleApply);
|
||||
window.addEventListener("orientationchange", scheduleApply);
|
||||
window.visualViewport?.addEventListener("resize", scheduleApply);
|
||||
window.visualViewport?.addEventListener("scroll", scheduleApply);
|
||||
|
||||
return () => {
|
||||
if (frameId != null) window.cancelAnimationFrame(frameId);
|
||||
window.removeEventListener("resize", scheduleApply);
|
||||
window.removeEventListener("orientationchange", scheduleApply);
|
||||
window.visualViewport?.removeEventListener("resize", scheduleApply);
|
||||
window.visualViewport?.removeEventListener("scroll", scheduleApply);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
:root {
|
||||
/* 按钮 */
|
||||
--button-height: 48px;
|
||||
--button-height-lg: 56px;
|
||||
--button-height: var(--responsive-control-height, 48px);
|
||||
--button-height-lg: clamp(50px, 10.37vw, 56px);
|
||||
|
||||
/* Splash Facebook 登录按钮 */
|
||||
--splash-facebook-button-height: 56px;
|
||||
@@ -17,8 +17,8 @@
|
||||
--pwa-button-height: 44px;
|
||||
|
||||
/* 对话框 */
|
||||
--dialog-max-width: 360px;
|
||||
--pwa-install-dialog-max-width: 360px;
|
||||
--dialog-max-width: min(calc(100vw - 40px), 380px);
|
||||
--pwa-install-dialog-max-width: min(calc(100vw - 40px), 380px);
|
||||
--pwa-install-container-size: 64px;
|
||||
|
||||
/* 列表项 */
|
||||
@@ -26,12 +26,12 @@
|
||||
|
||||
/* 消息气泡 */
|
||||
--bubble-min-width: 60px;
|
||||
--bubble-max-width: 280px;
|
||||
--bubble-max-width: min(75vw, 280px);
|
||||
--bubble-min-height: 36px;
|
||||
|
||||
/* 聊天媒体 */
|
||||
--chat-media-max-width: 240px;
|
||||
--chat-media-max-height: 240px;
|
||||
--chat-media-max-width: min(64vw, 260px);
|
||||
--chat-media-max-height: min(64vw, 260px);
|
||||
|
||||
/* ==================== Auth 页尺寸(与 Dart AppDimensions / Spacing 对齐) ==================== */
|
||||
/* 输入框 / 主按钮高度(46px = Dart AppDimensions.height46) */
|
||||
@@ -41,7 +41,7 @@
|
||||
--auth-social-button-height: 40px;
|
||||
|
||||
/* 主按钮固定宽度(Dart AppDimensions.buttonWidth = 334) */
|
||||
--auth-primary-button-width: 334px;
|
||||
--auth-primary-button-width: min(100%, 334px);
|
||||
|
||||
/* Logo 高度(120px) */
|
||||
--auth-logo-height: 120px;
|
||||
|
||||
@@ -4,15 +4,41 @@
|
||||
* 设计稿基准:1080x1920 @2x,因此 CSS 设计宽度为 540px。
|
||||
* 原则:
|
||||
* - 桌面调试最大宽度使用 --shell-max-width / --app-max-width。
|
||||
* - 组件尺寸使用 clamp(min, design-ratio vw, design-value),在 360-540px
|
||||
* - 组件尺寸使用 clamp(min, design-ratio vw, design-value),在 320-540px
|
||||
* 之间平滑缩放,并避免小屏过度压缩。
|
||||
* - 业务 CSS 优先使用语义 token,避免页面散落硬编码尺寸。
|
||||
*/
|
||||
:root {
|
||||
--app-design-width: 540px;
|
||||
--app-max-width: var(--shell-max-width, 540px);
|
||||
--app-viewport-height: 100dvh;
|
||||
--app-visible-height: 100dvh;
|
||||
--app-safe-top: env(safe-area-inset-top, 0px);
|
||||
--app-safe-right: env(safe-area-inset-right, 0px);
|
||||
--app-safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--app-safe-left: env(safe-area-inset-left, 0px);
|
||||
|
||||
/* 56px @2x = 28px;小屏保留至少 22px,避免内容贴边。 */
|
||||
--page-padding-x: clamp(22px, 5.185vw, 28px);
|
||||
--page-padding-x: clamp(18px, 5.185vw, 28px);
|
||||
--page-padding-y: clamp(14px, 3.333vw, 20px);
|
||||
--page-section-gap: clamp(12px, 3.333vw, 18px);
|
||||
--page-section-gap-lg: clamp(16px, 4.444vw, 24px);
|
||||
|
||||
/* 页面级语义尺寸 */
|
||||
--responsive-display-title: clamp(28px, 6.296vw, 34px);
|
||||
--responsive-page-title: clamp(22px, 5.185vw, 28px);
|
||||
--responsive-section-title: clamp(17px, 3.704vw, 20px);
|
||||
--responsive-card-title: clamp(16px, 3.333vw, 18px);
|
||||
--responsive-body: clamp(14px, 2.963vw, 16px);
|
||||
--responsive-caption: clamp(12px, 2.407vw, 13px);
|
||||
--responsive-micro: clamp(10px, 2.222vw, 12px);
|
||||
|
||||
--responsive-card-radius: clamp(20px, 5.185vw, 28px);
|
||||
--responsive-card-radius-sm: clamp(18px, 4.074vw, 22px);
|
||||
--responsive-card-padding: clamp(14px, 3.333vw, 18px);
|
||||
--responsive-card-padding-lg: clamp(18px, 4.074vw, 22px);
|
||||
--responsive-control-height: clamp(44px, 8.889vw, 48px);
|
||||
--responsive-icon-button-size: clamp(38px, 8.148vw, 44px);
|
||||
|
||||
/* Sidebar 专用响应式尺寸 */
|
||||
--sidebar-user-padding-y: clamp(18px, 3.704vw, 20px);
|
||||
|
||||
Reference in New Issue
Block a user