diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 12c1e68e..e606e9d1 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -274,8 +274,9 @@ button:disabled { cursor: default; opacity: .45; } @media (max-width: 767px) { .brand-row { padding: 0 4px; } .brand-mark { width: 26px; height: 26px; } - .session-nav { overflow: hidden; } + .session-nav { overflow: visible; } .session-list { overflow-x: clip; } + [data-sidebar="content"] { -webkit-overflow-scrolling: touch; } .chat-header > div { min-width: 0; flex: 1; } .chat-header strong { max-width: 100%; } .onboarding-card { padding: 16px; } @@ -294,8 +295,7 @@ button:disabled { cursor: default; opacity: .45; } .profile-grid, .location-grid, .code-form { grid-template-columns: 1fr; } .code-form .note-field { grid-column: auto; } .code-form .button-primary { width: 100%; } - .auth-page { padding: 16px; } - .auth-brand { margin-bottom: 32px; } + .auth-brand { margin-bottom: var(--space-6); } } @media (prefers-reduced-motion: reduce) { @@ -825,8 +825,8 @@ button:disabled { cursor: default; opacity: .45; } .synastry-history-item { display: grid; gap: 3px; padding: var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); text-align: left; } .synastry-history-item small { color: var(--color-ink-secondary); font-size: var(--type-caption); } -input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input), select:not([class^="ant-"]):not([class*=" ant-"]) { width: 100%; min-height: 44px; padding: 0 12px; border: 1px solid var(--color-border-strong); color: var(--color-ink); border-color: var(--color-border-strong); border-radius: var(--radius-md); background: var(--color-canvas); font-size: 14px; } -input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled, select:not([class^="ant-"]):not([class*=" ant-"]):disabled { color: var(--color-ink-tertiary); background: var(--color-canvas-muted); } +input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input), select:not([class^="ant-"]):not([class*=" ant-"]) { width: 100%; min-height: 44px; padding: 0 12px; border: 1px solid var(--color-border-strong); color: var(--color-ink); border-color: var(--color-border-strong); border-radius: var(--radius-md); background: var(--color-canvas); font-size: 14px; } +input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled, select:not([class^="ant-"]):not([class*=" ant-"]):disabled { color: var(--color-ink-tertiary); background: var(--color-canvas-muted); } .button-primary, .button-secondary { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0 15px; border: 1px solid var(--color-action); cursor: pointer; text-decoration: none; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; border-radius: var(--radius-md); font-size: 14px; font-weight: 500; } .button-primary { border-color: var(--color-action); background: var(--color-action); color: var(--color-on-dark); } .button-secondary { border-color: var(--color-border-strong); background: var(--color-canvas); color: var(--color-ink); } @@ -925,17 +925,19 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled .message-list { width: 100%; padding: var(--space-5) var(--space-4) var(--space-12); } .message-content { max-width: 88%; } .composer-wrap { padding: var(--space-2) var(--space-3) max(var(--space-3), env(safe-area-inset-bottom)); } + :root { --composer-reserve: 116px; } + .location-combobox-results { position: static; top: auto; max-height: min(240px, 42dvh); margin-top: var(--space-2); box-shadow: none; } .account-modal { max-height: calc(100dvh - var(--space-8)); padding: var(--space-6); } .profile-modal .account-modal-header { margin: calc(var(--space-6) * -1) calc(var(--space-6) * -1) var(--space-4); padding: var(--space-6) var(--space-6) var(--space-4); } .avatar-editor { grid-template-columns: 72px minmax(0, 1fr); gap: var(--space-4); } .avatar-editor > .user-avatar { width: 72px !important; height: 72px !important; } .avatar-palette-list { grid-template-columns: repeat(2, minmax(64px, 1fr)); } - .auth-page { padding: 0; } - .auth-shell { min-height: 100dvh; grid-template-columns: 1fr; grid-template-rows: auto 1fr; border-radius: 0; box-shadow: none; } - .auth-story { min-height: 248px; padding: var(--space-8) var(--space-6); } + .auth-page { height: 100dvh; overflow-x: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 0; } + .auth-shell { min-height: 100%; overflow: visible; align-content: start; grid-template-columns: 1fr; grid-template-rows: auto auto; border-radius: 0; box-shadow: none; } + .auth-story { min-height: 0; justify-content: flex-start; gap: var(--space-3); padding: var(--space-6) var(--space-6) var(--space-5); } .auth-story h2 { max-width: 560px; margin: var(--space-3) 0 0; font-size: var(--type-display-sm); } .auth-story > div > p, .auth-footnote { display: none; } - .auth-panel { padding: var(--space-8) var(--space-6); } + .auth-panel { justify-content: flex-start; padding: var(--space-8) var(--space-6) max(var(--space-8), env(safe-area-inset-bottom)); } .auth-panel h1 { font-size: var(--type-display-md); } .admin-header { padding: 0 var(--space-4); } .admin-scroll { padding: var(--space-6) var(--space-4) var(--space-12); } @@ -959,15 +961,20 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled .paywall-footer > a, .paywall-footer > button { width: 100%; } .dialog-actions { flex-direction: column-reverse; } .dialog-actions > button { width: 100%; } - .auth-story { min-height: 220px; padding: var(--space-6) var(--space-5); } - .auth-story h2 { font-size: var(--type-display-sm); } - .auth-panel { padding: var(--space-8) var(--space-5); } + .auth-story { min-height: 0; padding: var(--space-5); } + .auth-story h2 { font-size: var(--type-title-lg); } + .auth-panel { padding: var(--space-6) var(--space-5) max(var(--space-6), env(safe-area-inset-bottom)); } .auth-panel h1 { font-size: var(--type-display-sm); } .admin-section { padding: var(--space-5); } } .session-delete-overlay { z-index: 100; } .session-delete-confirmation p { margin: 0; color: var(--color-ink-secondary); } +@media (max-width: 767px) and (max-height: 640px) { + .auth-story { display: none; } + .auth-brand { display: flex; margin-bottom: var(--space-6); } +} + @media (max-width: 430px) { [data-sonner-toaster] { right: var(--space-4) !important; left: var(--space-4) !important; width: auto !important; } .conversational-rectification { width: 100%; max-width: 100%; gap: var(--space-4); overflow-x: clip; } @@ -1571,18 +1578,36 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled .onboarding-card-heading b { font-size: clamp(22px, 2.6vw, 30px); letter-spacing: -.03em; } .onboarding-card-heading small { max-width: 260px; color: var(--color-ink-secondary); line-height: 1.55; font-size: var(--type-caption); text-wrap: pretty; } .birth-time-intake { gap: var(--space-6); } +.conversation.is-onboarding-form { align-content: start; padding-bottom: var(--space-8); } +.conversation.is-onboarding-form .welcome { padding-top: var(--space-6); padding-bottom: var(--space-8); } +.conversation.is-onboarding-form .welcome > .onboarding-message:first-child { padding-bottom: var(--space-3); } +.conversation.is-onboarding-form .welcome > .onboarding-message:first-child .message-bubble p { + max-width: 680px; + font-size: var(--type-title-md); + letter-spacing: -.2px; + line-height: 1.4; +} .birth-time-source-fieldset { padding-top: var(--space-5); border-top: 1px solid var(--color-border); } -.birth-time-source-fieldset legend { margin-bottom: 5px; color: var(--color-ink); font-size: var(--type-title-sm); font-weight: 700; } +.birth-time-source-fieldset legend { + display: block; + float: none; + width: 100%; + margin: 0 0 5px; + padding: 0; + color: var(--color-ink); + font-size: var(--type-title-sm); + font-weight: 700; +} .birth-time-source-intro { margin: 0 0 var(--space-3); color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.55; } -.birth-time-source-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); } -.birth-time-source-option { min-height: 118px; grid-template-columns: 20px minmax(0, 1fr); align-items: start; gap: var(--space-3); padding: 18px; border-color: var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); transition: border-color 160ms ease-out, background-color 160ms ease-out; } +.birth-time-source-list { grid-template-columns: 1fr; gap: var(--space-2); } +.birth-time-source-option { min-height: 64px; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-color: var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); transition: border-color 160ms ease-out, background-color 160ms ease-out; } .birth-time-source-option:hover { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); } .birth-time-source-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); box-shadow: inset 0 0 0 1px var(--color-action); } -.birth-time-source-option input { appearance: none; position: relative; width: 16px; min-width: 16px; height: 16px; min-height: 16px; margin: 3px 0 0; border: 1.5px solid var(--color-border-strong); border-radius: 50%; background: var(--color-canvas); } +.birth-time-source-option input { appearance: none; align-self: center; justify-self: center; position: static; box-sizing: border-box; width: 16px; min-width: 16px; max-width: 16px; height: 16px; min-height: 16px; max-height: 16px; margin: 0; overflow: visible; clip: auto; clip-path: none; border: 1.5px solid var(--color-border-strong); border-radius: 50%; background: var(--color-canvas); } .birth-time-source-option input:checked { border-color: var(--color-action); background: var(--color-action); box-shadow: inset 0 0 0 4px var(--color-action-soft); } -.birth-time-source-option > span { gap: 7px; } -.birth-time-source-option b { font-size: var(--type-title-sm); line-height: 1.3; } -.birth-time-source-option small { font-size: var(--type-body-sm); line-height: 1.55; } +.birth-time-source-option > span { gap: 4px; } +.birth-time-source-option b { font-size: var(--type-body-sm); line-height: 1.35; } +.birth-time-source-option small { font-size: var(--type-caption); line-height: 1.5; } .birth-time-detail-grid { grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas-soft); } .birth-time-period-details { align-items: start; } .birth-time-period-details > label { align-self: start; } @@ -1650,8 +1675,7 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled .select-item-indicator { display: inline-flex; flex: 0 0 auto; color: var(--color-action); } @keyframes select-content-in { from { opacity: 0; transform: translateY(-3px) scale(.98); } to { opacity: 1; transform: translateY(0) scale(1); } } -.birth-time-source-option { grid-template-columns: minmax(0, 1fr); } -.birth-time-source-option input { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; } +.birth-time-source-option { grid-template-columns: 18px minmax(0, 1fr); } .birth-time-source-option:focus-within { border-color: var(--color-action); outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-action) 16%, transparent); } .birth-time-source-option.is-selected:focus-within { box-shadow: inset 0 0 0 1px var(--color-action), 0 0 0 3px color-mix(in srgb, var(--color-action) 16%, transparent); } .birth-time-source-option > span { min-width: 0; } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 4ff46db7..acec6471 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import Script from "next/script"; import { Toaster } from "@/components/ui/sonner"; import "./globals.css"; @@ -9,6 +9,13 @@ export const metadata: Metadata = { description: "与 Mastra Agent 对话,基于星盘证据讨论事业、关系与时间窗口。", }; +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + viewportFit: "cover", + interactiveWidget: "resizes-content", +}; + export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { const enableReactDevTools = process.env.NODE_ENV === "development" && process.env.NEXT_PUBLIC_ENABLE_REACT_DEVTOOLS === "1"; diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 95e858e0..bd19c400 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -52,6 +52,7 @@ import { describeBirthTimeDraft, isDeclaredBirthProfileComplete, isBirthTimeDraftReady, + birthTimeDraftReadyHint, normalizePersistedBirthDate, type BirthTimeDraft, type BirthTimeSource, @@ -1255,6 +1256,8 @@ export default function Home() { && !onboardingPending && !rectificationSurfaceOpen && !activeSession?.messages.length; + const onboardingFormActive = !profileComplete && onboardingStep !== "name"; + const birthTimeContinueHint = onboardingStep === "birth" ? birthTimeDraftReadyHint(profileDraft) : ""; const daypartGreeting = greetingForHour(new Date().getHours()); function restoreConsultationRecovery(session: ChatSession, requestId: string) { @@ -3257,7 +3260,7 @@ export default function Home() { {!rectificationSurfaceOpen && ( -
{accountError}
}{birthTimeContinueHint}
: }这仍是未确认候选,不会自动成为出生分钟;{birthTimeConsultationOptionsCopy(value)}
)} {displayState.kind === "accepted" && ( -这是你从候选中选择的校正采用时间,并非引擎唯一确认分钟;后续排盘会使用它。
+这是你从候选中选择的校正采用时间,不是外部档案确认的出生时间;后续排盘会使用它。
)} )} diff --git a/frontend/src/lib/birth-time-intake-model.ts b/frontend/src/lib/birth-time-intake-model.ts index e1716736..9abf79db 100644 --- a/frontend/src/lib/birth-time-intake-model.ts +++ b/frontend/src/lib/birth-time-intake-model.ts @@ -83,8 +83,8 @@ export function formatBirthDate(value: Date): string { } export const birthTimeSourceOptions = [ - { value: "family_exact", label: "我知道准确出生时间", hint: "保存为初始化填报时间,不会自动标记为引擎确认" }, - { value: "period_only", label: "我不确定准确时间", hint: "告诉我们大致时段;完全不清楚也可以直接跳过" }, + { value: "family_exact", label: "我知道准确出生时间", hint: "按医院记录或家人记得的时间填写,精确到分钟" }, + { value: "period_only", label: "我不确定准确时间", hint: "只知道大概几点、上午下午,或完全不清楚" }, ] as const; export const birthTimeSourceDefaults = { @@ -223,6 +223,15 @@ export function isBirthTimeDraftReady(draft: BirthTimeDraft) { } } +export function birthTimeDraftReadyHint(draft: BirthTimeDraft) { + if (!parseBirthDate(draft.date)) return "请先选择出生日期"; + if (!draft.birthTimeSource) return "请选择你对出生时间了解多少"; + if (isBirthTimeDraftReady(draft)) return ""; + if (draft.birthTimeSource === "period_only") return "请选择一个大致时段"; + if (draft.birthTimeSource === "unknown") return ""; + return "请填写时和分"; +} + /** * Whether the user has finished declaring what they actually know about birth time. * This is an onboarding condition, not a claim that an exact chart minute is ready. diff --git a/frontend/tests/admin-model-management-ui-contract.test.ts b/frontend/tests/admin-model-management-ui-contract.test.ts index 488a7782..0e6ee46d 100644 --- a/frontend/tests/admin-model-management-ui-contract.test.ts +++ b/frontend/tests/admin-model-management-ui-contract.test.ts @@ -117,11 +117,11 @@ test("model management renders localized labels without changing stored enum val test("global native input sizing excludes Ant Design picker internals", () => { assert.match( globals, - /input:not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\), select[^\{]+\{ width: 100%; min-height: 44px; padding: 0 12px;/, + /input:not\(\[type="radio"\]\):not\(\[type="checkbox"\]\):not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\), select[^\{]+\{ width: 100%; min-height: 44px; padding: 0 12px;/, ); assert.match( globals, - /input:not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\):disabled/, + /input:not\(\[type="radio"\]\):not\(\[type="checkbox"\]\):not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\):disabled/, ); assert.doesNotMatch(globals, /(?:^|\n)input, select \{ width: 100%; min-height: 44px; padding: 0 12px;/); }); diff --git a/frontend/tests/birth-time-consultation-consent.test.ts b/frontend/tests/birth-time-consultation-consent.test.ts index a2a3ef5e..58e48589 100644 --- a/frontend/tests/birth-time-consultation-consent.test.ts +++ b/frontend/tests/birth-time-consultation-consent.test.ts @@ -229,3 +229,15 @@ test("homepage and profile result copy use the source-aware consultation options assert.doesNotMatch(page, /birthTimeConsultationOptionsCopy\(profile\)/); assert.match(intake, /birthTimeConsultationOptionsCopy\(value\)/); }); + +test("onboarding form steps hide the dead composer and keep source choices in a single column", () => { + const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); + const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); + + assert.match(page, /const onboardingFormActive = !profileComplete && onboardingStep !== "name"/); + assert.match(page, /!rectificationSurfaceOpen && !onboardingFormActive &&