@utopia-studio-design/design-system-cli 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/utopia-ds-mcp.mjs +5 -15
- package/bin/utopia-ds.mjs +3 -16
- package/data/docs/create-a-brandbook.md +11 -1
- package/data/manifests/blocks.json +21 -230
- package/data/manifests/patterns.json +0 -330
- package/lib/api.mjs +6 -193
- package/lib/brandbook-validation.mjs +112 -0
- package/package.json +2 -2
- package/data/blocks/semantic-pattern-parts/AuthParts.tsx +0 -48
- package/data/blocks/semantic-pattern-parts/ChoiceCards.tsx +0 -7
- package/data/blocks/semantic-pattern-parts/ListFilters.tsx +0 -9
- package/data/blocks/semantic-pattern-parts/OutcomeNotice.tsx +0 -7
- package/data/blocks/semantic-pattern-parts/PatternParts.tsx +0 -7
- package/data/blocks/semantic-pattern-parts/ProductParts.tsx +0 -38
- package/data/blocks/semantic-pattern-parts/ProgressSteps.tsx +0 -6
- package/data/blocks/semantic-pattern-parts/ReviewList.tsx +0 -7
- package/data/blocks/semantic-pattern-parts/ValidatedField.tsx +0 -8
- package/data/blocks/semantic-pattern-parts/WorkflowParts.tsx +0 -339
- package/data/blocks/semantic-pattern-parts/auth-parts.css +0 -27
- package/data/blocks/semantic-pattern-parts/pattern-parts.css +0 -25
- package/data/blocks/semantic-pattern-parts/product-parts.css +0 -25
- package/data/blocks/semantic-pattern-parts/types.ts +0 -3
- package/data/blocks/semantic-pattern-parts/workflow-parts.css +0 -118
- package/data/blocks/ux-pattern-examples/AccessExamples.tsx +0 -46
- package/data/blocks/ux-pattern-examples/AccountExamples.tsx +0 -133
- package/data/blocks/ux-pattern-examples/OnboardingExample.tsx +0 -15
- package/data/blocks/ux-pattern-examples/ProductExamples.tsx +0 -79
- package/data/blocks/ux-pattern-examples/ReviewExample.tsx +0 -14
- package/data/blocks/ux-pattern-examples/SignupExample.tsx +0 -22
- package/data/blocks/ux-pattern-examples/WorkflowExample.tsx +0 -1761
- package/data/blocks/ux-pattern-examples/WorkflowExamples.tsx +0 -190
- package/data/blocks/ux-pattern-examples/copy.ts +0 -6
- package/data/blocks/ux-pattern-examples/ux-examples.css +0 -18
- package/data/blocks/ux-pattern-examples/workflow-copy.ts +0 -76
- package/data/blocks/ux-pattern-examples/workflow-definitions.ts +0 -1291
- package/data/docs/ui-parts-qa.md +0 -172
- package/data/docs/ui-parts.md +0 -105
- package/data/docs/ux-pattern-adaptation-qa.md +0 -38
- package/data/docs/ux-pattern-adaptation.md +0 -41
- package/data/docs/ux-pattern-migration.md +0 -111
- package/data/docs/ux-pattern-release-2026-09-13.md +0 -26
- package/data/docs/ux-source-evidence/account-auth.json +0 -41
- package/data/docs/ux-source-evidence/account-forgot-password.json +0 -36
- package/data/docs/ux-source-evidence/account-invite.json +0 -63
- package/data/docs/ux-source-evidence/account-locked.json +0 -63
- package/data/docs/ux-source-evidence/account-login.json +0 -28
- package/data/docs/ux-source-evidence/account-mfa.json +0 -62
- package/data/docs/ux-source-evidence/account-onboarding-credentials.json +0 -52
- package/data/docs/ux-source-evidence/account-onboarding-legal.json +0 -56
- package/data/docs/ux-source-evidence/account-onboarding-orientation.json +0 -66
- package/data/docs/ux-source-evidence/account-onboarding-profile.json +0 -50
- package/data/docs/ux-source-evidence/account-onboarding.json +0 -69
- package/data/docs/ux-source-evidence/account-reset-password.json +0 -27
- package/data/docs/ux-source-evidence/account-session-expired.json +0 -62
- package/data/docs/ux-source-evidence/account-signup.json +0 -33
- package/data/docs/ux-source-evidence/account-verify-email.json +0 -34
- package/data/docs/ux-source-evidence/account-welcome.json +0 -41
- package/data/docs/ux-source-evidence/admin-analytics.json +0 -54
- package/data/docs/ux-source-evidence/admin-audit.json +0 -31
- package/data/docs/ux-source-evidence/admin-broadcasts-new.json +0 -36
- package/data/docs/ux-source-evidence/admin-broadcasts.json +0 -41
- package/data/docs/ux-source-evidence/admin-closures.json +0 -52
- package/data/docs/ux-source-evidence/admin-devices.json +0 -51
- package/data/docs/ux-source-evidence/admin-escalations.json +0 -35
- package/data/docs/ux-source-evidence/admin-incidents.json +0 -54
- package/data/docs/ux-source-evidence/admin-loading.json +0 -17
- package/data/docs/ux-source-evidence/admin-onboarding-detail.json +0 -80
- package/data/docs/ux-source-evidence/admin-onboarding.json +0 -62
- package/data/docs/ux-source-evidence/admin-payments.json +0 -45
- package/data/docs/ux-source-evidence/admin-providers.json +0 -38
- package/data/docs/ux-source-evidence/admin-settings.json +0 -18
- package/data/docs/ux-source-evidence/admin-subscriptions-detail-allocate.json +0 -42
- package/data/docs/ux-source-evidence/admin-subscriptions-detail.json +0 -48
- package/data/docs/ux-source-evidence/admin-subscriptions-new.json +0 -43
- package/data/docs/ux-source-evidence/admin-subscriptions.json +0 -41
- package/data/docs/ux-source-evidence/admin-transactions.json +0 -41
- package/data/docs/ux-source-evidence/error.json +0 -30
- package/data/docs/ux-source-evidence/loading.json +0 -19
- package/data/docs/ux-source-evidence/marketing-blog.json +0 -48
- package/data/docs/ux-source-evidence/marketing-contact.json +0 -36
- package/data/docs/ux-source-evidence/marketing.json +0 -64
- package/data/docs/ux-source-evidence/not-found.json +0 -28
- package/data/docs/ux-source-evidence/portal-loading.json +0 -17
- package/data/docs/ux-source-evidence/portal-mentor-messages.json +0 -31
- package/data/docs/ux-source-evidence/portal-mentor-profile.json +0 -23
- package/data/docs/ux-source-evidence/portal-mentor-requests-detail.json +0 -54
- package/data/docs/ux-source-evidence/portal-mentor-sessions-detail-assess.json +0 -42
- package/data/docs/ux-source-evidence/portal-mentor-settings.json +0 -18
- package/data/docs/ux-source-evidence/portal-mentor.json +0 -38
- package/data/docs/ux-source-evidence/portal-sessions-detail-live.json +0 -48
- package/data/docs/ux-source-evidence/portal-sponsor-billing.json +0 -28
- package/data/docs/ux-source-evidence/portal-sponsor-profile.json +0 -22
- package/data/docs/ux-source-evidence/portal-sponsor-sessions.json +0 -28
- package/data/docs/ux-source-evidence/portal-sponsor-settings.json +0 -18
- package/data/docs/ux-source-evidence/portal-sponsor-trainees.json +0 -36
- package/data/docs/ux-source-evidence/portal-sponsor.json +0 -52
- package/data/docs/ux-source-evidence/portal-trainee-competency.json +0 -42
- package/data/docs/ux-source-evidence/portal-trainee-credits.json +0 -40
- package/data/docs/ux-source-evidence/portal-trainee-mentors.json +0 -48
- package/data/docs/ux-source-evidence/portal-trainee-messages.json +0 -32
- package/data/docs/ux-source-evidence/portal-trainee-notifications.json +0 -35
- package/data/docs/ux-source-evidence/portal-trainee-profile.json +0 -26
- package/data/docs/ux-source-evidence/portal-trainee-request.json +0 -54
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +0 -36
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +0 -42
- package/data/docs/ux-source-evidence/portal-trainee-sessions-detail.json +0 -55
- package/data/docs/ux-source-evidence/portal-trainee-sessions.json +0 -36
- package/data/docs/ux-source-evidence/portal-trainee-settings-notifications.json +0 -38
- package/data/docs/ux-source-evidence/portal-trainee-settings.json +0 -18
- package/data/docs/ux-source-evidence/portal-trainee.json +0 -44
- package/data/docs/ux-source-evidence/portal-tv-detail.json +0 -39
- package/data/docs/ux-source-evidence/portal-tv.json +0 -44
- package/data/docs/ux-source-evidence/shared-admin-admin-shell.json +0 -35
- package/data/docs/ux-source-evidence/shared-auth-auth-primitives.json +0 -105
- package/data/docs/ux-source-evidence/shared-auth-auth-shell.json +0 -25
- package/data/docs/ux-source-evidence/shared-auth-onboarding-primitives.json +0 -52
- package/data/docs/ux-source-evidence/shared-auth-onboarding-shell.json +0 -28
- package/data/docs/ux-source-evidence/shared-auth-status-indicator.json +0 -41
- package/data/docs/ux-source-evidence/shared-portal-empty-state.json +0 -24
- package/data/docs/ux-source-evidence/shared-portal-loading-skeleton.json +0 -27
- package/data/docs/ux-source-evidence/shared-portal-notifications-dropdown.json +0 -39
- package/data/docs/ux-source-evidence/shared-portal-portal-shell.json +0 -36
- package/data/docs/ux-source-evidence/shared-portal-profile-form.json +0 -32
- package/data/docs/ux-source-evidence/shared-portal-settings-view.json +0 -81
- package/data/docs/ux-source-evidence/shared-portal-usage-sparkline.json +0 -18
- package/data/docs/ux-source-evidence/shared-portal-user-menu.json +0 -34
- package/data/manifests/ui-patterns.json +0 -1029
- package/data/manifests/ux-examples.json +0 -2177
- package/data/manifests/ux-patterns.json +0 -1135
- package/data/manifests/ux-source-inventory.json +0 -4669
- package/data/ui-parts/blocks/analytics-dashboard/AnalyticsMetricBlocks.tsx +0 -87
- package/data/ui-parts/blocks/analytics-dashboard/analytics-dashboard.css +0 -92
- package/data/ui-parts/blocks/semantic-pattern-parts/AuthParts.tsx +0 -48
- package/data/ui-parts/blocks/semantic-pattern-parts/ChoiceCards.tsx +0 -7
- package/data/ui-parts/blocks/semantic-pattern-parts/ListFilters.tsx +0 -9
- package/data/ui-parts/blocks/semantic-pattern-parts/OutcomeNotice.tsx +0 -7
- package/data/ui-parts/blocks/semantic-pattern-parts/ProductParts.tsx +0 -38
- package/data/ui-parts/blocks/semantic-pattern-parts/ProgressSteps.tsx +0 -6
- package/data/ui-parts/blocks/semantic-pattern-parts/ReviewList.tsx +0 -7
- package/data/ui-parts/blocks/semantic-pattern-parts/ValidatedField.tsx +0 -8
- package/data/ui-parts/blocks/semantic-pattern-parts/WorkflowParts.tsx +0 -339
- package/data/ui-parts/blocks/semantic-pattern-parts/auth-parts.css +0 -27
- package/data/ui-parts/blocks/semantic-pattern-parts/pattern-parts.css +0 -25
- package/data/ui-parts/blocks/semantic-pattern-parts/product-parts.css +0 -25
- package/data/ui-parts/blocks/semantic-pattern-parts/types.ts +0 -3
- package/data/ui-parts/blocks/semantic-pattern-parts/workflow-parts.css +0 -118
- package/data/ui-parts/blocks/ux-pattern-examples/AccessExamples.tsx +0 -46
- package/data/ui-parts/blocks/ux-pattern-examples/AccountExamples.tsx +0 -133
- package/data/ui-parts/blocks/ux-pattern-examples/ProductExamples.tsx +0 -79
- package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExample.tsx +0 -1761
- package/data/ui-parts/blocks/ux-pattern-examples/WorkflowExamples.tsx +0 -190
- package/data/ui-parts/blocks/ux-pattern-examples/copy.ts +0 -6
- package/data/ui-parts/blocks/ux-pattern-examples/ux-examples.css +0 -18
- package/data/ui-parts/blocks/ux-pattern-examples/workflow-copy.ts +0 -76
- package/data/ui-parts/blocks/ux-pattern-examples/workflow-definitions.ts +0 -1291
- package/data/ux-source-evidence/account-auth.json +0 -41
- package/data/ux-source-evidence/account-forgot-password.json +0 -36
- package/data/ux-source-evidence/account-invite.json +0 -63
- package/data/ux-source-evidence/account-locked.json +0 -63
- package/data/ux-source-evidence/account-login.json +0 -28
- package/data/ux-source-evidence/account-mfa.json +0 -62
- package/data/ux-source-evidence/account-onboarding-credentials.json +0 -52
- package/data/ux-source-evidence/account-onboarding-legal.json +0 -56
- package/data/ux-source-evidence/account-onboarding-orientation.json +0 -66
- package/data/ux-source-evidence/account-onboarding-profile.json +0 -50
- package/data/ux-source-evidence/account-onboarding.json +0 -69
- package/data/ux-source-evidence/account-reset-password.json +0 -27
- package/data/ux-source-evidence/account-session-expired.json +0 -62
- package/data/ux-source-evidence/account-signup.json +0 -33
- package/data/ux-source-evidence/account-verify-email.json +0 -34
- package/data/ux-source-evidence/account-welcome.json +0 -41
- package/data/ux-source-evidence/admin-analytics.json +0 -54
- package/data/ux-source-evidence/admin-audit.json +0 -31
- package/data/ux-source-evidence/admin-broadcasts-new.json +0 -36
- package/data/ux-source-evidence/admin-broadcasts.json +0 -41
- package/data/ux-source-evidence/admin-closures.json +0 -52
- package/data/ux-source-evidence/admin-devices.json +0 -51
- package/data/ux-source-evidence/admin-escalations.json +0 -35
- package/data/ux-source-evidence/admin-incidents.json +0 -54
- package/data/ux-source-evidence/admin-loading.json +0 -17
- package/data/ux-source-evidence/admin-onboarding-detail.json +0 -80
- package/data/ux-source-evidence/admin-onboarding.json +0 -62
- package/data/ux-source-evidence/admin-payments.json +0 -45
- package/data/ux-source-evidence/admin-providers.json +0 -38
- package/data/ux-source-evidence/admin-settings.json +0 -18
- package/data/ux-source-evidence/admin-subscriptions-detail-allocate.json +0 -42
- package/data/ux-source-evidence/admin-subscriptions-detail.json +0 -48
- package/data/ux-source-evidence/admin-subscriptions-new.json +0 -43
- package/data/ux-source-evidence/admin-subscriptions.json +0 -41
- package/data/ux-source-evidence/admin-transactions.json +0 -41
- package/data/ux-source-evidence/error.json +0 -30
- package/data/ux-source-evidence/loading.json +0 -19
- package/data/ux-source-evidence/marketing-blog.json +0 -48
- package/data/ux-source-evidence/marketing-contact.json +0 -36
- package/data/ux-source-evidence/marketing.json +0 -64
- package/data/ux-source-evidence/not-found.json +0 -28
- package/data/ux-source-evidence/portal-loading.json +0 -17
- package/data/ux-source-evidence/portal-mentor-messages.json +0 -31
- package/data/ux-source-evidence/portal-mentor-profile.json +0 -23
- package/data/ux-source-evidence/portal-mentor-requests-detail.json +0 -54
- package/data/ux-source-evidence/portal-mentor-sessions-detail-assess.json +0 -42
- package/data/ux-source-evidence/portal-mentor-settings.json +0 -18
- package/data/ux-source-evidence/portal-mentor.json +0 -38
- package/data/ux-source-evidence/portal-sessions-detail-live.json +0 -48
- package/data/ux-source-evidence/portal-sponsor-billing.json +0 -28
- package/data/ux-source-evidence/portal-sponsor-profile.json +0 -22
- package/data/ux-source-evidence/portal-sponsor-sessions.json +0 -28
- package/data/ux-source-evidence/portal-sponsor-settings.json +0 -18
- package/data/ux-source-evidence/portal-sponsor-trainees.json +0 -36
- package/data/ux-source-evidence/portal-sponsor.json +0 -52
- package/data/ux-source-evidence/portal-trainee-competency.json +0 -42
- package/data/ux-source-evidence/portal-trainee-credits.json +0 -40
- package/data/ux-source-evidence/portal-trainee-mentors.json +0 -48
- package/data/ux-source-evidence/portal-trainee-messages.json +0 -32
- package/data/ux-source-evidence/portal-trainee-notifications.json +0 -35
- package/data/ux-source-evidence/portal-trainee-profile.json +0 -26
- package/data/ux-source-evidence/portal-trainee-request.json +0 -54
- package/data/ux-source-evidence/portal-trainee-sessions-detail-feedback.json +0 -36
- package/data/ux-source-evidence/portal-trainee-sessions-detail-precheck.json +0 -42
- package/data/ux-source-evidence/portal-trainee-sessions-detail.json +0 -55
- package/data/ux-source-evidence/portal-trainee-sessions.json +0 -36
- package/data/ux-source-evidence/portal-trainee-settings-notifications.json +0 -38
- package/data/ux-source-evidence/portal-trainee-settings.json +0 -18
- package/data/ux-source-evidence/portal-trainee.json +0 -44
- package/data/ux-source-evidence/portal-tv-detail.json +0 -39
- package/data/ux-source-evidence/portal-tv.json +0 -44
- package/data/ux-source-evidence/shared-admin-admin-shell.json +0 -35
- package/data/ux-source-evidence/shared-auth-auth-primitives.json +0 -105
- package/data/ux-source-evidence/shared-auth-auth-shell.json +0 -25
- package/data/ux-source-evidence/shared-auth-onboarding-primitives.json +0 -52
- package/data/ux-source-evidence/shared-auth-onboarding-shell.json +0 -28
- package/data/ux-source-evidence/shared-auth-status-indicator.json +0 -41
- package/data/ux-source-evidence/shared-portal-empty-state.json +0 -24
- package/data/ux-source-evidence/shared-portal-loading-skeleton.json +0 -27
- package/data/ux-source-evidence/shared-portal-notifications-dropdown.json +0 -39
- package/data/ux-source-evidence/shared-portal-portal-shell.json +0 -36
- package/data/ux-source-evidence/shared-portal-profile-form.json +0 -32
- package/data/ux-source-evidence/shared-portal-settings-view.json +0 -81
- package/data/ux-source-evidence/shared-portal-usage-sparkline.json +0 -18
- package/data/ux-source-evidence/shared-portal-user-menu.json +0 -34
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
-
"file": "app/(auth)/auth/page.tsx",
|
|
4
|
-
"sha256": "ce905c95c982fc644e4d00624f35a87e2f9ee276ea49922dc34858bc9bddb0d7",
|
|
5
|
-
"imports": [
|
|
6
|
-
"next/link",
|
|
7
|
-
"react",
|
|
8
|
-
"lucide-react",
|
|
9
|
-
"@/components/ui/input",
|
|
10
|
-
"@/components/ui/badge",
|
|
11
|
-
"@/components/design-system/theme-toggle"
|
|
12
|
-
],
|
|
13
|
-
"tokens": [
|
|
14
|
-
"--color-bg-default",
|
|
15
|
-
"--color-bg-subtle",
|
|
16
|
-
"--color-border-default",
|
|
17
|
-
"--color-border-muted",
|
|
18
|
-
"--color-text-brand",
|
|
19
|
-
"--color-text-muted",
|
|
20
|
-
"--color-text-primary",
|
|
21
|
-
"--color-text-secondary"
|
|
22
|
-
],
|
|
23
|
-
"fragments": [
|
|
24
|
-
{
|
|
25
|
-
"id": "AuthIndexPage",
|
|
26
|
-
"line": 384,
|
|
27
|
-
"code": "export default function AuthIndexPage() {\n const [q, setQ] = useState(\"\");\n const filtered = FLOWS.map(g => ({\n ...g,\n pages: g.pages.filter(p =>\n !q ||\n p.title.toLowerCase().includes(q.toLowerCase()) ||\n p.desc.toLowerCase().includes(q.toLowerCase()) ||\n g.group.toLowerCase().includes(q.toLowerCase()) ||\n p.states?.some(s => s.label.toLowerCase().includes(q.toLowerCase()))\n ),\n })).filter(g => g.pages.length > 0);\n\n const totalStates = FLOWS.reduce((acc, g) => acc + g.pages.reduce((a, p) => a + (p.states?.length ?? 0), 0), 0);\n\n return (\n <div className=\"min-h-screen\" style={{ background: \"var(--color-bg-default)\", color: \"var(--color-text-primary)\" }}>\n {/* Header */}\n <header className=\"border-b sticky top-0 z-10\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-default)\" }}>\n <div className=\"max-w-5xl mx-auto px-6 h-14 flex items-center justify-between\">\n <Link href=\"/marketing\" className=\"flex items-center gap-2.5 group\">\n <img src=\"/mentix-logo.png\" alt=\"\" style={{ height: 22, width: 22 }} />\n <span className=\"text-[13px] font-semibold tracking-tight transition-opacity group-hover:opacity-70\">Mentix · Auth</span>\n </Link>\n <ThemeToggle />\n </div>\n </header>\n\n <div className=\"max-w-5xl mx-auto px-6 py-12 md:py-16\">\n {/* Title */}\n <div className=\"mb-10\">\n <Badge variant=\"outline\" className=\"rounded-full mb-4\">Design system reference</Badge>\n <h1 className=\"text-[2.25rem] font-semibold mb-3\" style={{ letterSpacing: \"-0.025em\", lineHeight: 1.1 }}>\n Authentication flows\n </h1>\n <p className=\"text-base max-w-2xl\" style={{ color: \"var(--color-text-secondary)\" }}>\n Every auth screen, every state. Built entirely with the Mentix design system —\n tokens, primitives, components, and patterns.\n </p>\n <div className=\"mt-5 flex items-center gap-2 text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>\n <Stat label=\"Flows\" value={FLOWS.length} />\n <Dot />\n <Stat label=\"Pages\" value={FLOWS.reduce((a, g) => a + g.pages.length, 0)} />\n <Dot />\n <Stat label=\"States\" value={totalStates} />\n </div>\n </div>\n\n {/* Search */}\n <div className=\"relative mb-8\">\n <Search size={15} className=\"absolute left-3 top-1/2 -translate-y-1/2\" style={{ color: \"var(--color-text-muted)\" }} />\n <Input\n value={q}\n onChange={(e) => setQ(e.target.value)}\n placeholder=\"Search flows, pages, or states…\"\n className=\"h-11 pl-10 pr-10\"\n />\n {q && (\n <button onClick={() => setQ(\"\")} className=\"absolute right-3 top-1/2 -translate-y-1/2 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-muted)\" }}>\n <X size={15} />\n </button>\n )}\n </div>\n\n {/* Flow groups */}\n <div className=\"space-y-10\">\n {filtered.map(g => (\n <section key={g.group}>\n <h2 className=\"text-[10px] font-semibold uppercase tracking-[0.15em] mb-3\" style={{ color: \"var(--color-text-brand)\" }}>\n {g.group}\n </h2>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-3\">\n {g.pages.map(p => (\n <article key={p.href} className=\"rounded-2xl border p-5 transition-shadow hover:shadow-md\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <Link href={p.href} className=\"group block\">\n <div className=\"flex items-start justify-between gap-3 mb-3\">\n <div>\n <h3 className=\"text-[15px] font-semibold mb-1 transition-colors group-hover:text-[var(--color-text-brand)]\">{p.title}</h3>\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{p.desc}</p>\n </div>\n <ArrowRight size={15} className=\"shrink-0 mt-1 transition-transform group-hover:translate-x-0.5\" style={{ color: \"var(--color-text-muted)\" }} />\n </div>\n </Link>\n {p.states && (\n <div className=\"flex flex-wrap gap-1.5 pt-3 mt-1 border-t\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n {p.states.map(s => (\n <Link\n key={s.href}\n href={s.href}\n className=\"text-[11px] px-2 py-1 rounded-md transition-colors\"\n style={{\n color: \"var(--color-text-secondary)\",\n background: \"var(--color-bg-subtle)\",\n }}\n >\n {s.label}\n </Link>\n ))}\n </div>\n )}\n </article>\n ))}\n </div>\n </section>\n ))}\n {filtered.length === 0 && (\n <div className=\"text-center py-16\" style={{ color: \"var(--color-text-muted)\" }}>\n <p className=\"text-sm\">No flows match \"{q}\"</p>\n </div>\n )}\n </div>\n\n {/* Footer note */}\n <footer className=\"mt-16 pt-6 border-t text-[11px] flex items-center justify-between\" style={{ borderColor: \"var(--color-border-muted)\", color: \"var(--color-text-muted)\" }}>\n <span>Demo credentials — email: <code style={{ color: \"var(--color-text-secondary)\" }}>demo@mentix.world</code> · password: <code style={{ color: \"var(--color-text-secondary)\" }}>Demo1234!</code></span>\n <Link href=\"/marketing\" className=\"hover:underline underline-offset-4\">← Back to marketing</Link>\n </footer>\n </div>\n </div>\n );\n}"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"id": "Stat",
|
|
31
|
-
"line": 506,
|
|
32
|
-
"code": "function Stat({ label, value }: { label: string; value: number }) {\n return (\n <span><strong style={{ color: \"var(--color-text-primary)\" }} className=\"font-semibold tabular-nums\">{value}</strong> {label}</span>\n );\n}"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"id": "Dot",
|
|
36
|
-
"line": 511,
|
|
37
|
-
"code": "function Dot() {\n return <span style={{ color: \"var(--color-text-muted)\" }}>·</span>;\n}"
|
|
38
|
-
}
|
|
39
|
-
],
|
|
40
|
-
"content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState } from \"react\";\nimport { ArrowRight, Search, X } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { ThemeToggle } from \"@/components/design-system/theme-toggle\";\n\ntype Flow = {\n group: string;\n pages: { title: string; href: string; desc: string; states?: { label: string; href: string }[] }[];\n};\n\nconst FLOWS: Flow[] = [\n {\n group: \"Sign in\",\n pages: [\n {\n title: \"Sign in\", href: \"/login\", desc: \"Email/password, magic link, SSO, remember me.\",\n states: [\n { label: \"Default\", href: \"/login\" },\n { label: \"Loading\", href: \"/login?state=loading\" },\n { label: \"Invalid credentials\", href: \"/login?state=error\" },\n { label: \"Suspended\", href: \"/login?state=suspended\" },\n { label: \"Magic link sent\", href: \"/login?state=magic-sent\" },\n ],\n },\n ],\n },\n {\n group: \"Sign up\",\n pages: [\n {\n title: \"Sign up\", href: \"/signup\", desc: \"Account creation with social SSO + terms.\",\n states: [\n { label: \"Default\", href: \"/signup\" },\n { label: \"Email taken\", href: \"/signup?state=email-taken\" },\n { label: \"Invite-only\", href: \"/signup?state=invite-only\" },\n ],\n },\n ],\n },\n {\n group: \"Password recovery\",\n pages: [\n {\n title: \"Forgot password\", href: \"/forgot-password\", desc: \"Request reset email.\",\n states: [\n { label: \"Default\", href: \"/forgot-password\" },\n { label: \"Email sent\", href: \"/forgot-password?state=sent\" },\n { label: \"Invalid email\", href: \"/forgot-password?state=invalid\" },\n { label: \"Link expired\", href: \"/forgot-password?state=expired\" },\n ],\n },\n {\n title: \"Reset password\", href: \"/reset-password\", desc: \"Set new password from reset link.\",\n states: [\n { label: \"Default\", href: \"/reset-password\" },\n { label: \"Success\", href: \"/reset-password?state=success\" },\n { label: \"Mismatch\", href: \"/reset-password?state=mismatch\" },\n { label: \"Link expired\", href: \"/reset-password?state=expired\" },\n ],\n },\n ],\n },\n {\n group: \"Email verification\",\n pages: [\n {\n title: \"Verify email\", href: \"/verify-email\", desc: \"Waiting for verification + resend.\",\n states: [\n { label: \"Waiting\", href: \"/verify-email\" },\n { label: \"Success\", href: \"/verify-email?state=success\" },\n { label: \"Expired\", href: \"/verify-email?state=expired\" },\n ],\n },\n ],\n },\n {\n group: \"Multi-factor\",\n pages: [\n {\n title: \"MFA\", href: \"/mfa\", desc: \"OTP entry, setup, backup, recovery.\",\n states: [\n { label: \"Verify (OTP)\", href: \"/mfa\" },\n { label: \"Verify — error\", href: \"/mfa?state=error\" },\n { label: \"Setup authenticator\", href: \"/mfa?mode=setup\" },\n { label: \"Backup codes\", href: \"/mfa?mode=backup\" },\n { label: \"Recovery (backup code)\", href: \"/mfa?mode=recovery\" },\n ],\n },\n ],\n },\n {\n group: \"Invitations\",\n pages: [\n {\n title: \"Accept invite\", href: \"/invite\", desc: \"Workspace invite acceptance flow.\",\n states: [\n { label: \"New user\", href: \"/invite\" },\n { label: \"Existing user\", href: \"/invite?state=existing-user\" },\n { label: \"Code entry\", href: \"/invite?state=code-entry\" },\n { label: \"Expired invite\", href: \"/invite?state=expired\" },\n ],\n },\n ],\n },\n {\n group: \"Security\",\n pages: [\n {\n title: \"Locked / Security\", href: \"/locked\", desc: \"Lockout, suspicious activity, device verify.\",\n states: [\n { label: \"Too many attempts\", href: \"/locked\" },\n { label: \"Suspicious login\", href: \"/locked?state=suspicious\" },\n { label: \"Device verification\", href: \"/locked?state=device-verify\" },\n { label: \"Confirm action modal\", href: \"/locked?state=confirm-action\" },\n ],\n },\n ],\n },\n {\n group: \"Session\",\n pages: [\n {\n title: \"Session expired\", href: \"/session-expired\", desc: \"Expired, logged-out, concurrent sessions.\",\n states: [\n { label: \"Modal — mid-action\", href: \"/session-expired\" },\n { label: \"Full page\", href: \"/session-expired?state=expired-full\" },\n { label: \"Logged out\", href: \"/session-expired?state=logged-out\" },\n { label: \"Concurrent session\", href: \"/session-expired?state=concurrent\" },\n ],\n },\n ],\n },\n {\n group: \"Onboarding\",\n pages: [\n {\n title: \"Welcome\", href: \"/welcome\", desc: \"Post-signup celebration + CTA to start onboarding.\",\n states: [\n { label: \"Celebrate\", href: \"/welcome\" },\n { label: \"Redirect\", href: \"/welcome?state=redirect\" },\n ],\n },\n {\n title: \"Onboarding hub\", href: \"/onboarding\", desc: \"Resumable 4-step wizard. Pick up where you left off.\",\n states: [\n { label: \"No role (picker)\", href: \"/onboarding\" },\n { label: \"Trainee — fresh\", href: \"/onboarding?role=trainee&demo=fresh\" },\n { label: \"Trainee — midway\", href: \"/onboarding?role=trainee&demo=midway\" },\n { label: \"Trainee — pending review\", href: \"/onboarding?role=trainee&demo=review\" },\n { label: \"Trainee — complete\", href: \"/onboarding?role=trainee&demo=complete\" },\n { label: \"Mentor — fresh\", href: \"/onboarding?role=mentor&demo=fresh\" },\n { label: \"Sponsor — fresh\", href: \"/onboarding?role=sponsor&demo=fresh\" },\n ],\n },\n {\n title: \"Step 1 — Profile\", href: \"/onboarding/profile?role=trainee\", desc: \"Name, photo, languages, region.\",\n states: [\n { label: \"Trainee\", href: \"/onboarding/profile?role=trainee\" },\n { label: \"Mentor\", href: \"/onboarding/profile?role=mentor\" },\n { label: \"Sponsor\", href: \"/onboarding/profile?role=sponsor\" },\n ],\n },\n {\n title: \"Step 2 — Credentials\", href: \"/onboarding/credentials?role=trainee\", desc: \"Professional info & verification.\",\n states: [\n { label: \"Trainee\", href: \"/onboarding/credentials?role=trainee\" },\n { label: \"Mentor\", href: \"/onboarding/credentials?role=mentor\" },\n { label: \"Sponsor\", href: \"/onboarding/credentials?role=sponsor\" },\n ],\n },\n {\n title: \"Step 3 — Legal\", href: \"/onboarding/legal?role=trainee\", desc: \"Contracts & agreements to sign.\",\n states: [\n { label: \"Trainee\", href: \"/onboarding/legal?role=trainee\" },\n { label: \"Mentor\", href: \"/onboarding/legal?role=mentor\" },\n { label: \"Sponsor\", href: \"/onboarding/legal?role=sponsor\" },\n ],\n },\n {\n title: \"Step 4 — Orientation\", href: \"/onboarding/orientation?role=trainee\", desc: \"Final acknowledgements & activation.\",\n states: [\n { label: \"Trainee\", href: \"/onboarding/orientation?role=trainee\" },\n { label: \"Mentor\", href: \"/onboarding/orientation?role=mentor\" },\n { label: \"Sponsor (credits)\", href: \"/onboarding/orientation?role=sponsor\" },\n ],\n },\n ],\n },\n {\n group: \"Admin Console\",\n pages: [\n {\n title: \"Onboarding queue\", href: \"/admin/onboarding\", desc: \"Pending applications dashboard with SLA highlighting.\",\n states: [\n { label: \"Dashboard\", href: \"/admin/onboarding\" },\n { label: \"Filter — Trainees\", href: \"/admin/onboarding?filter=hardware_pending\" },\n { label: \"Filter — Mentors\", href: \"/admin/onboarding?filter=under_verification\" },\n ],\n },\n {\n title: \"Mentor review (MAV M1–M6)\", href: \"/admin/onboarding/app-001\", desc: \"Per-mentor 6-item verification checklist.\",\n states: [\n { label: \"Active mentor (Prof. Haji)\", href: \"/admin/onboarding/app-001\" },\n { label: \"SLA breach (Dr. Yamamoto)\", href: \"/admin/onboarding/app-002\" },\n { label: \"Legal pending (Dr. Dubois)\", href: \"/admin/onboarding/app-003\" },\n ],\n },\n {\n title: \"Trainee review + Hardware (ME1–ME6)\", href: \"/admin/onboarding/app-101\", desc: \"Trainee verification + 4-gate hardware readiness.\",\n states: [\n { label: \"Mid hardware install\", href: \"/admin/onboarding/app-101\" },\n { label: \"SLA breach\", href: \"/admin/onboarding/app-102\" },\n { label: \"Fresh application\", href: \"/admin/onboarding/app-103\" },\n { label: \"Hardware complete\", href: \"/admin/onboarding/app-104\" },\n ],\n },\n {\n title: \"Audit log\", href: \"/admin/audit\", desc: \"Compliance log of all verification actions.\",\n },\n ],\n },\n {\n group: \"Credit & Subscription\",\n pages: [\n {\n title: \"Subscriptions (admin)\", href: \"/admin/subscriptions\", desc: \"All industry-partner subscriptions with utilisation.\",\n states: [\n { label: \"List\", href: \"/admin/subscriptions\" },\n { label: \"Detail (Olympus, Growth)\", href: \"/admin/subscriptions/sub-001\" },\n { label: \"Detail (Boston Sci, Scale)\", href: \"/admin/subscriptions/sub-002\" },\n { label: \"Detail (Cook, low credits)\", href: \"/admin/subscriptions/sub-003\" },\n { label: \"Detail (Medtronic, Enterprise)\", href: \"/admin/subscriptions/sub-004\" },\n { label: \"New subscription\", href: \"/admin/subscriptions/new\" },\n { label: \"Allocate credits flow\", href: \"/admin/subscriptions/sub-001/allocate\" },\n ],\n },\n {\n title: \"Transaction log (admin)\", href: \"/admin/transactions\", desc: \"Append-only credit ledger with type filter.\",\n },\n {\n title: \"Sponsor portal\", href: \"/portal/sponsor\", desc: \"Industry partner view: subscription, trainees, usage.\",\n states: [\n { label: \"Overview\", href: \"/portal/sponsor\" },\n { label: \"Trainees\", href: \"/portal/sponsor/trainees\" },\n ],\n },\n {\n title: \"Trainee portal\", href: \"/portal/trainee\", desc: \"Mentee credit dashboard with sessions.\",\n },\n ],\n },\n {\n group: \"Session Request & Matching\",\n pages: [\n {\n title: \"Trainee — Request session\", href: \"/portal/trainee/request\", desc: \"Submit a session request with mode selector + case docs.\",\n states: [\n { label: \"Generic broadcast\", href: \"/portal/trainee/request\" },\n { label: \"Specific mentor (preselected)\", href: \"/portal/trainee/request?mentor=men-001\" },\n ],\n },\n {\n title: \"Trainee — Browse mentors\", href: \"/portal/trainee/mentors\", desc: \"Filterable mentor directory with ratings.\",\n },\n {\n title: \"Mentor — Dashboard\", href: \"/portal/mentor\", desc: \"Available requests + upcoming sessions + my rating.\",\n },\n {\n title: \"Mentor — Request detail\", href: \"/portal/mentor/requests/req-001\", desc: \"Trainee history, case docs, accept/decline.\",\n states: [\n { label: \"Specific (Hannah → Haji)\", href: \"/portal/mentor/requests/req-001\" },\n { label: \"Broadcast (Sarah, first ESD)\", href: \"/portal/mentor/requests/req-002\" },\n { label: \"Escalated (Aaron, ERCP)\", href: \"/portal/mentor/requests/req-003\" },\n ],\n },\n ],\n },\n {\n group: \"Pre-Session Preparation\",\n pages: [\n {\n title: \"Trainee — Session detail\", href: \"/portal/trainee/sessions/req-004\", desc: \"Alignment record + docs workspace + chat.\",\n },\n {\n title: \"Connectivity Pre-Check\", href: \"/portal/trainee/sessions/req-004/precheck\", desc: \"5-step wizard with fail-safe ack.\",\n },\n ],\n },\n {\n group: \"Live Session Execution\",\n pages: [\n {\n title: \"Live session interface\", href: \"/portal/sessions/req-004/live\", desc: \"Multi-party video + recording + safety events + fail-safe.\",\n },\n ],\n },\n {\n group: \"Post-Session Workflow\",\n pages: [\n {\n title: \"Mentor — Structured Assessment\", href: \"/portal/mentor/sessions/req-004/assess\", desc: \"GRS + entrustment level + narrative feedback.\",\n },\n {\n title: \"Trainee — Rate mentor\", href: \"/portal/trainee/sessions/req-004/feedback\", desc: \"4-dimension star rating.\",\n },\n ],\n },\n {\n group: \"Competency & Analytics\",\n pages: [\n {\n title: \"Trainee — Competency dashboard\", href: \"/portal/trainee/competency\", desc: \"Case log, skill trends, milestones, per-domain breakdown.\",\n },\n {\n title: \"Admin — Platform analytics\", href: \"/admin/analytics\", desc: \"Overview, mentor performance, trainee progression, risk flags.\",\n },\n ],\n },\n {\n group: \"Profile & Account\",\n pages: [\n { title: \"Trainee profile\", href: \"/portal/trainee/profile\", desc: \"Bio, languages, region, procedures requiring mentorship, experience per procedure.\" },\n { title: \"Mentor profile\", href: \"/portal/mentor/profile\", desc: \"Professional title, expertise, languages, hourly rate.\" },\n { title: \"Sponsor profile\", href: \"/portal/sponsor/profile\", desc: \"Company, industry, billing email.\" },\n ],\n },\n {\n group: \"Empty states\",\n pages: [\n { title: \"Trainee — Sessions\", href: \"/portal/trainee/sessions\", desc: \"Upcoming / Pending / Past with empty-state for Past.\" },\n { title: \"Trainee — Credits\", href: \"/portal/trainee/credits\", desc: \"Allocation history + consumption history.\" },\n { title: \"Trainee — Messages\", href: \"/portal/trainee/messages\", desc: \"Active chat threads.\" },\n { title: \"Trainee — Notifications\", href: \"/portal/trainee/notifications\", desc: \"All / Unread / Archived with empty state.\" },\n { title: \"Mentor — Messages\", href: \"/portal/mentor/messages\", desc: \"Active trainee chats.\" },\n { title: \"Sponsor — Sessions\", href: \"/portal/sponsor/sessions\", desc: \"Delivered sessions log.\" },\n { title: \"Sponsor — Billing\", href: \"/portal/sponsor/billing\", desc: \"Invoices & receipts.\" },\n ],\n },\n {\n group: \"Mentix TV\",\n pages: [\n { title: \"TV hub\", href: \"/portal/tv\", desc: \"Live + Coming up + Past broadcasts.\" },\n {\n title: \"Broadcast viewer\", href: \"/portal/tv/tv-001\", desc: \"Live player with YouTube embed + role badges.\",\n states: [\n { label: \"Live (with YT embed)\", href: \"/portal/tv/tv-001\" },\n { label: \"Scheduled\", href: \"/portal/tv/tv-002\" },\n { label: \"Cohort-restricted\", href: \"/portal/tv/tv-003\" },\n { label: \"Ended\", href: \"/portal/tv/tv-004\" },\n ],\n },\n { title: \"Admin — Broadcasts\", href: \"/admin/broadcasts\", desc: \"Manage and schedule live broadcasts.\" },\n { title: \"Admin — New broadcast\", href: \"/admin/broadcasts/new\", desc: \"Create + schedule form.\" },\n ],\n },\n {\n group: \"Session Closure & Operations\",\n pages: [\n { title: \"Closure queue\", href: \"/admin/closures\", desc: \"Sessions ready + pending with blocking issues.\" },\n { title: \"Matching escalations\", href: \"/admin/escalations\", desc: \"Unmatched broadcasts past 4-hour SLA + manual mentor assignment.\" },\n { title: \"Incidents\", href: \"/admin/incidents\", desc: \"Quality/safety/compliance/technical incidents.\" },\n { title: \"Mentor payments\", href: \"/admin/payments\", desc: \"Monthly payment cycle: queued → invoiced → paid.\" },\n ],\n },\n {\n group: \"Teleproctoring\",\n pages: [\n { title: \"Devices\", href: \"/admin/devices\", desc: \"Live device status across all sites + providers.\" },\n { title: \"Providers\", href: \"/admin/providers\", desc: \"API integrations with Surgease, Proximie, Rods & Cones.\" },\n ],\n },\n {\n group: \"Settings\",\n pages: [\n { title: \"Notification preferences\", href: \"/portal/trainee/settings/notifications\", desc: \"Per-event delivery channel: push / email / both / none.\" },\n ],\n },\n];\n\nexport default function AuthIndexPage() {\n const [q, setQ] = useState(\"\");\n const filtered = FLOWS.map(g => ({\n ...g,\n pages: g.pages.filter(p =>\n !q ||\n p.title.toLowerCase().includes(q.toLowerCase()) ||\n p.desc.toLowerCase().includes(q.toLowerCase()) ||\n g.group.toLowerCase().includes(q.toLowerCase()) ||\n p.states?.some(s => s.label.toLowerCase().includes(q.toLowerCase()))\n ),\n })).filter(g => g.pages.length > 0);\n\n const totalStates = FLOWS.reduce((acc, g) => acc + g.pages.reduce((a, p) => a + (p.states?.length ?? 0), 0), 0);\n\n return (\n <div className=\"min-h-screen\" style={{ background: \"var(--color-bg-default)\", color: \"var(--color-text-primary)\" }}>\n {/* Header */}\n <header className=\"border-b sticky top-0 z-10\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-default)\" }}>\n <div className=\"max-w-5xl mx-auto px-6 h-14 flex items-center justify-between\">\n <Link href=\"/marketing\" className=\"flex items-center gap-2.5 group\">\n <img src=\"/mentix-logo.png\" alt=\"\" style={{ height: 22, width: 22 }} />\n <span className=\"text-[13px] font-semibold tracking-tight transition-opacity group-hover:opacity-70\">Mentix · Auth</span>\n </Link>\n <ThemeToggle />\n </div>\n </header>\n\n <div className=\"max-w-5xl mx-auto px-6 py-12 md:py-16\">\n {/* Title */}\n <div className=\"mb-10\">\n <Badge variant=\"outline\" className=\"rounded-full mb-4\">Design system reference</Badge>\n <h1 className=\"text-[2.25rem] font-semibold mb-3\" style={{ letterSpacing: \"-0.025em\", lineHeight: 1.1 }}>\n Authentication flows\n </h1>\n <p className=\"text-base max-w-2xl\" style={{ color: \"var(--color-text-secondary)\" }}>\n Every auth screen, every state. Built entirely with the Mentix design system —\n tokens, primitives, components, and patterns.\n </p>\n <div className=\"mt-5 flex items-center gap-2 text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>\n <Stat label=\"Flows\" value={FLOWS.length} />\n <Dot />\n <Stat label=\"Pages\" value={FLOWS.reduce((a, g) => a + g.pages.length, 0)} />\n <Dot />\n <Stat label=\"States\" value={totalStates} />\n </div>\n </div>\n\n {/* Search */}\n <div className=\"relative mb-8\">\n <Search size={15} className=\"absolute left-3 top-1/2 -translate-y-1/2\" style={{ color: \"var(--color-text-muted)\" }} />\n <Input\n value={q}\n onChange={(e) => setQ(e.target.value)}\n placeholder=\"Search flows, pages, or states…\"\n className=\"h-11 pl-10 pr-10\"\n />\n {q && (\n <button onClick={() => setQ(\"\")} className=\"absolute right-3 top-1/2 -translate-y-1/2 transition-opacity hover:opacity-70\" style={{ color: \"var(--color-text-muted)\" }}>\n <X size={15} />\n </button>\n )}\n </div>\n\n {/* Flow groups */}\n <div className=\"space-y-10\">\n {filtered.map(g => (\n <section key={g.group}>\n <h2 className=\"text-[10px] font-semibold uppercase tracking-[0.15em] mb-3\" style={{ color: \"var(--color-text-brand)\" }}>\n {g.group}\n </h2>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-3\">\n {g.pages.map(p => (\n <article key={p.href} className=\"rounded-2xl border p-5 transition-shadow hover:shadow-md\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <Link href={p.href} className=\"group block\">\n <div className=\"flex items-start justify-between gap-3 mb-3\">\n <div>\n <h3 className=\"text-[15px] font-semibold mb-1 transition-colors group-hover:text-[var(--color-text-brand)]\">{p.title}</h3>\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>{p.desc}</p>\n </div>\n <ArrowRight size={15} className=\"shrink-0 mt-1 transition-transform group-hover:translate-x-0.5\" style={{ color: \"var(--color-text-muted)\" }} />\n </div>\n </Link>\n {p.states && (\n <div className=\"flex flex-wrap gap-1.5 pt-3 mt-1 border-t\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n {p.states.map(s => (\n <Link\n key={s.href}\n href={s.href}\n className=\"text-[11px] px-2 py-1 rounded-md transition-colors\"\n style={{\n color: \"var(--color-text-secondary)\",\n background: \"var(--color-bg-subtle)\",\n }}\n >\n {s.label}\n </Link>\n ))}\n </div>\n )}\n </article>\n ))}\n </div>\n </section>\n ))}\n {filtered.length === 0 && (\n <div className=\"text-center py-16\" style={{ color: \"var(--color-text-muted)\" }}>\n <p className=\"text-sm\">No flows match \"{q}\"</p>\n </div>\n )}\n </div>\n\n {/* Footer note */}\n <footer className=\"mt-16 pt-6 border-t text-[11px] flex items-center justify-between\" style={{ borderColor: \"var(--color-border-muted)\", color: \"var(--color-text-muted)\" }}>\n <span>Demo credentials — email: <code style={{ color: \"var(--color-text-secondary)\" }}>demo@mentix.world</code> · password: <code style={{ color: \"var(--color-text-secondary)\" }}>Demo1234!</code></span>\n <Link href=\"/marketing\" className=\"hover:underline underline-offset-4\">← Back to marketing</Link>\n </footer>\n </div>\n </div>\n );\n}\n\nfunction Stat({ label, value }: { label: string; value: number }) {\n return (\n <span><strong style={{ color: \"var(--color-text-primary)\" }} className=\"font-semibold tabular-nums\">{value}</strong> {label}</span>\n );\n}\nfunction Dot() {\n return <span style={{ color: \"var(--color-text-muted)\" }}>·</span>;\n}\n"
|
|
41
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
-
"file": "app/(auth)/forgot-password/page.tsx",
|
|
4
|
-
"sha256": "fe40fa090ce63964bb61d13b9310525dbddd6359c393d4bd4c8251ce5f388240",
|
|
5
|
-
"imports": [
|
|
6
|
-
"next/link",
|
|
7
|
-
"react",
|
|
8
|
-
"next/navigation",
|
|
9
|
-
"lucide-react",
|
|
10
|
-
"@/components/ui/button",
|
|
11
|
-
"@/components/ui/input",
|
|
12
|
-
"@/components/auth/auth-shell",
|
|
13
|
-
"@/components/auth/auth-primitives"
|
|
14
|
-
],
|
|
15
|
-
"tokens": [
|
|
16
|
-
"--color-bg-brand-muted",
|
|
17
|
-
"--color-border-default",
|
|
18
|
-
"--color-text-brand",
|
|
19
|
-
"--color-text-muted",
|
|
20
|
-
"--color-text-primary",
|
|
21
|
-
"--color-text-secondary"
|
|
22
|
-
],
|
|
23
|
-
"fragments": [
|
|
24
|
-
{
|
|
25
|
-
"id": "ForgotPasswordPage",
|
|
26
|
-
"line": 13,
|
|
27
|
-
"code": "export default function ForgotPasswordPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n\n const [email, setEmail] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n const [sent, setSent] = useState(false);\n const [invalid, setInvalid] = useState(false);\n const [resendCooldown, setResendCooldown] = useState(0);\n\n const state: State = useMemo(() => {\n if (forced) return forced;\n if (submitting) return \"loading\";\n if (invalid) return \"invalid\";\n if (sent) return \"sent\";\n return \"idle\";\n }, [forced, submitting, sent, invalid]);\n\n useEffect(() => {\n if (resendCooldown > 0) {\n const t = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);\n return () => clearTimeout(t);\n }\n }, [resendCooldown]);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setSubmitting(true);\n setInvalid(false);\n setTimeout(() => {\n setSubmitting(false);\n if (!/.+@.+\\..+/.test(email) || email.includes(\"invalid\")) setInvalid(true);\n else { setSent(true); setResendCooldown(30); }\n }, 900);\n };\n\n const resend = () => {\n if (resendCooldown > 0) return;\n setResendCooldown(30);\n };\n\n // ─── Expired link state ──\n if (state === \"expired\") {\n return (\n <AuthShell\n kicker=\"Reset link expired\"\n title=\"That link has expired\"\n subtitle=\"For security, password reset links expire after 1 hour. Request a new one to continue.\"\n >\n <div className=\"space-y-4\">\n <InlineAlert variant=\"warning\" title=\"Link no longer valid\">\n The reset link you used has expired or has already been used.\n </InlineAlert>\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\" onClick={() => window.location.href = \"/forgot-password\"}>\n Send a new reset link <ArrowRight size={14} />\n </Button>\n <BackToLogin />\n </div>\n </AuthShell>\n );\n }\n\n // ─── Sent state ──\n if (state === \"sent\") {\n return (\n <AuthShell\n kicker=\"Check your inbox\"\n title={<>We sent a reset link<br />to your email</>}\n subtitle={<>If an account exists for <strong style={{ color: \"var(--color-text-primary)\" }}>{email || \"your address\"}</strong>, you'll receive an email with reset instructions within a minute.</>}\n >\n <div className=\"space-y-4\">\n <div className=\"rounded-2xl border p-5 flex items-center gap-4\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Mail size={18} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium mb-0.5\">Email sent</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>The link expires in 60 minutes.</div>\n </div>\n </div>\n\n <div className=\"text-[12px] text-center space-y-1.5\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p>\n Didn't get the email? Check your spam folder, or{\" \"}\n <button onClick={resend} disabled={resendCooldown > 0} className=\"font-medium hover:underline underline-offset-4 disabled:opacity-50 disabled:no-underline\" style={{ color: \"var(--color-text-brand)\" }}>\n {resendCooldown > 0 ? `Resend in ${resendCooldown}s` : \"resend it\"}\n </button>.\n </p>\n <p>\n Wrong email?{\" \"}\n <button onClick={() => setSent(false)} className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Use a different email\n </button>\n </p>\n </div>\n\n <BackToLogin />\n </div>\n </AuthShell>\n );\n }\n\n // ─── Idle / Loading / Invalid ──\n return (\n <AuthShell\n kicker=\"Forgot password\"\n title=\"Reset your password\"\n subtitle=\"Enter the email you used to sign up. We'll send you a link to set a new password.\"\n footer={<BackToLogin />}\n >\n {state === \"invalid\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title=\"We couldn't find that email\">\n Double-check your spelling or sign up for a new account.\n </InlineAlert>\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n <Field label=\"Work email\" htmlFor=\"email\">\n <Input\n id=\"email\"\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"you@hospital.com\"\n autoComplete=\"email\"\n className=\"h-10\"\n required\n aria-invalid={state === \"invalid\"}\n />\n </Field>\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" disabled={submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Sending…</> : <>Send reset link <ArrowRight size={14} /></>}\n </Button>\n </form>\n </AuthShell>\n );\n}"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"id": "BackToLogin",
|
|
31
|
-
"line": 153,
|
|
32
|
-
"code": "function BackToLogin() {\n return (\n <Link\n href=\"/login\"\n className=\"inline-flex items-center justify-center gap-1.5 w-full h-9 rounded-md text-[13px] font-medium transition-opacity hover:opacity-70\"\n style={{ color: \"var(--color-text-secondary)\" }}\n >\n <ArrowLeft size={13} strokeWidth={1.8} /> Back to sign in\n </Link>\n );\n}"
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
"content": "\"use client\";\nimport Link from \"next/link\";\nimport { useState, useMemo, useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowLeft, ArrowRight, Mail } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport { Field, InlineAlert, Spinner } from \"@/components/auth/auth-primitives\";\n\ntype State = \"idle\" | \"loading\" | \"sent\" | \"invalid\" | \"expired\";\n\nexport default function ForgotPasswordPage() {\n const search = useSearchParams();\n const forced = search.get(\"state\") as State | null;\n\n const [email, setEmail] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n const [sent, setSent] = useState(false);\n const [invalid, setInvalid] = useState(false);\n const [resendCooldown, setResendCooldown] = useState(0);\n\n const state: State = useMemo(() => {\n if (forced) return forced;\n if (submitting) return \"loading\";\n if (invalid) return \"invalid\";\n if (sent) return \"sent\";\n return \"idle\";\n }, [forced, submitting, sent, invalid]);\n\n useEffect(() => {\n if (resendCooldown > 0) {\n const t = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);\n return () => clearTimeout(t);\n }\n }, [resendCooldown]);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setSubmitting(true);\n setInvalid(false);\n setTimeout(() => {\n setSubmitting(false);\n if (!/.+@.+\\..+/.test(email) || email.includes(\"invalid\")) setInvalid(true);\n else { setSent(true); setResendCooldown(30); }\n }, 900);\n };\n\n const resend = () => {\n if (resendCooldown > 0) return;\n setResendCooldown(30);\n };\n\n // ─── Expired link state ──\n if (state === \"expired\") {\n return (\n <AuthShell\n kicker=\"Reset link expired\"\n title=\"That link has expired\"\n subtitle=\"For security, password reset links expire after 1 hour. Request a new one to continue.\"\n >\n <div className=\"space-y-4\">\n <InlineAlert variant=\"warning\" title=\"Link no longer valid\">\n The reset link you used has expired or has already been used.\n </InlineAlert>\n <Button variant=\"brand\" size=\"lg\" className=\"w-full\" onClick={() => window.location.href = \"/forgot-password\"}>\n Send a new reset link <ArrowRight size={14} />\n </Button>\n <BackToLogin />\n </div>\n </AuthShell>\n );\n }\n\n // ─── Sent state ──\n if (state === \"sent\") {\n return (\n <AuthShell\n kicker=\"Check your inbox\"\n title={<>We sent a reset link<br />to your email</>}\n subtitle={<>If an account exists for <strong style={{ color: \"var(--color-text-primary)\" }}>{email || \"your address\"}</strong>, you'll receive an email with reset instructions within a minute.</>}\n >\n <div className=\"space-y-4\">\n <div className=\"rounded-2xl border p-5 flex items-center gap-4\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Mail size={18} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium mb-0.5\">Email sent</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>The link expires in 60 minutes.</div>\n </div>\n </div>\n\n <div className=\"text-[12px] text-center space-y-1.5\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p>\n Didn't get the email? Check your spam folder, or{\" \"}\n <button onClick={resend} disabled={resendCooldown > 0} className=\"font-medium hover:underline underline-offset-4 disabled:opacity-50 disabled:no-underline\" style={{ color: \"var(--color-text-brand)\" }}>\n {resendCooldown > 0 ? `Resend in ${resendCooldown}s` : \"resend it\"}\n </button>.\n </p>\n <p>\n Wrong email?{\" \"}\n <button onClick={() => setSent(false)} className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Use a different email\n </button>\n </p>\n </div>\n\n <BackToLogin />\n </div>\n </AuthShell>\n );\n }\n\n // ─── Idle / Loading / Invalid ──\n return (\n <AuthShell\n kicker=\"Forgot password\"\n title=\"Reset your password\"\n subtitle=\"Enter the email you used to sign up. We'll send you a link to set a new password.\"\n footer={<BackToLogin />}\n >\n {state === \"invalid\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title=\"We couldn't find that email\">\n Double-check your spelling or sign up for a new account.\n </InlineAlert>\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n <Field label=\"Work email\" htmlFor=\"email\">\n <Input\n id=\"email\"\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"you@hospital.com\"\n autoComplete=\"email\"\n className=\"h-10\"\n required\n aria-invalid={state === \"invalid\"}\n />\n </Field>\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" disabled={submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Sending…</> : <>Send reset link <ArrowRight size={14} /></>}\n </Button>\n </form>\n </AuthShell>\n );\n}\n\nfunction BackToLogin() {\n return (\n <Link\n href=\"/login\"\n className=\"inline-flex items-center justify-center gap-1.5 w-full h-9 rounded-md text-[13px] font-medium transition-opacity hover:opacity-70\"\n style={{ color: \"var(--color-text-secondary)\" }}\n >\n <ArrowLeft size={13} strokeWidth={1.8} /> Back to sign in\n </Link>\n );\n}\n"
|
|
36
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
-
"file": "app/(auth)/invite/page.tsx",
|
|
4
|
-
"sha256": "0ce563cde47927c0f8fe56de424da82b9e5e3a57f49bbcfc8daf176239d37b44",
|
|
5
|
-
"imports": [
|
|
6
|
-
"next/link",
|
|
7
|
-
"react",
|
|
8
|
-
"next/navigation",
|
|
9
|
-
"lucide-react",
|
|
10
|
-
"@/components/ui/button",
|
|
11
|
-
"@/components/ui/input",
|
|
12
|
-
"@/components/auth/auth-shell",
|
|
13
|
-
"@/components/auth/auth-primitives"
|
|
14
|
-
],
|
|
15
|
-
"tokens": [
|
|
16
|
-
"--color-bg-brand-muted",
|
|
17
|
-
"--color-bg-subtle",
|
|
18
|
-
"--color-border-default",
|
|
19
|
-
"--color-border-muted",
|
|
20
|
-
"--color-status-error-bg",
|
|
21
|
-
"--color-status-error-border",
|
|
22
|
-
"--color-status-error-text",
|
|
23
|
-
"--color-text-brand",
|
|
24
|
-
"--color-text-muted",
|
|
25
|
-
"--color-text-primary",
|
|
26
|
-
"--color-text-secondary",
|
|
27
|
-
"--gray-950",
|
|
28
|
-
"--teal-500"
|
|
29
|
-
],
|
|
30
|
-
"fragments": [
|
|
31
|
-
{
|
|
32
|
-
"id": "InvitePage",
|
|
33
|
-
"line": 24,
|
|
34
|
-
"code": "export default function InvitePage() {\n const search = useSearchParams();\n const stateParam = (search.get(\"state\") as State | null) ?? \"new-user\";\n\n if (stateParam === \"expired\") return <ExpiredView />;\n if (stateParam === \"code-entry\") return <CodeEntryView />;\n if (stateParam === \"existing-user\") return <ExistingUserView />;\n return <NewUserView />;\n}"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"id": "WorkspaceBadge",
|
|
38
|
-
"line": 36,
|
|
39
|
-
"code": "function WorkspaceBadge() {\n return (\n <div className=\"rounded-2xl border p-5 mb-6\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-subtle)\" }}>\n <div className=\"flex items-center gap-4\">\n <div className=\"w-12 h-12 rounded-xl flex items-center justify-center shrink-0 text-base font-bold\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>\n KC\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-semibold truncate\">{WORKSPACE.name}</div>\n <div className=\"text-[12px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>\n {WORKSPACE.members} members · invited as <strong style={{ color: \"var(--color-text-primary)\" }}>{WORKSPACE.role}</strong>\n </div>\n </div>\n </div>\n <div className=\"text-[12px] mt-3 pt-3 border-t flex items-center gap-2\" style={{ borderColor: \"var(--color-border-muted)\", color: \"var(--color-text-secondary)\" }}>\n <div className=\"w-6 h-6 rounded-full flex items-center justify-center text-[9px] font-semibold\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>AH</div>\n <span>\n <strong style={{ color: \"var(--color-text-primary)\" }}>{WORKSPACE.invitedBy}</strong> invited you to join\n </span>\n </div>\n </div>\n );\n}"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"id": "NewUserView",
|
|
43
|
-
"line": 62,
|
|
44
|
-
"code": "function NewUserView() {\n const [name, setName] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [terms, setTerms] = useState(false);\n const [submitting, setSubmitting] = useState(false);\n const { score } = passwordStrength(password);\n const canSubmit = name.length >= 2 && score >= 2 && terms;\n\n return (\n <AuthShell\n kicker=\"You've been invited\"\n title=\"Join the workspace\"\n subtitle=\"Create your account to accept the invitation. We've already saved your email.\"\n >\n <WorkspaceBadge />\n\n <SocialButtons onClick={() => {}} />\n <OrDivider />\n\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => window.location.href = \"/welcome\", 1000); }} className=\"space-y-4\">\n <Field label=\"Email\" htmlFor=\"em\">\n <Input id=\"em\" type=\"email\" value={WORKSPACE.inviteeEmail} disabled className=\"h-10\" />\n </Field>\n <Field label=\"Full name\" htmlFor=\"nm\">\n <Input id=\"nm\" value={name} onChange={(e) => setName(e.target.value)} placeholder=\"Dr. Sarah Whitfield\" className=\"h-10\" required />\n </Field>\n <Field label=\"Create password\" htmlFor=\"pw\">\n <PasswordInput id=\"pw\" value={password} onChange={setPassword} placeholder=\"At least 8 characters\" />\n {password && <PasswordStrength password={password} />}\n </Field>\n <Checkbox id=\"t\" checked={terms} onChange={setTerms}>\n I agree to the <Link href=\"#\" className=\"underline underline-offset-4\">Terms</Link> and <Link href=\"#\" className=\"underline underline-offset-4\">Privacy Policy</Link>.\n </Checkbox>\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" className=\"w-full\" disabled={!canSubmit || submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Joining…</> : <>Accept invitation & join <ArrowRight size={14} /></>}\n </Button>\n <Link href=\"?state=code-entry\" className=\"block text-center text-[12px] hover:underline underline-offset-4\" style={{ color: \"var(--color-text-muted)\" }}>\n Have an invite code instead?\n </Link>\n </form>\n </AuthShell>\n );\n}"
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"id": "ExistingUserView",
|
|
48
|
-
"line": 108,
|
|
49
|
-
"code": "function ExistingUserView() {\n const [submitting, setSubmitting] = useState(false);\n return (\n <AuthShell\n kicker=\"You're already signed in\"\n title=\"Accept the invitation\"\n subtitle={<>You're signed in as <strong style={{ color: \"var(--color-text-primary)\" }}>sarah.park@kcl.ac.uk</strong>.</>}\n >\n <WorkspaceBadge />\n\n <div className=\"rounded-lg border p-3 flex items-start gap-3 mb-5\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>SP</div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">Dr. Sarah Whitfield</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>sarah.park@kcl.ac.uk · 3 workspaces</div>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-2\">\n <Button variant=\"brand\" size=\"lg\" onClick={() => { setSubmitting(true); setTimeout(() => window.location.href = \"/welcome\", 800); }} disabled={submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Joining workspace…</> : <>Accept & join {WORKSPACE.name} <ArrowRight size={14} /></>}\n </Button>\n <Button variant=\"outline\" size=\"lg\">Decline invitation</Button>\n <Link href=\"/login\" className=\"text-center text-[12px] hover:underline underline-offset-4 mt-2\" style={{ color: \"var(--color-text-muted)\" }}>\n Sign in as a different user\n </Link>\n </div>\n </AuthShell>\n );\n}"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"id": "CodeEntryView",
|
|
53
|
-
"line": 141,
|
|
54
|
-
"code": "function CodeEntryView() {\n const [code, setCode] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n const [errored, setErrored] = useState(false);\n return (\n <AuthShell\n kicker=\"Have an invite code?\"\n title=\"Enter your invite code\"\n subtitle=\"If a colleague shared a workspace invite code with you, enter it below to join.\"\n >\n <form onSubmit={(e) => {\n e.preventDefault();\n setSubmitting(true); setErrored(false);\n setTimeout(() => {\n setSubmitting(false);\n if (code === \"MENTIX-2026\" || code === \"demo\") window.location.href = \"/invite\";\n else setErrored(true);\n }, 800);\n }} className=\"space-y-4\">\n {errored && <InlineAlert variant=\"error\" title=\"Invalid code\">This invite code doesn't exist or has been used.</InlineAlert>}\n\n <Field label=\"Invite code\" htmlFor=\"ic\">\n <Input id=\"ic\" value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} placeholder=\"MENTIX-XXXX\" className=\"h-10 font-mono uppercase tracking-wider\" autoFocus required aria-invalid={errored} />\n </Field>\n\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" className=\"w-full\" disabled={!code || submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Checking…</> : <>Continue <ArrowRight size={14} /></>}\n </Button>\n </form>\n </AuthShell>\n );\n}"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"id": "ExpiredView",
|
|
58
|
-
"line": 176,
|
|
59
|
-
"code": "function ExpiredView() {\n return (\n <AuthShell kicker=\"Invitation expired\" title=\"This invitation is no longer valid\" subtitle=\"Invitations expire after 7 days for security. Reach out to the person who invited you to send a new one.\">\n <div className=\"space-y-4\">\n <div className=\"rounded-2xl border p-5 flex items-center gap-4\" style={{ borderColor: \"var(--color-status-error-border)\", background: \"var(--color-status-error-bg)\" }}>\n <XCircle size={20} style={{ color: \"var(--color-status-error-text)\" }} />\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium\" style={{ color: \"var(--color-status-error-text)\" }}>Invitation from {WORKSPACE.invitedBy}</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-status-error-text)\", opacity: 0.85 }}>Expired 3 days ago</div>\n </div>\n </div>\n\n <div className=\"rounded-xl border p-4\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"flex items-center gap-2 mb-2\"><Mail size={14} style={{ color: \"var(--color-text-muted)\" }} /><span className=\"text-[13px] font-medium\">Request a new invitation</span></div>\n <p className=\"text-[12px] mb-3\" style={{ color: \"var(--color-text-secondary)\" }}>\n We'll email <strong style={{ color: \"var(--color-text-primary)\" }}>{WORKSPACE.invitedBy}</strong> letting them know you tried to join.\n </p>\n <Button variant=\"outline\" size=\"sm\">Notify {WORKSPACE.invitedBy.split(\" \")[0]}</Button>\n </div>\n\n <Link href=\"/login\" className=\"block text-center text-[12px] hover:underline underline-offset-4 pt-3\" style={{ color: \"var(--color-text-muted)\" }}>\n ← Back to sign in\n </Link>\n </div>\n </AuthShell>\n );\n}"
|
|
60
|
-
}
|
|
61
|
-
],
|
|
62
|
-
"content": "\"use client\";\nimport Link from \"next/link\";\nimport { useState, useMemo } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, Building2, Mail, XCircle } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport {\n Field, PasswordInput, PasswordStrength, InlineAlert, Checkbox, SocialButtons, OrDivider, Spinner, passwordStrength,\n} from \"@/components/auth/auth-primitives\";\n\ntype State = \"new-user\" | \"existing-user\" | \"code-entry\" | \"expired\";\n\nconst WORKSPACE = {\n name: \"King's College Hospital — GI Unit\",\n invitedBy: \"Prof. Amyn Haji\",\n invitedByEmail: \"a.haji@kch.nhs.uk\",\n inviteeEmail: \"you@hospital.com\",\n role: \"Trainee\" as const,\n members: 24,\n};\n\nexport default function InvitePage() {\n const search = useSearchParams();\n const stateParam = (search.get(\"state\") as State | null) ?? \"new-user\";\n\n if (stateParam === \"expired\") return <ExpiredView />;\n if (stateParam === \"code-entry\") return <CodeEntryView />;\n if (stateParam === \"existing-user\") return <ExistingUserView />;\n return <NewUserView />;\n}\n\n/* ─── Shared workspace badge ────────────────────────────────────────────── */\n\nfunction WorkspaceBadge() {\n return (\n <div className=\"rounded-2xl border p-5 mb-6\" style={{ borderColor: \"var(--color-border-default)\", background: \"var(--color-bg-subtle)\" }}>\n <div className=\"flex items-center gap-4\">\n <div className=\"w-12 h-12 rounded-xl flex items-center justify-center shrink-0 text-base font-bold\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>\n KC\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-semibold truncate\">{WORKSPACE.name}</div>\n <div className=\"text-[12px] mt-0.5\" style={{ color: \"var(--color-text-muted)\" }}>\n {WORKSPACE.members} members · invited as <strong style={{ color: \"var(--color-text-primary)\" }}>{WORKSPACE.role}</strong>\n </div>\n </div>\n </div>\n <div className=\"text-[12px] mt-3 pt-3 border-t flex items-center gap-2\" style={{ borderColor: \"var(--color-border-muted)\", color: \"var(--color-text-secondary)\" }}>\n <div className=\"w-6 h-6 rounded-full flex items-center justify-center text-[9px] font-semibold\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>AH</div>\n <span>\n <strong style={{ color: \"var(--color-text-primary)\" }}>{WORKSPACE.invitedBy}</strong> invited you to join\n </span>\n </div>\n </div>\n );\n}\n\n/* ─── New User → Create account from invite ─────────────────────────────── */\n\nfunction NewUserView() {\n const [name, setName] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [terms, setTerms] = useState(false);\n const [submitting, setSubmitting] = useState(false);\n const { score } = passwordStrength(password);\n const canSubmit = name.length >= 2 && score >= 2 && terms;\n\n return (\n <AuthShell\n kicker=\"You've been invited\"\n title=\"Join the workspace\"\n subtitle=\"Create your account to accept the invitation. We've already saved your email.\"\n >\n <WorkspaceBadge />\n\n <SocialButtons onClick={() => {}} />\n <OrDivider />\n\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => window.location.href = \"/welcome\", 1000); }} className=\"space-y-4\">\n <Field label=\"Email\" htmlFor=\"em\">\n <Input id=\"em\" type=\"email\" value={WORKSPACE.inviteeEmail} disabled className=\"h-10\" />\n </Field>\n <Field label=\"Full name\" htmlFor=\"nm\">\n <Input id=\"nm\" value={name} onChange={(e) => setName(e.target.value)} placeholder=\"Dr. Sarah Whitfield\" className=\"h-10\" required />\n </Field>\n <Field label=\"Create password\" htmlFor=\"pw\">\n <PasswordInput id=\"pw\" value={password} onChange={setPassword} placeholder=\"At least 8 characters\" />\n {password && <PasswordStrength password={password} />}\n </Field>\n <Checkbox id=\"t\" checked={terms} onChange={setTerms}>\n I agree to the <Link href=\"#\" className=\"underline underline-offset-4\">Terms</Link> and <Link href=\"#\" className=\"underline underline-offset-4\">Privacy Policy</Link>.\n </Checkbox>\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" className=\"w-full\" disabled={!canSubmit || submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Joining…</> : <>Accept invitation & join <ArrowRight size={14} /></>}\n </Button>\n <Link href=\"?state=code-entry\" className=\"block text-center text-[12px] hover:underline underline-offset-4\" style={{ color: \"var(--color-text-muted)\" }}>\n Have an invite code instead?\n </Link>\n </form>\n </AuthShell>\n );\n}\n\n/* ─── Existing User → Just confirm and join ─────────────────────────────── */\n\nfunction ExistingUserView() {\n const [submitting, setSubmitting] = useState(false);\n return (\n <AuthShell\n kicker=\"You're already signed in\"\n title=\"Accept the invitation\"\n subtitle={<>You're signed in as <strong style={{ color: \"var(--color-text-primary)\" }}>sarah.park@kcl.ac.uk</strong>.</>}\n >\n <WorkspaceBadge />\n\n <div className=\"rounded-lg border p-3 flex items-start gap-3 mb-5\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0\" style={{ background: \"var(--teal-500)\", color: \"var(--gray-950)\" }}>SP</div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">Dr. Sarah Whitfield</div>\n <div className=\"text-[11px]\" style={{ color: \"var(--color-text-muted)\" }}>sarah.park@kcl.ac.uk · 3 workspaces</div>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-2\">\n <Button variant=\"brand\" size=\"lg\" onClick={() => { setSubmitting(true); setTimeout(() => window.location.href = \"/welcome\", 800); }} disabled={submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Joining workspace…</> : <>Accept & join {WORKSPACE.name} <ArrowRight size={14} /></>}\n </Button>\n <Button variant=\"outline\" size=\"lg\">Decline invitation</Button>\n <Link href=\"/login\" className=\"text-center text-[12px] hover:underline underline-offset-4 mt-2\" style={{ color: \"var(--color-text-muted)\" }}>\n Sign in as a different user\n </Link>\n </div>\n </AuthShell>\n );\n}\n\n/* ─── Code entry ────────────────────────────────────────────────────────── */\n\nfunction CodeEntryView() {\n const [code, setCode] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n const [errored, setErrored] = useState(false);\n return (\n <AuthShell\n kicker=\"Have an invite code?\"\n title=\"Enter your invite code\"\n subtitle=\"If a colleague shared a workspace invite code with you, enter it below to join.\"\n >\n <form onSubmit={(e) => {\n e.preventDefault();\n setSubmitting(true); setErrored(false);\n setTimeout(() => {\n setSubmitting(false);\n if (code === \"MENTIX-2026\" || code === \"demo\") window.location.href = \"/invite\";\n else setErrored(true);\n }, 800);\n }} className=\"space-y-4\">\n {errored && <InlineAlert variant=\"error\" title=\"Invalid code\">This invite code doesn't exist or has been used.</InlineAlert>}\n\n <Field label=\"Invite code\" htmlFor=\"ic\">\n <Input id=\"ic\" value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} placeholder=\"MENTIX-XXXX\" className=\"h-10 font-mono uppercase tracking-wider\" autoFocus required aria-invalid={errored} />\n </Field>\n\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" className=\"w-full\" disabled={!code || submitting}>\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Checking…</> : <>Continue <ArrowRight size={14} /></>}\n </Button>\n </form>\n </AuthShell>\n );\n}\n\n/* ─── Expired invite ────────────────────────────────────────────────────── */\n\nfunction ExpiredView() {\n return (\n <AuthShell kicker=\"Invitation expired\" title=\"This invitation is no longer valid\" subtitle=\"Invitations expire after 7 days for security. Reach out to the person who invited you to send a new one.\">\n <div className=\"space-y-4\">\n <div className=\"rounded-2xl border p-5 flex items-center gap-4\" style={{ borderColor: \"var(--color-status-error-border)\", background: \"var(--color-status-error-bg)\" }}>\n <XCircle size={20} style={{ color: \"var(--color-status-error-text)\" }} />\n <div className=\"flex-1\">\n <div className=\"text-[13px] font-medium\" style={{ color: \"var(--color-status-error-text)\" }}>Invitation from {WORKSPACE.invitedBy}</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-status-error-text)\", opacity: 0.85 }}>Expired 3 days ago</div>\n </div>\n </div>\n\n <div className=\"rounded-xl border p-4\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"flex items-center gap-2 mb-2\"><Mail size={14} style={{ color: \"var(--color-text-muted)\" }} /><span className=\"text-[13px] font-medium\">Request a new invitation</span></div>\n <p className=\"text-[12px] mb-3\" style={{ color: \"var(--color-text-secondary)\" }}>\n We'll email <strong style={{ color: \"var(--color-text-primary)\" }}>{WORKSPACE.invitedBy}</strong> letting them know you tried to join.\n </p>\n <Button variant=\"outline\" size=\"sm\">Notify {WORKSPACE.invitedBy.split(\" \")[0]}</Button>\n </div>\n\n <Link href=\"/login\" className=\"block text-center text-[12px] hover:underline underline-offset-4 pt-3\" style={{ color: \"var(--color-text-muted)\" }}>\n ← Back to sign in\n </Link>\n </div>\n </AuthShell>\n );\n}\n"
|
|
63
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
-
"file": "app/(auth)/locked/page.tsx",
|
|
4
|
-
"sha256": "099b99dbfe45073504ab4d141c3d8ae80f0b874660a45b7b8362acbe3ca078af",
|
|
5
|
-
"imports": [
|
|
6
|
-
"next/link",
|
|
7
|
-
"react",
|
|
8
|
-
"next/navigation",
|
|
9
|
-
"lucide-react",
|
|
10
|
-
"@/components/ui/button",
|
|
11
|
-
"@/components/auth/auth-shell",
|
|
12
|
-
"@/components/auth/auth-primitives"
|
|
13
|
-
],
|
|
14
|
-
"tokens": [
|
|
15
|
-
"--amber-500",
|
|
16
|
-
"--color-bg-brand-muted",
|
|
17
|
-
"--color-bg-default",
|
|
18
|
-
"--color-bg-subtle",
|
|
19
|
-
"--color-border-default",
|
|
20
|
-
"--color-border-muted",
|
|
21
|
-
"--color-status-warning-bg",
|
|
22
|
-
"--color-status-warning-border",
|
|
23
|
-
"--color-status-warning-text",
|
|
24
|
-
"--color-text-brand",
|
|
25
|
-
"--color-text-muted",
|
|
26
|
-
"--color-text-primary",
|
|
27
|
-
"--color-text-secondary",
|
|
28
|
-
"--gray-950"
|
|
29
|
-
],
|
|
30
|
-
"fragments": [
|
|
31
|
-
{
|
|
32
|
-
"id": "LockedPage",
|
|
33
|
-
"line": 12,
|
|
34
|
-
"code": "export default function LockedPage() {\n const search = useSearchParams();\n const stateParam = (search.get(\"state\") as State | null) ?? \"attempts\";\n\n if (stateParam === \"suspicious\") return <SuspiciousLoginView />;\n if (stateParam === \"device-verify\") return <DeviceVerifyView />;\n if (stateParam === \"confirm-action\") return <ConfirmActionView />;\n return <TooManyAttemptsView />;\n}"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"id": "TooManyAttemptsView",
|
|
38
|
-
"line": 24,
|
|
39
|
-
"code": "function TooManyAttemptsView() {\n const [seconds, setSeconds] = useState(900); // 15 min lockout\n\n useEffect(() => {\n if (seconds > 0) {\n const t = setTimeout(() => setSeconds(s => s - 1), 1000);\n return () => clearTimeout(t);\n }\n }, [seconds]);\n\n const minutes = Math.floor(seconds / 60);\n const secs = seconds % 60;\n\n return (\n <AuthShell\n kicker=\"Account temporarily locked\"\n title=\"Too many sign-in attempts\"\n subtitle=\"We've temporarily locked sign-ins on your account to protect it. Try again later or reset your password.\"\n \n >\n <div className=\"space-y-5\">\n <div className=\"rounded-2xl border p-6 text-center\" style={{ borderColor: \"var(--color-status-warning-border)\", background: \"var(--color-status-warning-bg)\" }}>\n <div className=\"w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3\" style={{ background: \"color-mix(in srgb, var(--amber-500) 22%, transparent)\", color: \"var(--color-status-warning-text)\" }}>\n <Lock size={20} strokeWidth={1.8} />\n </div>\n <div className=\"text-[11px] font-semibold uppercase tracking-[0.12em] mb-1\" style={{ color: \"var(--color-status-warning-text)\" }}>Try again in</div>\n <div className=\"text-3xl font-semibold tabular-nums\" style={{ color: \"var(--color-status-warning-text)\", letterSpacing: \"-0.02em\" }}>\n {String(minutes).padStart(2, \"0\")}:{String(secs).padStart(2, \"0\")}\n </div>\n </div>\n\n <div className=\"space-y-2.5 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p>This usually means someone is trying to sign in with the wrong password. If that was you, take a deep breath and try one of the options below.</p>\n </div>\n\n <div className=\"flex flex-col gap-2 pt-2\">\n <Link href=\"/forgot-password\"><Button variant=\"brand\" size=\"lg\" className=\"w-full\">Reset my password <ArrowRight size={14} /></Button></Link>\n <Link href=\"#\"><Button variant=\"outline\" size=\"lg\" className=\"w-full\">Contact support</Button></Link>\n </div>\n\n <p className=\"text-[11px] text-center pt-2\" style={{ color: \"var(--color-text-muted)\" }}>\n Wasn't you? Your account is safe — but{\" \"}\n <Link href=\"#\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n review your security settings\n </Link>.\n </p>\n </div>\n </AuthShell>\n );\n}"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"id": "SuspiciousLoginView",
|
|
43
|
-
"line": 77,
|
|
44
|
-
"code": "function SuspiciousLoginView() {\n return (\n <AuthShell\n kicker=\"Security alert\"\n title=\"Unusual sign-in attempt\"\n subtitle=\"We noticed a sign-in attempt from a new location. We've blocked it for now — please verify it was you.\"\n \n >\n <div className=\"space-y-5\">\n <InlineAlert variant=\"warning\" title=\"Sign-in attempt blocked\">\n A new device tried to sign in to your account 2 minutes ago.\n </InlineAlert>\n\n {/* Attempt detail */}\n <div className=\"rounded-2xl border\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"p-4 flex items-start gap-3 border-b\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <Monitor size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">Chrome on Windows</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>Frankfurt, Germany · IP 185.220.x.x</div>\n <div className=\"text-[11px] mt-1\" style={{ color: \"var(--color-text-muted)\" }}>2 minutes ago · Tor exit node detected</div>\n </div>\n </div>\n <div className=\"p-3 flex gap-2\">\n <Button variant=\"outline\" size=\"sm\" className=\"flex-1\">This was me</Button>\n <Button variant=\"destructive\" size=\"sm\" className=\"flex-1\">Not me — secure account</Button>\n </div>\n </div>\n\n <div className=\"pt-2 text-[12px] space-y-1.5\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p className=\"font-medium\" style={{ color: \"var(--color-text-primary)\" }}>To stay safe, we recommend:</p>\n <ul className=\"space-y-1 list-disc list-inside\" style={{ color: \"var(--color-text-secondary)\" }}>\n <li>Change your password if you don't recognize this device.</li>\n <li>Enable two-factor authentication.</li>\n <li>Review your active sessions.</li>\n </ul>\n </div>\n </div>\n </AuthShell>\n );\n}"
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"id": "DeviceVerifyView",
|
|
48
|
-
"line": 123,
|
|
49
|
-
"code": "function DeviceVerifyView() {\n const [code, setCode] = useState(\"\");\n const [verifying, setVerifying] = useState(false);\n\n useEffect(() => {\n if (code.length === 6) {\n setVerifying(true);\n setTimeout(() => window.location.href = \"/welcome\", 1000);\n }\n }, [code]);\n\n return (\n <AuthShell\n kicker=\"Verify this device\"\n title=\"Is that you?\"\n subtitle={<>We sent a 6-digit code to <strong style={{ color: \"var(--color-text-primary)\" }}>s•••@kcl.ac.uk</strong>. Enter it to confirm this device.</>}\n >\n <div className=\"space-y-5\">\n <div className=\"rounded-xl border p-4 flex items-start gap-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Smartphone size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 text-[12px]\">\n <div className=\"font-medium\" style={{ color: \"var(--color-text-primary)\" }}>Signing in from MacBook · Safari</div>\n <div style={{ color: \"var(--color-text-muted)\" }}>London, UK · Today at 4:21 PM</div>\n </div>\n </div>\n\n <div className=\"flex justify-center py-2\">\n <OtpInput value={code} onChange={setCode} disabled={verifying} />\n </div>\n\n {verifying && (\n <div className=\"flex items-center justify-center gap-2 text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Verifying…\n </div>\n )}\n\n <div className=\"text-center text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>\n Didn't get a code?{\" \"}\n <button className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>Resend</button>\n </div>\n </div>\n </AuthShell>\n );\n}"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"id": "ConfirmActionView",
|
|
53
|
-
"line": 172,
|
|
54
|
-
"code": "function ConfirmActionView() {\n return (\n <AuthShell kicker=\"Sensitive action\" title=\"Confirm it's you\" subtitle=\"For your security, please re-enter your password before completing this action.\">\n <ConfirmModal />\n </AuthShell>\n );\n}"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"id": "ConfirmModal",
|
|
58
|
-
"line": 180,
|
|
59
|
-
"code": "function ConfirmModal() {\n const [open, setOpen] = useState(true);\n const [pw, setPw] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n if (!open) return <p className=\"text-center text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>Action confirmed. <Link href=\"/welcome\" className=\"hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>Continue →</Link></p>;\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style={{ background: \"color-mix(in srgb, var(--gray-950) 60%, transparent)\" }}>\n <div className=\"w-full max-w-md rounded-2xl p-6 shadow-xl\" style={{ background: \"var(--color-bg-default)\", border: \"1px solid var(--color-border-default)\" }}>\n <div className=\"flex items-start gap-3 mb-5\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <ShieldAlert size={18} strokeWidth={1.8} />\n </div>\n <div>\n <h2 className=\"text-base font-semibold mb-1\">Verify your identity</h2>\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>You're about to delete your workspace. Enter your password to continue.</p>\n </div>\n </div>\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => setOpen(false), 800); }} className=\"space-y-4\">\n <PasswordInput\n id=\"confirm-pw\"\n value={pw}\n onChange={setPw}\n placeholder=\"Enter your password\"\n autoComplete=\"current-password\"\n />\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"default\" onClick={() => setOpen(false)} className=\"flex-1\">Cancel</Button>\n <Button type=\"submit\" variant=\"destructive\" disabled={pw.length < 4 || submitting} className=\"flex-1\">\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Confirming…</> : \"Confirm & continue\"}\n </Button>\n </div>\n </form>\n </div>\n </div>\n );\n}"
|
|
60
|
-
}
|
|
61
|
-
],
|
|
62
|
-
"content": "\"use client\";\nimport Link from \"next/link\";\nimport { useState, useEffect } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, Lock, AlertTriangle, Smartphone, Monitor, ShieldAlert } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport { InlineAlert, OtpInput, Spinner, PasswordInput } from \"@/components/auth/auth-primitives\";\n\ntype State = \"attempts\" | \"suspicious\" | \"device-verify\" | \"confirm-action\";\n\nexport default function LockedPage() {\n const search = useSearchParams();\n const stateParam = (search.get(\"state\") as State | null) ?? \"attempts\";\n\n if (stateParam === \"suspicious\") return <SuspiciousLoginView />;\n if (stateParam === \"device-verify\") return <DeviceVerifyView />;\n if (stateParam === \"confirm-action\") return <ConfirmActionView />;\n return <TooManyAttemptsView />;\n}\n\n/* ─── Too many attempts ─────────────────────────────────────────────────── */\n\nfunction TooManyAttemptsView() {\n const [seconds, setSeconds] = useState(900); // 15 min lockout\n\n useEffect(() => {\n if (seconds > 0) {\n const t = setTimeout(() => setSeconds(s => s - 1), 1000);\n return () => clearTimeout(t);\n }\n }, [seconds]);\n\n const minutes = Math.floor(seconds / 60);\n const secs = seconds % 60;\n\n return (\n <AuthShell\n kicker=\"Account temporarily locked\"\n title=\"Too many sign-in attempts\"\n subtitle=\"We've temporarily locked sign-ins on your account to protect it. Try again later or reset your password.\"\n \n >\n <div className=\"space-y-5\">\n <div className=\"rounded-2xl border p-6 text-center\" style={{ borderColor: \"var(--color-status-warning-border)\", background: \"var(--color-status-warning-bg)\" }}>\n <div className=\"w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3\" style={{ background: \"color-mix(in srgb, var(--amber-500) 22%, transparent)\", color: \"var(--color-status-warning-text)\" }}>\n <Lock size={20} strokeWidth={1.8} />\n </div>\n <div className=\"text-[11px] font-semibold uppercase tracking-[0.12em] mb-1\" style={{ color: \"var(--color-status-warning-text)\" }}>Try again in</div>\n <div className=\"text-3xl font-semibold tabular-nums\" style={{ color: \"var(--color-status-warning-text)\", letterSpacing: \"-0.02em\" }}>\n {String(minutes).padStart(2, \"0\")}:{String(secs).padStart(2, \"0\")}\n </div>\n </div>\n\n <div className=\"space-y-2.5 text-[12px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p>This usually means someone is trying to sign in with the wrong password. If that was you, take a deep breath and try one of the options below.</p>\n </div>\n\n <div className=\"flex flex-col gap-2 pt-2\">\n <Link href=\"/forgot-password\"><Button variant=\"brand\" size=\"lg\" className=\"w-full\">Reset my password <ArrowRight size={14} /></Button></Link>\n <Link href=\"#\"><Button variant=\"outline\" size=\"lg\" className=\"w-full\">Contact support</Button></Link>\n </div>\n\n <p className=\"text-[11px] text-center pt-2\" style={{ color: \"var(--color-text-muted)\" }}>\n Wasn't you? Your account is safe — but{\" \"}\n <Link href=\"#\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n review your security settings\n </Link>.\n </p>\n </div>\n </AuthShell>\n );\n}\n\n/* ─── Suspicious login ──────────────────────────────────────────────────── */\n\nfunction SuspiciousLoginView() {\n return (\n <AuthShell\n kicker=\"Security alert\"\n title=\"Unusual sign-in attempt\"\n subtitle=\"We noticed a sign-in attempt from a new location. We've blocked it for now — please verify it was you.\"\n \n >\n <div className=\"space-y-5\">\n <InlineAlert variant=\"warning\" title=\"Sign-in attempt blocked\">\n A new device tried to sign in to your account 2 minutes ago.\n </InlineAlert>\n\n {/* Attempt detail */}\n <div className=\"rounded-2xl border\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"p-4 flex items-start gap-3 border-b\" style={{ borderColor: \"var(--color-border-muted)\" }}>\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-subtle)\", color: \"var(--color-text-secondary)\" }}>\n <Monitor size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-[13px] font-medium\">Chrome on Windows</div>\n <div className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>Frankfurt, Germany · IP 185.220.x.x</div>\n <div className=\"text-[11px] mt-1\" style={{ color: \"var(--color-text-muted)\" }}>2 minutes ago · Tor exit node detected</div>\n </div>\n </div>\n <div className=\"p-3 flex gap-2\">\n <Button variant=\"outline\" size=\"sm\" className=\"flex-1\">This was me</Button>\n <Button variant=\"destructive\" size=\"sm\" className=\"flex-1\">Not me — secure account</Button>\n </div>\n </div>\n\n <div className=\"pt-2 text-[12px] space-y-1.5\" style={{ color: \"var(--color-text-secondary)\" }}>\n <p className=\"font-medium\" style={{ color: \"var(--color-text-primary)\" }}>To stay safe, we recommend:</p>\n <ul className=\"space-y-1 list-disc list-inside\" style={{ color: \"var(--color-text-secondary)\" }}>\n <li>Change your password if you don't recognize this device.</li>\n <li>Enable two-factor authentication.</li>\n <li>Review your active sessions.</li>\n </ul>\n </div>\n </div>\n </AuthShell>\n );\n}\n\n/* ─── Device verification (OTP via email) ────────────────────────────────── */\n\nfunction DeviceVerifyView() {\n const [code, setCode] = useState(\"\");\n const [verifying, setVerifying] = useState(false);\n\n useEffect(() => {\n if (code.length === 6) {\n setVerifying(true);\n setTimeout(() => window.location.href = \"/welcome\", 1000);\n }\n }, [code]);\n\n return (\n <AuthShell\n kicker=\"Verify this device\"\n title=\"Is that you?\"\n subtitle={<>We sent a 6-digit code to <strong style={{ color: \"var(--color-text-primary)\" }}>s•••@kcl.ac.uk</strong>. Enter it to confirm this device.</>}\n >\n <div className=\"space-y-5\">\n <div className=\"rounded-xl border p-4 flex items-start gap-3\" style={{ borderColor: \"var(--color-border-default)\" }}>\n <div className=\"w-9 h-9 rounded-lg flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <Smartphone size={16} strokeWidth={1.8} />\n </div>\n <div className=\"flex-1 text-[12px]\">\n <div className=\"font-medium\" style={{ color: \"var(--color-text-primary)\" }}>Signing in from MacBook · Safari</div>\n <div style={{ color: \"var(--color-text-muted)\" }}>London, UK · Today at 4:21 PM</div>\n </div>\n </div>\n\n <div className=\"flex justify-center py-2\">\n <OtpInput value={code} onChange={setCode} disabled={verifying} />\n </div>\n\n {verifying && (\n <div className=\"flex items-center justify-center gap-2 text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>\n <Spinner className=\"w-4 h-4\" /> Verifying…\n </div>\n )}\n\n <div className=\"text-center text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>\n Didn't get a code?{\" \"}\n <button className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>Resend</button>\n </div>\n </div>\n </AuthShell>\n );\n}\n\n/* ─── Security confirmation modal (in-page) ──────────────────────────────── */\n\nfunction ConfirmActionView() {\n return (\n <AuthShell kicker=\"Sensitive action\" title=\"Confirm it's you\" subtitle=\"For your security, please re-enter your password before completing this action.\">\n <ConfirmModal />\n </AuthShell>\n );\n}\n\nfunction ConfirmModal() {\n const [open, setOpen] = useState(true);\n const [pw, setPw] = useState(\"\");\n const [submitting, setSubmitting] = useState(false);\n if (!open) return <p className=\"text-center text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>Action confirmed. <Link href=\"/welcome\" className=\"hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>Continue →</Link></p>;\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" style={{ background: \"color-mix(in srgb, var(--gray-950) 60%, transparent)\" }}>\n <div className=\"w-full max-w-md rounded-2xl p-6 shadow-xl\" style={{ background: \"var(--color-bg-default)\", border: \"1px solid var(--color-border-default)\" }}>\n <div className=\"flex items-start gap-3 mb-5\">\n <div className=\"w-10 h-10 rounded-full flex items-center justify-center shrink-0\" style={{ background: \"var(--color-bg-brand-muted)\", color: \"var(--color-text-brand)\" }}>\n <ShieldAlert size={18} strokeWidth={1.8} />\n </div>\n <div>\n <h2 className=\"text-base font-semibold mb-1\">Verify your identity</h2>\n <p className=\"text-[13px]\" style={{ color: \"var(--color-text-secondary)\" }}>You're about to delete your workspace. Enter your password to continue.</p>\n </div>\n </div>\n <form onSubmit={(e) => { e.preventDefault(); setSubmitting(true); setTimeout(() => setOpen(false), 800); }} className=\"space-y-4\">\n <PasswordInput\n id=\"confirm-pw\"\n value={pw}\n onChange={setPw}\n placeholder=\"Enter your password\"\n autoComplete=\"current-password\"\n />\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"default\" onClick={() => setOpen(false)} className=\"flex-1\">Cancel</Button>\n <Button type=\"submit\" variant=\"destructive\" disabled={pw.length < 4 || submitting} className=\"flex-1\">\n {submitting ? <><Spinner className=\"w-4 h-4 mr-1\" /> Confirming…</> : \"Confirm & continue\"}\n </Button>\n </div>\n </form>\n </div>\n </div>\n );\n}\n"
|
|
63
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"revision": "6b4b2770d04e42cc2e5453c3d80e35c282b3438f",
|
|
3
|
-
"file": "app/(auth)/login/page.tsx",
|
|
4
|
-
"sha256": "a38098ffb02f3eb4ce1a70f166bf6afa5ac976e2265acc4edbd4b5e70acf1218",
|
|
5
|
-
"imports": [
|
|
6
|
-
"next/link",
|
|
7
|
-
"react",
|
|
8
|
-
"next/navigation",
|
|
9
|
-
"lucide-react",
|
|
10
|
-
"@/components/ui/button",
|
|
11
|
-
"@/components/ui/input",
|
|
12
|
-
"@/components/auth/auth-shell",
|
|
13
|
-
"@/components/auth/auth-primitives"
|
|
14
|
-
],
|
|
15
|
-
"tokens": [
|
|
16
|
-
"--color-text-brand",
|
|
17
|
-
"--color-text-muted",
|
|
18
|
-
"--color-text-secondary"
|
|
19
|
-
],
|
|
20
|
-
"fragments": [
|
|
21
|
-
{
|
|
22
|
-
"id": "LoginPage",
|
|
23
|
-
"line": 16,
|
|
24
|
-
"code": "export default function LoginPage() {\n const search = useSearchParams();\n // Dev-time state override via ?state=\n const forced = search.get(\"state\") as State | null;\n\n const [mode, setMode] = useState<Mode>(\"password\");\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [remember, setRemember] = useState(true);\n const [socialLoading, setSocialLoading] = useState<null | \"google\" | \"apple\" | \"github\">(null);\n const [submitting, setSubmitting] = useState(false);\n const [errorKind, setErrorKind] = useState<null | \"invalid\" | \"suspended\">(null);\n const [magicSent, setMagicSent] = useState(false);\n\n const state: State = useMemo(() => {\n if (forced) return forced;\n if (submitting) return \"loading\";\n if (errorKind === \"suspended\") return \"suspended\";\n if (errorKind === \"invalid\") return \"error\";\n if (magicSent) return \"magic-sent\";\n return \"idle\";\n }, [forced, submitting, errorKind, magicSent]);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setSubmitting(true);\n setErrorKind(null);\n setTimeout(() => {\n setSubmitting(false);\n // Demo branches based on email\n if (email.includes(\"suspended\")) setErrorKind(\"suspended\");\n else if (email !== \"demo@mentix.world\" || password !== \"Demo1234!\") setErrorKind(\"invalid\");\n else if (mode === \"magic\") setMagicSent(true);\n }, 1200);\n };\n\n return (\n <AuthShell\n kicker=\"Welcome back\"\n title=\"Sign in to Mentix\"\n subtitle=\"Continue your surgical training journey.\"\n footer={\n <>\n Don't have an account?{\" \"}\n <Link href=\"/signup\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Create one\n </Link>\n </>\n }\n >\n {/* ── State banners ── */}\n {state === \"suspended\" && (\n <div className=\"mb-5\">\n <InlineAlert\n variant=\"error\"\n title=\"Account suspended\"\n action={\n <Link href=\"#\" className=\"text-[12px] font-medium hover:underline underline-offset-4\">Contact support →</Link>\n }\n >\n Your account has been temporarily suspended. Please contact support to restore access.\n </InlineAlert>\n </div>\n )}\n\n {state === \"error\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title=\"Invalid credentials\">\n The email or password you entered is incorrect. Try again or reset your password.\n </InlineAlert>\n </div>\n )}\n\n {state === \"magic-sent\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"success\" title=\"Magic link sent\">\n We sent a sign-in link to <strong>{email}</strong>. It expires in 15 minutes.\n </InlineAlert>\n </div>\n )}\n\n {/* ── Social SSO ── */}\n <SocialButtons\n onClick={(p) => { setSocialLoading(p); setTimeout(() => setSocialLoading(null), 1500); }}\n loading={socialLoading}\n />\n\n <OrDivider />\n\n {/* ── Form ── */}\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n <Field label=\"Work email\" htmlFor=\"email\">\n <Input\n id=\"email\"\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"you@hospital.com\"\n autoComplete=\"email\"\n className=\"h-10\"\n required\n aria-invalid={state === \"error\"}\n />\n </Field>\n\n {mode === \"password\" && (\n <Field\n label=\"Password\"\n htmlFor=\"password\"\n error={state === \"error\" ? \"Incorrect password\" : undefined}\n >\n <PasswordInput\n id=\"password\"\n value={password}\n onChange={setPassword}\n placeholder=\"Enter your password\"\n autoComplete=\"current-password\"\n ariaInvalid={state === \"error\"}\n />\n </Field>\n )}\n\n <div className=\"flex items-center justify-between -mt-1\">\n {mode === \"password\" ? (\n <Checkbox id=\"remember\" checked={remember} onChange={setRemember}>\n Keep me signed in\n </Checkbox>\n ) : (\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>\n We'll email you a one-time sign-in link.\n </p>\n )}\n <Link\n href=\"/forgot-password\"\n className=\"text-[12px] font-medium hover:underline underline-offset-4\"\n style={{ color: \"var(--color-text-brand)\" }}\n >\n Forgot password?\n </Link>\n </div>\n\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" disabled={state === \"loading\"} className=\"mt-1\">\n {state === \"loading\" ? (\n <><Spinner className=\"w-4 h-4 mr-1\" /> Signing in…</>\n ) : mode === \"magic\" ? (\n <>Email me a sign-in link <ArrowRight size={14} /></>\n ) : (\n <>Sign in <ArrowRight size={14} /></>\n )}\n </Button>\n\n <button\n type=\"button\"\n onClick={() => { setMode(mode === \"password\" ? \"magic\" : \"password\"); setMagicSent(false); }}\n className=\"text-[12px] inline-flex items-center justify-center gap-1.5 mt-1 transition-opacity hover:opacity-70\"\n style={{ color: \"var(--color-text-secondary)\" }}\n >\n <Mail size={12} strokeWidth={1.8} />\n {mode === \"password\" ? \"Email me a magic link instead\" : \"Use email & password instead\"}\n </button>\n </form>\n </AuthShell>\n );\n}"
|
|
25
|
-
}
|
|
26
|
-
],
|
|
27
|
-
"content": "\"use client\";\nimport Link from \"next/link\";\nimport { useState, useMemo } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { ArrowRight, Mail } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { AuthShell } from \"@/components/auth/auth-shell\";\nimport {\n Field, PasswordInput, OrDivider, InlineAlert, Checkbox, SocialButtons, Spinner,\n} from \"@/components/auth/auth-primitives\";\n\ntype Mode = \"password\" | \"magic\";\ntype State = \"idle\" | \"loading\" | \"error\" | \"suspended\" | \"magic-sent\";\n\nexport default function LoginPage() {\n const search = useSearchParams();\n // Dev-time state override via ?state=\n const forced = search.get(\"state\") as State | null;\n\n const [mode, setMode] = useState<Mode>(\"password\");\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [remember, setRemember] = useState(true);\n const [socialLoading, setSocialLoading] = useState<null | \"google\" | \"apple\" | \"github\">(null);\n const [submitting, setSubmitting] = useState(false);\n const [errorKind, setErrorKind] = useState<null | \"invalid\" | \"suspended\">(null);\n const [magicSent, setMagicSent] = useState(false);\n\n const state: State = useMemo(() => {\n if (forced) return forced;\n if (submitting) return \"loading\";\n if (errorKind === \"suspended\") return \"suspended\";\n if (errorKind === \"invalid\") return \"error\";\n if (magicSent) return \"magic-sent\";\n return \"idle\";\n }, [forced, submitting, errorKind, magicSent]);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setSubmitting(true);\n setErrorKind(null);\n setTimeout(() => {\n setSubmitting(false);\n // Demo branches based on email\n if (email.includes(\"suspended\")) setErrorKind(\"suspended\");\n else if (email !== \"demo@mentix.world\" || password !== \"Demo1234!\") setErrorKind(\"invalid\");\n else if (mode === \"magic\") setMagicSent(true);\n }, 1200);\n };\n\n return (\n <AuthShell\n kicker=\"Welcome back\"\n title=\"Sign in to Mentix\"\n subtitle=\"Continue your surgical training journey.\"\n footer={\n <>\n Don't have an account?{\" \"}\n <Link href=\"/signup\" className=\"font-medium hover:underline underline-offset-4\" style={{ color: \"var(--color-text-brand)\" }}>\n Create one\n </Link>\n </>\n }\n >\n {/* ── State banners ── */}\n {state === \"suspended\" && (\n <div className=\"mb-5\">\n <InlineAlert\n variant=\"error\"\n title=\"Account suspended\"\n action={\n <Link href=\"#\" className=\"text-[12px] font-medium hover:underline underline-offset-4\">Contact support →</Link>\n }\n >\n Your account has been temporarily suspended. Please contact support to restore access.\n </InlineAlert>\n </div>\n )}\n\n {state === \"error\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"error\" title=\"Invalid credentials\">\n The email or password you entered is incorrect. Try again or reset your password.\n </InlineAlert>\n </div>\n )}\n\n {state === \"magic-sent\" && (\n <div className=\"mb-5\">\n <InlineAlert variant=\"success\" title=\"Magic link sent\">\n We sent a sign-in link to <strong>{email}</strong>. It expires in 15 minutes.\n </InlineAlert>\n </div>\n )}\n\n {/* ── Social SSO ── */}\n <SocialButtons\n onClick={(p) => { setSocialLoading(p); setTimeout(() => setSocialLoading(null), 1500); }}\n loading={socialLoading}\n />\n\n <OrDivider />\n\n {/* ── Form ── */}\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\" noValidate>\n <Field label=\"Work email\" htmlFor=\"email\">\n <Input\n id=\"email\"\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder=\"you@hospital.com\"\n autoComplete=\"email\"\n className=\"h-10\"\n required\n aria-invalid={state === \"error\"}\n />\n </Field>\n\n {mode === \"password\" && (\n <Field\n label=\"Password\"\n htmlFor=\"password\"\n error={state === \"error\" ? \"Incorrect password\" : undefined}\n >\n <PasswordInput\n id=\"password\"\n value={password}\n onChange={setPassword}\n placeholder=\"Enter your password\"\n autoComplete=\"current-password\"\n ariaInvalid={state === \"error\"}\n />\n </Field>\n )}\n\n <div className=\"flex items-center justify-between -mt-1\">\n {mode === \"password\" ? (\n <Checkbox id=\"remember\" checked={remember} onChange={setRemember}>\n Keep me signed in\n </Checkbox>\n ) : (\n <p className=\"text-[12px]\" style={{ color: \"var(--color-text-muted)\" }}>\n We'll email you a one-time sign-in link.\n </p>\n )}\n <Link\n href=\"/forgot-password\"\n className=\"text-[12px] font-medium hover:underline underline-offset-4\"\n style={{ color: \"var(--color-text-brand)\" }}\n >\n Forgot password?\n </Link>\n </div>\n\n <Button type=\"submit\" variant=\"brand\" size=\"lg\" disabled={state === \"loading\"} className=\"mt-1\">\n {state === \"loading\" ? (\n <><Spinner className=\"w-4 h-4 mr-1\" /> Signing in…</>\n ) : mode === \"magic\" ? (\n <>Email me a sign-in link <ArrowRight size={14} /></>\n ) : (\n <>Sign in <ArrowRight size={14} /></>\n )}\n </Button>\n\n <button\n type=\"button\"\n onClick={() => { setMode(mode === \"password\" ? \"magic\" : \"password\"); setMagicSent(false); }}\n className=\"text-[12px] inline-flex items-center justify-center gap-1.5 mt-1 transition-opacity hover:opacity-70\"\n style={{ color: \"var(--color-text-secondary)\" }}\n >\n <Mail size={12} strokeWidth={1.8} />\n {mode === \"password\" ? \"Email me a magic link instead\" : \"Use email & password instead\"}\n </button>\n </form>\n </AuthShell>\n );\n}\n"
|
|
28
|
-
}
|