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:
saravanakumardb1 2026-05-22 23:51:34 -07:00
parent 4667f85e20
commit 9c65899387
5 changed files with 145 additions and 123 deletions

View File

@ -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,48 +17,47 @@ 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)" }}>
Enter your email and we&apos;ll send a reset link if the account exists.
</p>
<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&apos;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)" }}>
{error}
</div>
)}
{error && (
<AlertBanner tone="error" role="alert">
{error}
</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)" }}>
{success}
</div>
)}
{success && (
<AlertBanner tone="success" role="status">
{success}
</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"
disabled={isLoading}
loading={isLoading}
>
Send reset link
</Button>
<Button
type="submit"
disabled={isLoading}
loading={isLoading}
>
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>
</form>
<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>
);
}

View File

@ -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,52 +21,50 @@ 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)" }}>
{error}
</div>
)}
{error && (
<AlertBanner tone="error" role="alert">
{error}
</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"
disabled={isLoading}
loading={isLoading}
>
Sign in
</Button>
<Button
type="submit"
disabled={isLoading}
loading={isLoading}
>
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>
</form>
<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>
);
}

View File

@ -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,65 +22,59 @@ 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)" }}>
{error}
</div>
)}
{error && (
<AlertBanner tone="error" role="alert">
{error}
</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"
disabled={isLoading}
loading={isLoading}
>
Create account
</Button>
<Button
type="submit"
disabled={isLoading}
loading={isLoading}
>
Create account
</Button>
<p style={{ textAlign: "center", fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>
Already have an account?{" "}
<Link href="/login" style={{ color: "var(--nl-accent-primary)" }}>Sign in</Link>
</p>
</form>
<p className="text-center text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]">
Already have an account?{" "}
<Link href="/login" className="text-[color:var(--nl-accent-primary)]">
Sign in
</Link>
</p>
</form>
</Card>
);
}

View File

@ -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,36 +38,49 @@ 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>
</form>
<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>
</div>
);

View File

@ -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} />;
}