feat(web/ui5): migrate auth pages and CreateWorkspaceModal to @bytelyst/ui primitives
Sprint C / UI5 — migrate the highest-leverage user-facing forms off the legacy 'input-shell' / inline-style pattern onto the @bytelyst/ui Input, Textarea, and AlertBanner primitives via the local Primitives.tsx adapter. Adapter additions (web/src/components/ui/Primitives.tsx): - Re-export AlertBanner, FormSection, and FieldGrid from @bytelyst/ui so product code never imports from the underlying package directly. Migrated screens: - web/src/app/(auth)/login/page.tsx - web/src/app/(auth)/register/page.tsx - web/src/app/(auth)/forgot-password/page.tsx - web/src/components/CreateWorkspaceModal.tsx Each migration replaces the ad-hoc 'input-shell' inputs and manual label/error/success divs with the Input (label + hint props), Textarea, and AlertBanner (tone='error'|'success') primitives. Inline style blocks are replaced with Tailwind utility classes that read from the existing --nl-* CSS custom properties so the visual tokens remain unchanged. The 3 auth pages alone remove 9 input-shell call sites; the CreateWorkspaceModal removes 2 more. Verified: - pnpm --filter @notelett/web run typecheck: passes - pnpm --filter @notelett/web run test: 96/96 pass - pnpm run verify: end-to-end green (backend 380/380, web 96/96, mobile 97/97)
This commit is contained in:
parent
4667f85e20
commit
9c65899387
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { AlertBanner, Button, Card, Input } from "@/components/ui/Primitives";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
@ -17,35 +17,32 @@ export default function ForgotPasswordPage() {
|
||||
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Reset password</h1>
|
||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||
<h1 className="m-0 text-[length:var(--nl-fs-xl)]">Reset password</h1>
|
||||
<p className="m-0 text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]">
|
||||
Enter your email and we'll send a reset link if the account exists.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div role="alert" style={{ padding: "10px 14px", borderRadius: "var(--nl-radius-sm)", background: "var(--nl-danger-muted)", color: "var(--nl-danger)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<AlertBanner tone="error" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div role="status" style={{ padding: "10px 14px", borderRadius: "var(--nl-radius-sm)", background: "var(--nl-success-muted)", color: "var(--nl-status-success)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<AlertBanner tone="success" role="status">
|
||||
{success}
|
||||
</div>
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
<label style={{ display: "grid", gap: 6 }}>
|
||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Email</span>
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
label="Email"
|
||||
required
|
||||
autoComplete="email"
|
||||
className="input-shell"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
@ -55,8 +52,10 @@ export default function ForgotPasswordPage() {
|
||||
Send reset link
|
||||
</Button>
|
||||
|
||||
<p style={{ textAlign: "center", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<Link href="/login" style={{ color: "var(--nl-accent-primary)" }}>Back to sign in</Link>
|
||||
<p className="text-center text-[length:var(--nl-fs-sm)]">
|
||||
<Link href="/login" className="text-[color:var(--nl-accent-primary)]">
|
||||
Back to sign in
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { AlertBanner, Button, Card, Input } from "@/components/ui/Primitives";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
@ -21,38 +21,32 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Sign in</h1>
|
||||
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||
<h1 className="m-0 text-[length:var(--nl-fs-xl)]">Sign in</h1>
|
||||
|
||||
{error && (
|
||||
<div role="alert" style={{ padding: "10px 14px", borderRadius: "var(--nl-radius-sm)", background: "var(--nl-danger-muted)", color: "var(--nl-danger)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<AlertBanner tone="error" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
<label style={{ display: "grid", gap: 6 }}>
|
||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Email</span>
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
label="Email"
|
||||
required
|
||||
autoComplete="email"
|
||||
className="input-shell"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: "grid", gap: 6 }}>
|
||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Password</span>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
className="input-shell"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
@ -62,9 +56,13 @@ export default function LoginPage() {
|
||||
Sign in
|
||||
</Button>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<Link href="/forgot-password" style={{ color: "var(--nl-accent-primary)" }}>Forgot password?</Link>
|
||||
<Link href="/register" style={{ color: "var(--nl-accent-primary)" }}>Create account</Link>
|
||||
<div className="flex items-center justify-between text-[length:var(--nl-fs-sm)]">
|
||||
<Link href="/forgot-password" className="text-[color:var(--nl-accent-primary)]">
|
||||
Forgot password?
|
||||
</Link>
|
||||
<Link href="/register" className="text-[color:var(--nl-accent-primary)]">
|
||||
Create account
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { AlertBanner, Button, Card, Input } from "@/components/ui/Primitives";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
export default function RegisterPage() {
|
||||
@ -22,51 +22,43 @@ export default function RegisterPage() {
|
||||
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Create account</h1>
|
||||
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||
<h1 className="m-0 text-[length:var(--nl-fs-xl)]">Create account</h1>
|
||||
|
||||
{error && (
|
||||
<div role="alert" style={{ padding: "10px 14px", borderRadius: "var(--nl-radius-sm)", background: "var(--nl-danger-muted)", color: "var(--nl-danger)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<AlertBanner tone="error" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
<label style={{ display: "grid", gap: 6 }}>
|
||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Display name</span>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
label="Display name"
|
||||
required
|
||||
autoComplete="name"
|
||||
className="input-shell"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: "grid", gap: 6 }}>
|
||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Email</span>
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
label="Email"
|
||||
required
|
||||
autoComplete="email"
|
||||
className="input-shell"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: "grid", gap: 6 }}>
|
||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Password</span>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
className="input-shell"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
hint="Minimum 8 characters"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
@ -76,9 +68,11 @@ export default function RegisterPage() {
|
||||
Create account
|
||||
</Button>
|
||||
|
||||
<p style={{ textAlign: "center", fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>
|
||||
<p className="text-center text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]">
|
||||
Already have an account?{" "}
|
||||
<Link href="/login" style={{ color: "var(--nl-accent-primary)" }}>Sign in</Link>
|
||||
<Link href="/login" className="text-[color:var(--nl-accent-primary)]">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { AlertBanner, Button, Card, Input, Textarea } from "@/components/ui/Primitives";
|
||||
import { createWorkspace } from "@/lib/notes-client";
|
||||
|
||||
interface Props {
|
||||
@ -38,34 +38,47 @@ export function CreateWorkspaceModal({ onCreated, onClose }: Props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ position: "fixed", inset: 0, background: "var(--nl-overlay-scrim)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 }}
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
className="fixed inset-0 z-[1000] flex items-center justify-center bg-[color:var(--nl-overlay-scrim)]"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
padding="lg"
|
||||
style={{ width: "100%", maxWidth: 480 }}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{ display: "grid", gap: "var(--nl-space-4)" }}
|
||||
>
|
||||
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Create Workspace</div>
|
||||
<Card padding="lg" className="w-full max-w-[480px]">
|
||||
<form onSubmit={handleSubmit} className="grid gap-4">
|
||||
<div className="text-[length:var(--nl-fs-xl)] font-bold">Create Workspace</div>
|
||||
|
||||
{error && <div style={{ color: "var(--nl-danger)", fontSize: "var(--nl-fs-sm)" }}>{error}</div>}
|
||||
{error && (
|
||||
<AlertBanner tone="error" role="alert">
|
||||
{error}
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
<label style={{ display: "grid", gap: 6 }}>
|
||||
<span style={{ fontWeight: 600, fontSize: "var(--nl-fs-sm)" }}>Name</span>
|
||||
<input className="input-shell" type="text" required value={name} onChange={(e) => setName(e.target.value)} aria-label="Workspace name" autoFocus />
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
label="Name"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
aria-label="Workspace name"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<label style={{ display: "grid", gap: 6 }}>
|
||||
<span style={{ fontWeight: 600, fontSize: "var(--nl-fs-sm)" }}>Description</span>
|
||||
<textarea className="input-shell" value={description} onChange={(e) => setDescription(e.target.value)} rows={3} style={{ resize: "vertical" }} aria-label="Workspace description" />
|
||||
</label>
|
||||
<Textarea
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
className="resize-y"
|
||||
aria-label="Workspace description"
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-3)" }}>
|
||||
<Button type="button" variant="secondary" onClick={onClose}>Cancel</Button>
|
||||
<Button type="submit" disabled={!name.trim() || saving} loading={saving}>Create</Button>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!name.trim() || saving} loading={saving}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import {
|
||||
AlertBanner as BytelystAlertBanner,
|
||||
FormSection as BytelystFormSection,
|
||||
FieldGrid as BytelystFieldGrid,
|
||||
AppShell as BytelystAppShell,
|
||||
AppShellMain as BytelystAppShellMain,
|
||||
AppShellMobileToggle as BytelystAppShellMobileToggle,
|
||||
@ -77,6 +80,9 @@ import {
|
||||
type DataListItemProps,
|
||||
type DataListProps,
|
||||
type DiffCardProps,
|
||||
type AlertBannerProps,
|
||||
type FormSectionProps,
|
||||
type FieldGridProps,
|
||||
type EmptyStateProps,
|
||||
type IconButtonProps,
|
||||
type InputProps,
|
||||
@ -360,6 +366,18 @@ export function EmptyState({ className, ...props }: EmptyStateProps) {
|
||||
return <BytelystEmptyState className={mergeClassNames("rounded-[var(--nl-radius-md)]", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function AlertBanner({ className, ...props }: AlertBannerProps) {
|
||||
return <BytelystAlertBanner className={mergeClassNames("rounded-[var(--nl-radius-sm)]", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function FormSection({ className, ...props }: FormSectionProps) {
|
||||
return <BytelystFormSection className={className} {...props} />;
|
||||
}
|
||||
|
||||
export function FieldGrid({ className, ...props }: FieldGridProps) {
|
||||
return <BytelystFieldGrid className={className} {...props} />;
|
||||
}
|
||||
|
||||
export function LoadingSpinner({ className, ...props }: LoadingSpinnerProps) {
|
||||
return <BytelystLoadingSpinner className={className} {...props} />;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user