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 { useState, type FormEvent } from "react";
|
||||||
import Link from "next/link";
|
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";
|
import { useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
@ -17,35 +17,32 @@ export default function ForgotPasswordPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Reset password</h1>
|
<h1 className="m-0 text-[length:var(--nl-fs-xl)]">Reset password</h1>
|
||||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
|
<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.
|
Enter your email and we'll send a reset link if the account exists.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</AlertBanner>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{success && (
|
{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}
|
{success}
|
||||||
</div>
|
</AlertBanner>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label style={{ display: "grid", gap: 6 }}>
|
<Input
|
||||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Email</span>
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
required
|
required
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
className="input-shell"
|
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -55,8 +52,10 @@ export default function ForgotPasswordPage() {
|
|||||||
Send reset link
|
Send reset link
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p style={{ textAlign: "center", fontSize: "var(--nl-fs-sm)" }}>
|
<p className="text-center text-[length:var(--nl-fs-sm)]">
|
||||||
<Link href="/login" style={{ color: "var(--nl-accent-primary)" }}>Back to sign in</Link>
|
<Link href="/login" className="text-[color:var(--nl-accent-primary)]">
|
||||||
|
Back to sign in
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
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";
|
import { useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
@ -21,38 +21,32 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Sign in</h1>
|
<h1 className="m-0 text-[length:var(--nl-fs-xl)]">Sign in</h1>
|
||||||
|
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</AlertBanner>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label style={{ display: "grid", gap: 6 }}>
|
<Input
|
||||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Email</span>
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
required
|
required
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
className="input-shell"
|
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style={{ display: "grid", gap: 6 }}>
|
<Input
|
||||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Password</span>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Password"
|
||||||
required
|
required
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
className="input-shell"
|
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -62,9 +56,13 @@ export default function LoginPage() {
|
|||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "var(--nl-fs-sm)" }}>
|
<div className="flex items-center justify-between text-[length:var(--nl-fs-sm)]">
|
||||||
<Link href="/forgot-password" style={{ color: "var(--nl-accent-primary)" }}>Forgot password?</Link>
|
<Link href="/forgot-password" className="text-[color:var(--nl-accent-primary)]">
|
||||||
<Link href="/register" style={{ color: "var(--nl-accent-primary)" }}>Create account</Link>
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
<Link href="/register" className="text-[color:var(--nl-accent-primary)]">
|
||||||
|
Create account
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
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";
|
import { useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
@ -22,51 +22,43 @@ export default function RegisterPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Create account</h1>
|
<h1 className="m-0 text-[length:var(--nl-fs-xl)]">Create account</h1>
|
||||||
|
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</AlertBanner>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label style={{ display: "grid", gap: 6 }}>
|
<Input
|
||||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Display name</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Display name"
|
||||||
required
|
required
|
||||||
autoComplete="name"
|
autoComplete="name"
|
||||||
className="input-shell"
|
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style={{ display: "grid", gap: 6 }}>
|
<Input
|
||||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Email</span>
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
|
label="Email"
|
||||||
required
|
required
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
className="input-shell"
|
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style={{ display: "grid", gap: 6 }}>
|
<Input
|
||||||
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>Password</span>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
|
label="Password"
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
className="input-shell"
|
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
hint="Minimum 8 characters"
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -76,9 +68,11 @@ export default function RegisterPage() {
|
|||||||
Create account
|
Create account
|
||||||
</Button>
|
</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?{" "}
|
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>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type FormEvent } from "react";
|
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";
|
import { createWorkspace } from "@/lib/notes-client";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -38,34 +38,47 @@ export function CreateWorkspaceModal({ onCreated, onClose }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ position: "fixed", inset: 0, background: "var(--nl-overlay-scrim)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 }}
|
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(); }}
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card padding="lg" className="w-full max-w-[480px]">
|
||||||
padding="lg"
|
<form onSubmit={handleSubmit} className="grid gap-4">
|
||||||
style={{ width: "100%", maxWidth: 480 }}
|
<div className="text-[length:var(--nl-fs-xl)] font-bold">Create Workspace</div>
|
||||||
>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
style={{ display: "grid", gap: "var(--nl-space-4)" }}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>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 }}>
|
<Input
|
||||||
<span style={{ fontWeight: 600, fontSize: "var(--nl-fs-sm)" }}>Name</span>
|
type="text"
|
||||||
<input className="input-shell" type="text" required value={name} onChange={(e) => setName(e.target.value)} aria-label="Workspace name" autoFocus />
|
label="Name"
|
||||||
</label>
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
aria-label="Workspace name"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
<label style={{ display: "grid", gap: 6 }}>
|
<Textarea
|
||||||
<span style={{ fontWeight: 600, fontSize: "var(--nl-fs-sm)" }}>Description</span>
|
label="Description"
|
||||||
<textarea className="input-shell" value={description} onChange={(e) => setDescription(e.target.value)} rows={3} style={{ resize: "vertical" }} aria-label="Workspace description" />
|
value={description}
|
||||||
</label>
|
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)" }}>
|
<div className="flex justify-end gap-3">
|
||||||
<Button type="button" variant="secondary" onClick={onClose}>Cancel</Button>
|
<Button type="button" variant="secondary" onClick={onClose}>
|
||||||
<Button type="submit" disabled={!name.trim() || saving} loading={saving}>Create</Button>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={!name.trim() || saving} loading={saving}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
AlertBanner as BytelystAlertBanner,
|
||||||
|
FormSection as BytelystFormSection,
|
||||||
|
FieldGrid as BytelystFieldGrid,
|
||||||
AppShell as BytelystAppShell,
|
AppShell as BytelystAppShell,
|
||||||
AppShellMain as BytelystAppShellMain,
|
AppShellMain as BytelystAppShellMain,
|
||||||
AppShellMobileToggle as BytelystAppShellMobileToggle,
|
AppShellMobileToggle as BytelystAppShellMobileToggle,
|
||||||
@ -77,6 +80,9 @@ import {
|
|||||||
type DataListItemProps,
|
type DataListItemProps,
|
||||||
type DataListProps,
|
type DataListProps,
|
||||||
type DiffCardProps,
|
type DiffCardProps,
|
||||||
|
type AlertBannerProps,
|
||||||
|
type FormSectionProps,
|
||||||
|
type FieldGridProps,
|
||||||
type EmptyStateProps,
|
type EmptyStateProps,
|
||||||
type IconButtonProps,
|
type IconButtonProps,
|
||||||
type InputProps,
|
type InputProps,
|
||||||
@ -360,6 +366,18 @@ export function EmptyState({ className, ...props }: EmptyStateProps) {
|
|||||||
return <BytelystEmptyState className={mergeClassNames("rounded-[var(--nl-radius-md)]", className)} {...props} />;
|
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) {
|
export function LoadingSpinner({ className, ...props }: LoadingSpinnerProps) {
|
||||||
return <BytelystLoadingSpinner className={className} {...props} />;
|
return <BytelystLoadingSpinner className={className} {...props} />;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user