fix(ui): keep mobile onboarding and login usable
Hide the dead composer during birth-time intake, replace internal copy with user-facing hints, and keep short-screen login, location search, and session history reachable on phones.
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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() {
|
||||
</header>
|
||||
|
||||
{!rectificationSurfaceOpen && (
|
||||
<div ref={conversation} className={`conversation ${activeSession?.messages.length ? "" : "is-empty"}`}>
|
||||
<div ref={conversation} className={`conversation ${!activeSession?.messages.length && !onboardingFormActive ? "is-empty" : ""} ${onboardingFormActive ? "is-onboarding-form" : ""}`}>
|
||||
{!activeSession?.messages.length ? (
|
||||
<div className="welcome">
|
||||
{!profileComplete ? (
|
||||
@@ -3287,6 +3290,7 @@ export default function Home() {
|
||||
<BirthTimeIntakeFields value={profileDraft} onPatch={(patch) => setProfileDraft((current) => applyBirthTimeDraftPatch(current, patch))} />
|
||||
{accountError && <p className="form-error" role="alert">{accountError}</p>}
|
||||
<div className="onboarding-card-actions">
|
||||
{birthTimeContinueHint ? <p className="onboarding-card-action-hint">{birthTimeContinueHint}</p> : <span />}
|
||||
<button className="button-primary" type="submit" disabled={profileSaving || !isBirthTimeDraftReady(profileDraft)}>{profileSaving ? "保存中" : "继续"}</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -3460,7 +3464,7 @@ export default function Home() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!rectificationSurfaceOpen && <div className={`composer-wrap ${starterHomeVisible ? "composer-wrap-starter" : ""}`}>
|
||||
{!rectificationSurfaceOpen && !onboardingFormActive && <div className={`composer-wrap ${starterHomeVisible ? "composer-wrap-starter" : ""}`}>
|
||||
{activeSuggestions.length > 0 && (
|
||||
<div className="composer-suggestions" aria-label="推荐继续提问">
|
||||
{activeSuggestions.map((question) => (
|
||||
|
||||
@@ -85,7 +85,7 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
|
||||
<section className="birth-time-profile-result" aria-label="生时校正结果">
|
||||
<div className="birth-time-profile-result-heading">
|
||||
<span>出生时间记录</span>
|
||||
<strong>{displayState.kind === "candidate" ? "候选时间" : displayState.kind === "accepted" ? "校正采用" : "引擎确认"}</strong>
|
||||
<strong>{displayState.kind === "candidate" ? "候选时间" : displayState.kind === "accepted" ? "校正采用" : "已记录时间"}</strong>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
@@ -101,7 +101,7 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
|
||||
<p>这仍是未确认候选,不会自动成为出生分钟;{birthTimeConsultationOptionsCopy(value)}</p>
|
||||
)}
|
||||
{displayState.kind === "accepted" && (
|
||||
<p>这是你从候选中选择的校正采用时间,并非引擎唯一确认分钟;后续排盘会使用它。</p>
|
||||
<p>这是你从候选中选择的校正采用时间,不是外部档案确认的出生时间;后续排盘会使用它。</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;/);
|
||||
});
|
||||
|
||||
@@ -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 && <div className=\{`composer-wrap/);
|
||||
assert.match(page, /birthTimeDraftReadyHint\(profileDraft\)/);
|
||||
assert.match(css, /\.birth-time-source-list \{ grid-template-columns: 1fr/);
|
||||
assert.doesNotMatch(css, /\.birth-time-source-list \{ grid-template-columns: repeat\(2/);
|
||||
assert.doesNotMatch(css, /\.birth-time-source-option input \{[^}]*clip-path: inset\(50%\)/);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
applyBirthTimeDraftPatch,
|
||||
assistantIntentCopy,
|
||||
birthTimeDisplayState,
|
||||
birthTimeDraftReadyHint,
|
||||
birthTimePersistenceValues,
|
||||
birthTimeSourceDefaults,
|
||||
birthTimeSourceOptions,
|
||||
@@ -308,9 +309,34 @@ test("persisted birth dates normalize database ISO values without accepting inva
|
||||
assert.equal(normalizePersistedBirthDate("1997-08-08junk"), "");
|
||||
});
|
||||
|
||||
test("fresh intake copy stays user-facing and explains the next missing field", () => {
|
||||
assert.equal(
|
||||
birthTimeSourceOptions.find((option) => option.value === "family_exact")?.hint,
|
||||
"按医院记录或家人记得的时间填写,精确到分钟",
|
||||
);
|
||||
assert.equal(
|
||||
birthTimeSourceOptions.find((option) => option.value === "period_only")?.hint,
|
||||
"只知道大概几点、上午下午,或完全不清楚",
|
||||
);
|
||||
assert.equal(birthTimeSourceOptions.every((option) => !/引擎确认|初始化填报/.test(option.hint)), true);
|
||||
assert.equal(birthTimeDraftReadyHint({ ...emptyDraft, date: "" }), "请先选择出生日期");
|
||||
assert.equal(birthTimeDraftReadyHint(emptyDraft), "请选择你对出生时间了解多少");
|
||||
assert.equal(birthTimeDraftReadyHint({
|
||||
...emptyDraft,
|
||||
birthTimeSource: "family_exact",
|
||||
uncertaintyBeforeMinutes: 0,
|
||||
uncertaintyAfterMinutes: 0,
|
||||
}), "请填写时和分");
|
||||
assert.equal(birthTimeDraftReadyHint({
|
||||
...emptyDraft,
|
||||
birthTimeSource: "period_only",
|
||||
}), "请选择一个大致时段");
|
||||
});
|
||||
|
||||
test("fresh intake preserves exact, approximate-period, and unknown-time paths without auto-confirming", () => {
|
||||
const source = readFileSync(new URL("../src/components/birth-time-intake.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.doesNotMatch(source, /引擎确认/);
|
||||
assert.deepEqual(
|
||||
birthTimeSourceOptions.map((option) => option.value),
|
||||
["family_exact", "period_only"],
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
|
||||
|
||||
test("mobile login can scroll and keeps the form reachable on a short screen", () => {
|
||||
const mobileAuth = css.indexOf(".auth-page { height: 100dvh; overflow-x: hidden; overflow-y: auto;");
|
||||
assert.ok(mobileAuth >= 0, "the login page must be the mobile scroll container");
|
||||
assert.match(css.slice(mobileAuth, mobileAuth + 900), /\.auth-shell \{ min-height: 100%; overflow: visible;/);
|
||||
assert.match(css, /@media \(max-width: 767px\) and \(max-height: 640px\) \{\s*\.auth-story \{ display: none; \}/);
|
||||
assert.match(layout, /interactiveWidget:\s*"resizes-content"/);
|
||||
});
|
||||
|
||||
test("mobile onboarding does not clip location results or session history", () => {
|
||||
assert.match(css, /\.session-nav \{ overflow: visible; \}/);
|
||||
assert.doesNotMatch(css, /\.session-nav \{ overflow: hidden; \}/);
|
||||
assert.match(css, /\.location-combobox-results \{ position: static; top: auto;/);
|
||||
});
|
||||
Reference in New Issue
Block a user