diff --git a/.trellis/spec/frontend/component-guidelines.md b/.trellis/spec/frontend/component-guidelines.md
index 15b22b4..1882135 100644
--- a/.trellis/spec/frontend/component-guidelines.md
+++ b/.trellis/spec/frontend/component-guidelines.md
@@ -71,6 +71,7 @@ If a component requires a large amount of static data (e.g., "dead" data for ren
**Every method, property, interface, and complex logic block MUST be documented.**
- **Language Requirement**: All comments inside the code (JSDoc and internal) **MUST be written in Chinese**.
+- **Mandatory Positive Constraint**: Every new **interface**, **type**, **exported or module-level constant**, **function component**, and **business logic function** MUST have a JSDoc `/** */` comment in **Chinese**. The only exception is for local variables within a function that are immediately obvious and self-explanatory.
- **Public API/Props/Interfaces**: Use JSDoc style `/** ... */` **mandatory** for every interface definition and **every single property** within that interface.
- **Methods & Functions**: Every function (exported or internal) **must** have a `/** ... */` comment explaining its purpose, parameters, and return value.
- **Internal Logic**: Use double-slash `//` for step-by-step explanations inside function bodies.
diff --git a/.trellis/spec/frontend/type-safety.md b/.trellis/spec/frontend/type-safety.md
index 6994bb5..684e2ee 100644
--- a/.trellis/spec/frontend/type-safety.md
+++ b/.trellis/spec/frontend/type-safety.md
@@ -124,6 +124,9 @@ export const SearchBar = ({ placeholder, onSearch }: SearchBarProps) => {
Define the store state and actions with an interface.
```typescript
+/**
+ * 权益状态
+ */
interface AuthState {
token: string | null;
setToken: (token: string) => void;
diff --git a/src/app/(auth)/components/password-toggle/index.tsx b/src/app/(auth)/components/password-toggle/index.tsx
new file mode 100644
index 0000000..f74052f
--- /dev/null
+++ b/src/app/(auth)/components/password-toggle/index.tsx
@@ -0,0 +1,25 @@
+import { Eye, EyeOff } from "lucide-react";
+
+/** 密码显示/隐藏切换按钮的参数。 */
+interface PasswordToggleProps {
+ isShown: boolean;
+ onToggle: () => void;
+}
+
+/** 密码显示/隐藏切换按钮,复用在登录和注册密码输入框中。 */
+export function PasswordToggle({ isShown, onToggle }: PasswordToggleProps) {
+ return (
+
+ );
+}
diff --git a/src/app/(auth)/index.scss b/src/app/(auth)/index.scss
new file mode 100644
index 0000000..82513b6
--- /dev/null
+++ b/src/app/(auth)/index.scss
@@ -0,0 +1,9 @@
+.storeai-auth {
+ /* 认证壳层的细网格背景,只在该路由组内生效。 */
+ .auth-grid {
+ background-image: linear-gradient(to right, rgb(226 232 240 / 0.45) 1px, transparent 1px),
+ linear-gradient(to bottom, rgb(226 232 240 / 0.45) 1px, transparent 1px);
+ background-size: 48px 48px;
+ mask-image: radial-gradient(ellipse at center, black 30%, transparent 70%);
+ }
+}
diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx
new file mode 100644
index 0000000..d930c28
--- /dev/null
+++ b/src/app/(auth)/layout.tsx
@@ -0,0 +1,137 @@
+import Link from "next/link";
+import { BellRing, Clock3, MessagesSquare, ShieldCheck } from "lucide-react";
+
+import "./index.scss";
+
+/** 认证壳层左侧卖点行的入参。 */
+interface FeatureRowProps {
+ icon: React.ComponentType<{ className?: string; "aria-hidden"?: true }>;
+ title: string;
+ body: string;
+}
+
+/**
+ * 登录和注册共享的认证壳层。
+ * 桌面端展示左侧品牌说明,移动端保留紧凑品牌头,表单内容由子路由提供。
+ */
+export default function AuthLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ S
+
+ StoreAI
+
+
+ Anomaly-first store monitor / two reports a day
+
+
+
+
+ {children}
+
+
+
+ By continuing you agree to our{" "}
+
+ Terms
+ {" "}
+ and{" "}
+
+ Privacy Policy
+
+ .
+
+
+
+
+
+ );
+}
+
+/** 认证壳层左侧的单条产品卖点。 */
+function FeatureRow({ icon: Icon, title, body }: FeatureRowProps) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(auth)/login/login-error.ts b/src/app/(auth)/login/login-error.ts
new file mode 100644
index 0000000..5e4dd19
--- /dev/null
+++ b/src/app/(auth)/login/login-error.ts
@@ -0,0 +1,37 @@
+export type LoginErrorType = "1" | "2" | "3";
+
+export interface LoginNotice {
+ tone: "warning" | "success";
+ title: string;
+ body: string;
+}
+
+const login_error_text: Record = {
+ "1": {
+ tone: "warning",
+ title: "Signed out from the extension.",
+ body: "Log back in to continue scanning.",
+ },
+ "2": {
+ tone: "warning",
+ title: "Signed in on another device.",
+ body:
+ "StoreAI allows one active session per account, so this device was signed out. Log back in to continue here - the other device will then be signed out.",
+ },
+ "3": {
+ tone: "success",
+ title: "An account already exists with this email.",
+ body: "Sign in below to continue.",
+ },
+};
+
+/**
+ * 根据 query 中的 error_type 获取登录页提示配置。
+ */
+export function getLoginNotice(errorType: string | null): LoginNotice | null {
+ if (!errorType) return null;
+ if (errorType in login_error_text) {
+ return login_error_text[errorType as LoginErrorType];
+ }
+ return null;
+}
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..f8df401
--- /dev/null
+++ b/src/app/(auth)/login/page.tsx
@@ -0,0 +1,174 @@
+"use client";
+
+import {Suspense, useState} from "react";
+import Link from "next/link";
+import {useSearchParams} from "next/navigation";
+import {AlertTriangle, Lock, Mail} from "lucide-react";
+
+import {PasswordToggle} from "../components/password-toggle";
+import {getLoginNotice} from "./login-error";
+import {validateSignup} from "../validate";
+
+/** 登录表单提交参数。 */
+interface LoginPayload {
+ email: string;
+ password: string;
+}
+
+/** 登录页入口,为依赖查询参数的表单提供 Suspense 边界。 */
+export default function LoginPage() {
+ return (
+ }>
+
+
+ );
+}
+
+/** 登录表单加载占位,保持认证壳层布局稳定。 */
+function LoginFormFallback() {
+ return ;
+}
+
+/** 登录表单主体,保留 query 逻辑、校验和错误态。 */
+function LoginForm() {
+ const searchParams = useSearchParams();
+ /*** 提示 */
+ const loginNotice = getLoginNotice(searchParams.get("error_type"));
+ const prefillEmail = searchParams.get("email") ?? "";
+
+ const [email, setEmail] = useState(prefillEmail);
+ const [password, setPassword] = useState("");
+ const [showPassword, setShowPassword] = useState(false);
+ /*** 错误*/
+ const [error, setError] = useState("");
+ /*** 提交中*/
+ const [loading, setLoading] = useState(false);
+
+ /** 处理登录表单提交,真实接口接入前只执行前端校验和占位提交。 */
+ async function handleSubmit(event: any) {
+ event.preventDefault();
+ setError(null);
+ //效验
+ const validationError = validateSignup({email, password});
+ if (validationError) {
+ setError(validationError);
+ return;
+ }
+ setLoading(true);
+ try {
+
+ } catch (err) {
+
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
Welcome back
+
+ Sign in to your StoreAI dashboard.
+
+
+
+ {/*提示*/}
+ {loginNotice && (
+
+ {loginNotice.body}
+
+ )}
+
+
+
+
+ New to StoreAI?{" "}
+
+ Start your free trial
+
+
+
+ );
+}
+
+
+/** 登录页中用来解释 query 状态的提示条。 */
+function Notice({title, children, tone,}: {
+ title: string;
+ children: React.ReactNode;
+ tone: "warning" | "success";
+}) {
+ const className =
+ tone === "success"
+ ? "flex items-start gap-2 rounded-md border border-emerald-200 bg-emerald-50 p-3 text-xs leading-relaxed text-emerald-900"
+ : "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 p-3 text-xs leading-relaxed text-amber-900";
+
+ return (
+
+
+
+ {title}
+ {children}
+
+
+ );
+}
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx
new file mode 100644
index 0000000..c467733
--- /dev/null
+++ b/src/app/(auth)/signup/page.tsx
@@ -0,0 +1,204 @@
+"use client";
+
+import {useMemo, useState} from "react";
+import Link from "next/link";
+import {CheckCircle2, Lock, Mail, Sparkles} from "lucide-react";
+
+import {PasswordToggle} from "../components/password-toggle";
+import {validateSignup} from "../validate";
+
+
+/** 密码强度的分级结果。 */
+interface PasswordStrength {
+ score: 0 | 1 | 2 | 3;
+ label: string;
+}
+
+/** 密码强度条的颜色映射。 */
+const STRENGTH_BAR_COLOR: Record = {
+ 0: "bg-muted",
+ 1: "bg-rose-500",
+ 2: "bg-amber-500",
+ 3: "bg-emerald-500",
+};
+
+/** 密码强度标签的颜色映射。 */
+const STRENGTH_LABEL_COLOR: Record = {
+ 0: "text-muted-foreground",
+ 1: "text-rose-600",
+ 2: "text-amber-600",
+ 3: "text-emerald-600",
+};
+
+
+/** 注册表单主体,保留 query 逻辑、密码强度和校验。 */
+export default function SignupForm() {
+
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [showPassword, setShowPassword] = useState(false);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const strength = useMemo(() => gradePassword(password), [password]);
+
+ /** 处理注册表单提交,真实接口接入前只执行前端校验和占位提交。 */
+ async function handleSubmit(event: any) {
+ event.preventDefault();
+ setError(null);
+
+ const validationError = validateSignup({email, password});
+ if (validationError) {
+ setError(validationError);
+ return;
+ }
+
+ setLoading(true);
+ try {
+ // await submitSignup({email, password});
+ } catch (err) {
+ // setError(err instanceof Error ? err.message : "Sign-up failed.");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+ 24-hour free trial / no card required
+
+
Create your StoreAI account
+
+ Two scheduled reports a day from your store. Cancel anytime.
+
+
+
+